diff --git a/.editorconfig b/.editorconfig index f5544116c07..ba58fbd2d2e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,129 +1,127 @@ # To learn more about .editorconfig see https://aka.ms/editorconfigdocs -############################### -# Core EditorConfig Options # -############################### -# All files -[*] -indent_style = space -insert_final_newline = true -# Code files -[*.{cs,csx,vb,vbx}] -indent_size = 4 -trim_trailing_whitespace = true -charset = utf-8 -############################### -# .NET Coding Conventions # -############################### -[*.{cs,vb}] -# Organize usings -dotnet_sort_system_directives_first = true -# this. preferences -dotnet_style_qualification_for_field = true:suggestion -dotnet_style_qualification_for_property = true:suggestion -dotnet_style_qualification_for_method = true:suggestion -dotnet_style_qualification_for_event = true:suggestion -# Language keywords vs BCL types preferences -dotnet_style_predefined_type_for_locals_parameters_members = true:silent -dotnet_style_predefined_type_for_member_access = true:silent -# 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 -# Modifier preferences -dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent -dotnet_style_readonly_field = true:suggestion -# Expression-level preferences -dotnet_style_object_initializer = true:suggestion -dotnet_style_collection_initializer = true:suggestion -dotnet_style_explicit_tuple_names = true:suggestion -dotnet_style_null_propagation = true:suggestion -dotnet_style_coalesce_expression = true:suggestion -dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent -dotnet_prefer_inferred_tuple_names = true:suggestion -dotnet_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 -############################### -# Naming Conventions # -############################### -# Style Definitions -dotnet_naming_style.pascal_case_style.capitalization = pascal_case -# Use PascalCase for constant fields -dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion -dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields -dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style -dotnet_naming_symbols.constant_fields.applicable_kinds = field -dotnet_naming_symbols.constant_fields.applicable_accessibilities = * -dotnet_naming_symbols.constant_fields.required_modifiers = const -############################### -# C# Coding Conventions # -############################### -[*.cs] +############################### +# Core EditorConfig Options # +############################### +# All files +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true +[*.{cs,cshtml,htm,html,md,py,sln,xml}] +indent_size = 4 +############################### +# .NET Coding Conventions # +############################### +[*.cs] +# Organize usings +dotnet_sort_system_directives_first = true +# this. preferences +dotnet_style_qualification_for_field = true:suggestion +dotnet_style_qualification_for_property = true:suggestion +dotnet_style_qualification_for_method = true:suggestion +dotnet_style_qualification_for_event = true:suggestion +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true:silent +dotnet_style_predefined_type_for_member_access = true:silent +# 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 +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent +dotnet_style_readonly_field = true:suggestion +# Expression-level preferences +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent +dotnet_prefer_inferred_tuple_names = true:suggestion +dotnet_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 +############################### +# Naming Conventions # +############################### +# Style Definitions +dotnet_naming_style.pascal_case_style.capitalization = pascal_case +# Use PascalCase for constant fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style +dotnet_naming_symbols.constant_fields.applicable_kinds = field +dotnet_naming_symbols.constant_fields.applicable_accessibilities = * +dotnet_naming_symbols.constant_fields.required_modifiers = const +############################### +# C# Coding Conventions # +############################### +[*.cs] dotnet_diagnostic.CA1031.severity = none dotnet_diagnostic.CA1303.severity = none -# var preferences -csharp_style_var_for_built_in_types = true:silent -csharp_style_var_when_type_is_apparent = true:silent -csharp_style_var_elsewhere = true:silent -# 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 -# Pattern matching preferences -csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion -csharp_style_pattern_matching_over_as_with_null_check = true:suggestion -# Null-checking preferences -csharp_style_throw_expression = true:suggestion -csharp_style_conditional_delegate_call = true:suggestion -# Modifier preferences -csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion -# Expression-level preferences -csharp_prefer_braces = true:silent -csharp_style_deconstructed_variable_declaration = true:suggestion -csharp_prefer_simple_default_expression = true:suggestion -csharp_style_pattern_local_over_anonymous_function = true:suggestion -csharp_style_inlined_variable_declaration = true:suggestion -############################### -# C# Formatting Rules # -############################### -# New line preferences -csharp_new_line_before_open_brace = all -csharp_new_line_before_else = true -csharp_new_line_before_catch = true -csharp_new_line_before_finally = true -csharp_new_line_before_members_in_object_initializers = true -csharp_new_line_before_members_in_anonymous_types = true -csharp_new_line_between_query_expression_clauses = true -# Indentation preferences -csharp_indent_case_contents = true -csharp_indent_switch_labels = true -csharp_indent_labels = flush_left -# Space preferences -csharp_space_after_cast = false -csharp_space_after_keywords_in_control_flow_statements = true -csharp_space_between_method_call_parameter_list_parentheses = false -csharp_space_between_method_declaration_parameter_list_parentheses = false -csharp_space_between_parentheses = false -csharp_space_before_colon_in_inheritance_clause = true -csharp_space_after_colon_in_inheritance_clause = true -csharp_space_around_binary_operators = before_and_after -csharp_space_between_method_declaration_empty_parameter_list_parentheses = false -csharp_space_between_method_call_name_and_opening_parenthesis = false -csharp_space_between_method_call_empty_parameter_list_parentheses = false -# Wrapping preferences -csharp_preserve_single_line_statements = true -csharp_preserve_single_line_blocks = true -############################### -# VB Coding Conventions # -############################### -[*.vb] -# Modifier preferences -visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:suggestion +dotnet_diagnostic.IDE0001.severity = warning +dotnet_diagnostic.IDE0002.severity = warning +dotnet_diagnostic.IDE0005.severity = warning +# var preferences +csharp_style_var_for_built_in_types = true:silent +csharp_style_var_when_type_is_apparent = true:silent +csharp_style_var_elsewhere = true:silent +# 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 +# Pattern matching preferences +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +# Null-checking preferences +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion +# Modifier preferences +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion +# Expression-level preferences +csharp_prefer_braces = true:silent +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_pattern_local_over_anonymous_function = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +############################### +# C# Formatting Rules # +############################### +# New line preferences +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true +# Indentation preferences +csharp_indent_case_contents = true +csharp_indent_switch_labels = true +csharp_indent_labels = flush_left +# Space preferences +csharp_space_after_cast = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_around_binary_operators = before_and_after +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +# Wrapping preferences +csharp_preserve_single_line_statements = true +csharp_preserve_single_line_blocks = true + [obj/**.cs] generated_code = true diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index e591c4fa405..2216bfbe17b 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -7,14 +7,14 @@ labels: bug # Bug Report List of [NuGet packages](https://www.nuget.org/profiles/OpenTelemetry) and -version that you are using (e.g. `OpenTelemetry 0.4.0-beta.2`): +version that you are using (e.g. `OpenTelemetry 1.0.2`): -* +* TBD -Runtime version (e.g. `net461`, `net48`, `netcoreapp2.1`, `netcoreapp3.1`, etc. -You can find this information from the `*.csproj` file): +Runtime version (e.g. `net461`, `net48`, `netcoreapp3.1`, `net5.0` etc. You can +find this information from the `*.csproj` file): -* +* TBD ## Symptom diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 2d43531690c..e5ef779df58 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -6,6 +6,6 @@ Please provide a brief description of the changes here. For significant contributions please make sure you have completed the following items: -* [ ] `CHANGELOG.md` updated for non-trivial changes +* [ ] Appropriate `CHANGELOG.md` updated for non-trivial changes * [ ] Design discussion issue # * [ ] Changes in public API reviewed diff --git a/.github/workflows/apicompatibility.yml b/.github/workflows/apicompatibility.yml index 108a465b122..3efbecd4a18 100644 --- a/.github/workflows/apicompatibility.yml +++ b/.github/workflows/apicompatibility.yml @@ -9,11 +9,18 @@ on: jobs: build-test: runs-on: windows-latest + # https://github.com/actions/setup-dotnet/issues/122 env: CheckAPICompatibility: true + DOTNET_MULTILEVEL_LOOKUP: 1 steps: - uses: actions/checkout@v2 + - uses: actions/setup-dotnet@v1 + with: + dotnet-version: '6.0.x' + include-prerelease: true + - name: Install dependencies run: dotnet restore diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 8228a7bd12a..0bcc696a042 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -13,6 +13,7 @@ on: jobs: build-test-report: runs-on: ${{ matrix.os }} + # https://github.com/actions/setup-dotnet/issues/122 strategy: fail-fast: false @@ -20,12 +21,18 @@ jobs: os: [windows-latest] env: OS: ${{ matrix.os }} + DOTNET_MULTILEVEL_LOOKUP: 1 steps: - uses: actions/checkout@v2 with: fetch-depth: 0 # fetching all + - uses: actions/setup-dotnet@v1 + with: + dotnet-version: '6.0.x' + include-prerelease: true + - name: Install dependencies run: dotnet restore diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 4645a04c493..39a8839c57e 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -21,7 +21,7 @@ jobs: # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] language: ['csharp'] # Learn more... - # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection + # https://docs.github.com/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection steps: - name: configure Pagefile diff --git a/.github/workflows/docfx.yml b/.github/workflows/docfx.yml index a7a94fd508a..1e0b540dba9 100644 --- a/.github/workflows/docfx.yml +++ b/.github/workflows/docfx.yml @@ -15,7 +15,9 @@ jobs: uses: actions/checkout@v2 - name: install docfx - run: choco install docfx -y + # specifying 2.58.5 version as latest release (2.58.8) has an issue + # https://github.com/dotnet/docfx/issues/7689 + run: choco install docfx -y --version=2.58.5 - name: run .\build\docfx.cmd shell: cmd diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index b752f10ae93..a0970cb6a22 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -13,44 +13,80 @@ on: jobs: redis-test: runs-on: ubuntu-latest + # https://github.com/actions/setup-dotnet/issues/122 + env: + DOTNET_MULTILEVEL_LOOKUP: 1 strategy: fail-fast: false matrix: - version: [netcoreapp3.1,net5.0] + version: [netcoreapp3.1,net5.0,net6.0] steps: - uses: actions/checkout@v2 + + - uses: actions/setup-dotnet@v1 + with: + dotnet-version: '6.0.x' + include-prerelease: true + - name: Run redis docker-compose.integration run: docker-compose --file=test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/docker-compose.yml --file=build/docker-compose.${{ matrix.version }}.yml --project-directory=. up --exit-code-from=tests --build sql-test: runs-on: ubuntu-latest + # https://github.com/actions/setup-dotnet/issues/122 + env: + DOTNET_MULTILEVEL_LOOKUP: 1 strategy: fail-fast: false matrix: - version: [netcoreapp3.1,net5.0] + version: [netcoreapp3.1,net5.0,net6.0] steps: - uses: actions/checkout@v2 + + - uses: actions/setup-dotnet@v1 + with: + dotnet-version: '6.0.x' + include-prerelease: true + - name: Run sql docker-compose.integration run: docker-compose --file=test/OpenTelemetry.Instrumentation.SqlClient.Tests/docker-compose.yml --file=build/docker-compose.${{ matrix.version }}.yml --project-directory=. up --exit-code-from=tests --build w3c-trace-context-test: runs-on: ubuntu-latest + # https://github.com/actions/setup-dotnet/issues/122 + env: + DOTNET_MULTILEVEL_LOOKUP: 1 strategy: fail-fast: false matrix: - version: [netcoreapp3.1,net5.0] + version: [netcoreapp3.1,net5.0,net6.0] steps: - uses: actions/checkout@v2 + + - uses: actions/setup-dotnet@v1 + with: + dotnet-version: '6.0.x' + include-prerelease: true + - name: Run W3C Trace Context docker-compose.integration run: docker-compose --file=test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/docker-compose.yml --file=build/docker-compose.${{ matrix.version }}.yml --project-directory=. up --exit-code-from=tests --build otlp-exporter-test: runs-on: ubuntu-latest + # https://github.com/actions/setup-dotnet/issues/122 + env: + DOTNET_MULTILEVEL_LOOKUP: 1 strategy: fail-fast: false matrix: - version: [netcoreapp3.1,net5.0] + version: [netcoreapp3.1,net5.0,net6.0] steps: - uses: actions/checkout@v2 + + - uses: actions/setup-dotnet@v1 + with: + dotnet-version: '6.0.x' + include-prerelease: true + - name: Run OTLP Exporter docker-compose.integration run: docker-compose --file=test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/docker-compose.yml --file=build/docker-compose.${{ matrix.version }}.yml --project-directory=. up --exit-code-from=tests --build diff --git a/.github/workflows/linux-ci.yml b/.github/workflows/linux-ci.yml index f66fb6dd966..7f90e2e9b78 100644 --- a/.github/workflows/linux-ci.yml +++ b/.github/workflows/linux-ci.yml @@ -13,14 +13,30 @@ on: jobs: build-test: runs-on: ubuntu-latest + # https://github.com/actions/setup-dotnet/issues/122 + env: + DOTNET_MULTILEVEL_LOOKUP: 1 strategy: matrix: - version: [netcoreapp3.1,net5.0] + version: [netcoreapp3.1,net5.0,net6.0] steps: - uses: actions/checkout@v2 + - uses: actions/setup-dotnet@v1 + with: + dotnet-version: '3.1.x' + + - uses: actions/setup-dotnet@v1 + with: + dotnet-version: '5.0.x' + + - uses: actions/setup-dotnet@v1 + with: + dotnet-version: '6.0.x' + include-prerelease: true + - name: Install dependencies run: dotnet restore @@ -28,4 +44,6 @@ jobs: run: dotnet build --configuration Release --no-restore - name: Test ${{ matrix.version }} + env: + DOTNET_MULTILEVEL_LOOKUP: 1 run: dotnet test **/bin/**/${{ matrix.version }}/*.Tests.dll --configuration Release --no-build --logger:"console;verbosity=detailed" diff --git a/.github/workflows/publish-packages-1.0.yml b/.github/workflows/publish-packages-1.0.yml index ed5b4890236..971ab5971e6 100644 --- a/.github/workflows/publish-packages-1.0.yml +++ b/.github/workflows/publish-packages-1.0.yml @@ -32,6 +32,11 @@ jobs: fetch-depth: 0 # fetching all ref: ${{ matrix.branches }} + - uses: actions/setup-dotnet@v1 + with: + dotnet-version: '6.0.x' + include-prerelease: true + - name: Install dependencies run: dotnet restore diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 00000000000..07754b541f3 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,21 @@ +# 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" +on: + schedule: + - cron: "12 3 * * *" # arbitrary time not to DDOS GitHub + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v4 + with: + stale-pr-message: 'This PR was marked stale due to lack of activity. It will be closed in 7 days.' + 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-pr-close: 7 + days-before-issue-close: -1 diff --git a/.github/workflows/windows-ci.yml b/.github/workflows/windows-ci.yml index de5cdc95c81..8f386330335 100644 --- a/.github/workflows/windows-ci.yml +++ b/.github/workflows/windows-ci.yml @@ -13,14 +13,22 @@ on: jobs: build-test: runs-on: windows-latest + # https://github.com/actions/setup-dotnet/issues/122 + env: + DOTNET_MULTILEVEL_LOOKUP: 1 strategy: matrix: - version: [net461,netcoreapp3.1,net5.0] + version: [net461,netcoreapp3.1,net5.0,net6.0] steps: - uses: actions/checkout@v2 + - uses: actions/setup-dotnet@v1 + with: + dotnet-version: '6.0.x' + include-prerelease: true + - name: Install dependencies run: dotnet restore diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d8cf9f99854..a8455b9c787 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -48,16 +48,16 @@ On all platforms, the minimum requirements are: ### Linux or MacOS -* Visual Studio for Mac or Visual Studio Code +* Visual Studio 2022+ for Mac or Visual Studio Code Mono might be required by your IDE but is not required by this project. This is -because unit tests targeting .NET Framework (i.e: `net46`) are disabled outside +because unit tests targeting .NET Framework (i.e: `net461`) are disabled outside of Windows. ### Windows -* Visual Studio 2017+ or Visual Studio Code -* .NET Framework 4.6+ +* Visual Studio 2022+ or Visual Studio Code +* .NET Framework 4.6.1+ ### Public API @@ -76,14 +76,14 @@ helper methods. in each framework that you target. * Add the following lines to your csproj: + ```xml - - + + ``` + * Use [IntelliSense](https://docs.microsoft.com/visualstudio/ide/using-intellisense) diff --git a/OpenTelemetry.sln b/OpenTelemetry.sln index ec69c97b5fc..1a12155fb11 100644 --- a/OpenTelemetry.sln +++ b/OpenTelemetry.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29123.89 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.32014.148 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry", "src\OpenTelemetry\OpenTelemetry.csproj", "{AE3E3DF5-4083-4C6E-A840-8271B0ACDE7E}" EndProject @@ -65,8 +65,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Shims.OpenTra EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Shims.OpenTracing.Tests", "test\OpenTelemetry.Shims.OpenTracing.Tests\OpenTelemetry.Shims.OpenTracing.Tests.csproj", "{49A7853F-5B6F-4B65-A781-7D29A1C92164}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{0169B149-FB8B-46F4-9EF7-8A0E69F8FAAF}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Extensions.Hosting", "src\OpenTelemetry.Extensions.Hosting\OpenTelemetry.Extensions.Hosting.csproj", "{A03102CD-A996-4418-BA19-7141C34B5A7D}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Extensions.Hosting.Tests", "test\OpenTelemetry.Extensions.Hosting.Tests\OpenTelemetry.Extensions.Hosting.Tests.csproj", "{4CB1187F-27F2-48F9-83F4-3A7567F73B5F}" @@ -159,10 +157,6 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "getting-started", "docs\trace\getting-started\getting-started.csproj", "{BE60E3D5-DE30-4BAB-8E7A-63B21D0E80D7}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "metrics", "metrics", "{3277B1C0-BDFE-4460-9B0D-D9A661FB48DB}" - ProjectSection(SolutionItems) = preProject - docs\metrics\building-your-own-exporter.md = docs\metrics\building-your-own-exporter.md - docs\metrics\README.md = docs\metrics\README.md - EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "logs", "logs", "{3862190B-E2C5-418E-AFDC-DB281FB5C705}" EndProject @@ -198,23 +192,39 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Exporter.InMe EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "extending-the-sdk", "docs\logs\extending-the-sdk\extending-the-sdk.csproj", "{13C10C9A-07E8-43EB-91F5-C2B116FBE0FC}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Shared", "src\OpenTelemetry.Shared\OpenTelemetry.Shared.csproj", "{1E504265-1E32-4C61-8CC5-8FA373E16699}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestApp.AspNetCore.5.0", "test\TestApp.AspNetCore.5.0\TestApp.AspNetCore.5.0.csproj", "{972396A8-E35B-499C-9BA1-765E9B8822E1}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "exception-reporting", "docs\trace\exception-reporting\exception-reporting.csproj", "{08D29501-F0A3-468F-B18D-BD1821A72383}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "reporting-exceptions", "docs\trace\reporting-exceptions\reporting-exceptions.csproj", "{08D29501-F0A3-468F-B18D-BD1821A72383}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "customizing-the-sdk", "docs\trace\customizing-the-sdk\customizing-the-sdk.csproj", "{64E3D8BB-93AB-4571-93F7-ED8D64DFFD06}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "getting-started", "docs\metrics\getting-started\getting-started.csproj", "{DFB0AD2F-11BE-4BCD-A77B-1018C3344FA8}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Exporter.Prometheus", "src\OpenTelemetry.Exporter.Prometheus\OpenTelemetry.Exporter.Prometheus.csproj", "{52158A12-E7EF-45A1-859F-06F9B17410CB}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule", "src\OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule\OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.csproj", "{F38E511B-1877-4E8A-8051-7879FC7DF8A4}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.Tests", "test\OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.Tests\OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.Tests.csproj", "{4D7201BC-7124-4401-AD65-FAB58A053D45}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "getting-started-histogram", "docs\metrics\getting-started-histogram\getting-started-histogram.csproj", "{92ED77A6-37B4-447D-B4C4-15DB005A589C}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "learning-more-instruments", "docs\metrics\learning-more-instruments\learning-more-instruments.csproj", "{E7F491CC-C37E-4A56-9CA7-8F77F59E0614}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "getting-started", "docs\metrics\getting-started\getting-started.csproj", "{EA60B549-F712-4ABE-8E44-FCA83B78C06E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "extending-the-sdk", "docs\metrics\extending-the-sdk\extending-the-sdk.csproj", "{1F9D7748-D099-4E25-97F5-9C969D6FF969}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Tests.Stress.Metrics", "test\OpenTelemetry.Tests.Stress.Metrics\OpenTelemetry.Tests.Stress.Metrics.csproj", "{A885DBE2-4B82-432C-A77B-19844D7BBC96}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "customizing-the-sdk", "docs\metrics\customizing-the-sdk\customizing-the-sdk.csproj", "{81234AFA-B4E7-4D0D-AB97-FD559C78EDA2}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Tests.Stress", "test\OpenTelemetry.Tests.Stress\OpenTelemetry.Tests.Stress.csproj", "{2770158A-D220-414B-ABC6-179371323579}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Exporter.Prometheus.Tests", "test\OpenTelemetry.Exporter.Prometheus.Tests\OpenTelemetry.Exporter.Prometheus.Tests.csproj", "{380EE686-91F1-45B3-AEEB-755F0E5B068F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs", "src\OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs\OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs.csproj", "{6E1A5FA3-E024-4972-9EDC-11E36C5A0D6F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestApp.AspNetCore.6.0", "test\TestApp.AspNetCore.6.0\TestApp.AspNetCore.6.0.csproj", "{0076C657-564F-4787-9FFF-52D9D55166E8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "source-generation", "docs\logs\source-generation\source-generation.csproj", "{1F6CC903-04C9-4E7C-B388-C215C467BFB9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "getting-started-prometheus-grafana", "docs\metrics\getting-started-prometheus-grafana\getting-started-prometheus-grafana.csproj", "{41B784AA-3301-4126-AF9F-1D59BD04B0BF}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -394,10 +404,6 @@ Global {13C10C9A-07E8-43EB-91F5-C2B116FBE0FC}.Debug|Any CPU.Build.0 = Debug|Any CPU {13C10C9A-07E8-43EB-91F5-C2B116FBE0FC}.Release|Any CPU.ActiveCfg = Release|Any CPU {13C10C9A-07E8-43EB-91F5-C2B116FBE0FC}.Release|Any CPU.Build.0 = Release|Any CPU - {1E504265-1E32-4C61-8CC5-8FA373E16699}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1E504265-1E32-4C61-8CC5-8FA373E16699}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1E504265-1E32-4C61-8CC5-8FA373E16699}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1E504265-1E32-4C61-8CC5-8FA373E16699}.Release|Any CPU.Build.0 = Release|Any CPU {972396A8-E35B-499C-9BA1-765E9B8822E1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {972396A8-E35B-499C-9BA1-765E9B8822E1}.Debug|Any CPU.Build.0 = Debug|Any CPU {972396A8-E35B-499C-9BA1-765E9B8822E1}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -410,10 +416,6 @@ Global {64E3D8BB-93AB-4571-93F7-ED8D64DFFD06}.Debug|Any CPU.Build.0 = Debug|Any CPU {64E3D8BB-93AB-4571-93F7-ED8D64DFFD06}.Release|Any CPU.ActiveCfg = Release|Any CPU {64E3D8BB-93AB-4571-93F7-ED8D64DFFD06}.Release|Any CPU.Build.0 = Release|Any CPU - {DFB0AD2F-11BE-4BCD-A77B-1018C3344FA8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DFB0AD2F-11BE-4BCD-A77B-1018C3344FA8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DFB0AD2F-11BE-4BCD-A77B-1018C3344FA8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DFB0AD2F-11BE-4BCD-A77B-1018C3344FA8}.Release|Any CPU.Build.0 = Release|Any CPU {52158A12-E7EF-45A1-859F-06F9B17410CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {52158A12-E7EF-45A1-859F-06F9B17410CB}.Debug|Any CPU.Build.0 = Debug|Any CPU {52158A12-E7EF-45A1-859F-06F9B17410CB}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -426,10 +428,50 @@ Global {4D7201BC-7124-4401-AD65-FAB58A053D45}.Debug|Any CPU.Build.0 = Debug|Any CPU {4D7201BC-7124-4401-AD65-FAB58A053D45}.Release|Any CPU.ActiveCfg = Release|Any CPU {4D7201BC-7124-4401-AD65-FAB58A053D45}.Release|Any CPU.Build.0 = Release|Any CPU - {92ED77A6-37B4-447D-B4C4-15DB005A589C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {92ED77A6-37B4-447D-B4C4-15DB005A589C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {92ED77A6-37B4-447D-B4C4-15DB005A589C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {92ED77A6-37B4-447D-B4C4-15DB005A589C}.Release|Any CPU.Build.0 = Release|Any CPU + {E7F491CC-C37E-4A56-9CA7-8F77F59E0614}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E7F491CC-C37E-4A56-9CA7-8F77F59E0614}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E7F491CC-C37E-4A56-9CA7-8F77F59E0614}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E7F491CC-C37E-4A56-9CA7-8F77F59E0614}.Release|Any CPU.Build.0 = Release|Any CPU + {EA60B549-F712-4ABE-8E44-FCA83B78C06E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EA60B549-F712-4ABE-8E44-FCA83B78C06E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EA60B549-F712-4ABE-8E44-FCA83B78C06E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EA60B549-F712-4ABE-8E44-FCA83B78C06E}.Release|Any CPU.Build.0 = Release|Any CPU + {1F9D7748-D099-4E25-97F5-9C969D6FF969}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1F9D7748-D099-4E25-97F5-9C969D6FF969}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1F9D7748-D099-4E25-97F5-9C969D6FF969}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1F9D7748-D099-4E25-97F5-9C969D6FF969}.Release|Any CPU.Build.0 = Release|Any CPU + {A885DBE2-4B82-432C-A77B-19844D7BBC96}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A885DBE2-4B82-432C-A77B-19844D7BBC96}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A885DBE2-4B82-432C-A77B-19844D7BBC96}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A885DBE2-4B82-432C-A77B-19844D7BBC96}.Release|Any CPU.Build.0 = Release|Any CPU + {81234AFA-B4E7-4D0D-AB97-FD559C78EDA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {81234AFA-B4E7-4D0D-AB97-FD559C78EDA2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {81234AFA-B4E7-4D0D-AB97-FD559C78EDA2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {81234AFA-B4E7-4D0D-AB97-FD559C78EDA2}.Release|Any CPU.Build.0 = Release|Any CPU + {2770158A-D220-414B-ABC6-179371323579}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2770158A-D220-414B-ABC6-179371323579}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2770158A-D220-414B-ABC6-179371323579}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2770158A-D220-414B-ABC6-179371323579}.Release|Any CPU.Build.0 = Release|Any CPU + {380EE686-91F1-45B3-AEEB-755F0E5B068F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {380EE686-91F1-45B3-AEEB-755F0E5B068F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {380EE686-91F1-45B3-AEEB-755F0E5B068F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {380EE686-91F1-45B3-AEEB-755F0E5B068F}.Release|Any CPU.Build.0 = Release|Any CPU + {6E1A5FA3-E024-4972-9EDC-11E36C5A0D6F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6E1A5FA3-E024-4972-9EDC-11E36C5A0D6F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6E1A5FA3-E024-4972-9EDC-11E36C5A0D6F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6E1A5FA3-E024-4972-9EDC-11E36C5A0D6F}.Release|Any CPU.Build.0 = Release|Any CPU + {0076C657-564F-4787-9FFF-52D9D55166E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0076C657-564F-4787-9FFF-52D9D55166E8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0076C657-564F-4787-9FFF-52D9D55166E8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0076C657-564F-4787-9FFF-52D9D55166E8}.Release|Any CPU.Build.0 = Release|Any CPU + {1F6CC903-04C9-4E7C-B388-C215C467BFB9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1F6CC903-04C9-4E7C-B388-C215C467BFB9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1F6CC903-04C9-4E7C-B388-C215C467BFB9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1F6CC903-04C9-4E7C-B388-C215C467BFB9}.Release|Any CPU.Build.0 = Release|Any CPU + {41B784AA-3301-4126-AF9F-1D59BD04B0BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {41B784AA-3301-4126-AF9F-1D59BD04B0BF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {41B784AA-3301-4126-AF9F-1D59BD04B0BF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {41B784AA-3301-4126-AF9F-1D59BD04B0BF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -443,7 +485,6 @@ Global {9A4E3A68-904B-4835-A3C8-F664B73098DB} = {E359BB2B-9AEC-497D-B321-7DF2450C3B8E} {FF3E6E08-E8E4-4523-B526-847CD989279F} = {E359BB2B-9AEC-497D-B321-7DF2450C3B8E} {0935622B-9377-4056-8343-AE6ECDC274CF} = {E359BB2B-9AEC-497D-B321-7DF2450C3B8E} - {DE9130A4-F30A-49D7-8834-41DE3021218B} = {0169B149-FB8B-46F4-9EF7-8A0E69F8FAAF} {2C7DD1DA-C229-4D9E-9AF0-BCD5CD3E4948} = {7CB2F02E-03FA-4FFF-89A5-C51F107623FD} {5B7FB835-3FFF-4BC2-99C5-A5B5FAE3C818} = {7C87CAF9-79D7-4C26-9FFB-F3F1FB6911F1} {BE60E3D5-DE30-4BAB-8E7A-63B21D0E80D7} = {5B7FB835-3FFF-4BC2-99C5-A5B5FAE3C818} @@ -461,8 +502,13 @@ Global {972396A8-E35B-499C-9BA1-765E9B8822E1} = {77C7929A-2EED-4AA6-8705-B5C443C8AA0F} {08D29501-F0A3-468F-B18D-BD1821A72383} = {5B7FB835-3FFF-4BC2-99C5-A5B5FAE3C818} {64E3D8BB-93AB-4571-93F7-ED8D64DFFD06} = {5B7FB835-3FFF-4BC2-99C5-A5B5FAE3C818} - {DFB0AD2F-11BE-4BCD-A77B-1018C3344FA8} = {3277B1C0-BDFE-4460-9B0D-D9A661FB48DB} - {92ED77A6-37B4-447D-B4C4-15DB005A589C} = {3277B1C0-BDFE-4460-9B0D-D9A661FB48DB} + {E7F491CC-C37E-4A56-9CA7-8F77F59E0614} = {3277B1C0-BDFE-4460-9B0D-D9A661FB48DB} + {EA60B549-F712-4ABE-8E44-FCA83B78C06E} = {3277B1C0-BDFE-4460-9B0D-D9A661FB48DB} + {1F9D7748-D099-4E25-97F5-9C969D6FF969} = {3277B1C0-BDFE-4460-9B0D-D9A661FB48DB} + {81234AFA-B4E7-4D0D-AB97-FD559C78EDA2} = {3277B1C0-BDFE-4460-9B0D-D9A661FB48DB} + {0076C657-564F-4787-9FFF-52D9D55166E8} = {77C7929A-2EED-4AA6-8705-B5C443C8AA0F} + {1F6CC903-04C9-4E7C-B388-C215C467BFB9} = {3862190B-E2C5-418E-AFDC-DB281FB5C705} + {41B784AA-3301-4126-AF9F-1D59BD04B0BF} = {3277B1C0-BDFE-4460-9B0D-D9A661FB48DB} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {55639B5C-0770-4A22-AB56-859604650521} diff --git a/README.md b/README.md index 57de8832bcd..8644e0dc568 100644 --- a/README.md +++ b/README.md @@ -82,10 +82,7 @@ We meet weekly on Tuesdays, and the time of the meeting alternates between 11AM PT and 4PM PT. The meeting is subject to change depending on contributors' availability. Check the [OpenTelemetry community calendar](https://calendar.google.com/calendar/embed?src=google.com_b79e3e90j7bbsa2n2p5an5lf60%40group.calendar.google.com) -for specific dates. - -Meetings take place via [Zoom video conference](https://zoom.us/j/8287234601). -The passcode is `77777`. +for specific dates and for Zoom meeting links. Meeting notes are available as a public [Google doc](https://docs.google.com/document/d/1yjjD6aBcLxlRazYrawukDgrhZMObwHARJbB9glWdHj8/edit?usp=sharing). @@ -99,6 +96,7 @@ Approvers * [Eddy Nakamura](https://github.com/eddynaka), Microsoft * [Paulo Janotti](https://github.com/pjanotti), Splunk * [Reiley Yang](https://github.com/reyang), Microsoft +* [Robert Pajak](https://github.com/pellared), Splunk * [Utkarsh Umesan Pillai](https://github.com/utpilla), Microsoft *Find more about the approver role in [community diff --git a/build/Common.nonprod.props b/build/Common.nonprod.props index 23bb81f522d..7f63eee6097 100644 --- a/build/Common.nonprod.props +++ b/build/Common.nonprod.props @@ -7,11 +7,11 @@ $([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildProjectDirectory), 'OpenTelemetry.sln'))\build\OpenTelemetry.test.ruleset - + true - + PreserveNewest @@ -22,9 +22,9 @@ - [0.12.1,0.13) + [0.13.1,0.14) [2.3.0,3.0) [2.3.1,3.0) [3.15.5,4.0) @@ -35,9 +35,8 @@ [5.2.7,6.0) [3.2.7,4.0) [3.1.6,5.0) - [5.0.0,6.0) - [5.0.0,6.0) - [5.0.0,6.0) + [6.0.0,) + [6.0.0,) [16.10.0] [12.0.2,13.0) [4.14.5,5.0) diff --git a/build/Common.prod.props b/build/Common.prod.props index 1c810c880f3..c97f40a0ed9 100644 --- a/build/Common.prod.props +++ b/build/Common.prod.props @@ -6,13 +6,10 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - - - + + diff --git a/build/Common.props b/build/Common.props index 0db3be2760e..3e5a238500a 100644 --- a/build/Common.props +++ b/build/Common.props @@ -1,10 +1,11 @@ - 8.0 + 10.0 true $([System.IO.Directory]::GetParent($(MSBuildThisFileDirectory)).Parent.FullName) $(MSBuildThisFileDirectory)debug.snk $(DefineConstants);SIGNED + true @@ -19,7 +20,7 @@ [2.3.0,3.0) [3.15.5,4.0) @@ -39,9 +40,9 @@ [0.12.1,0.13) 1.1.0 [2.1.58,3.0) - [1.1.118,2.0) + [1.2.0-beta.354,2.0) 1.4.0 - 6.0.0-preview.6.21352.12 + 6.0.0 4.7.0 4.7.0 4.5.3 diff --git a/build/OpenTelemetry.prod.loose.ruleset b/build/OpenTelemetry.prod.loose.ruleset index 64b515fc681..86028efb601 100644 --- a/build/OpenTelemetry.prod.loose.ruleset +++ b/build/OpenTelemetry.prod.loose.ruleset @@ -125,7 +125,7 @@ - + @@ -168,6 +168,5 @@ - diff --git a/build/OpenTelemetry.prod.ruleset b/build/OpenTelemetry.prod.ruleset index 2bbbf6c0cf8..91dac9f4767 100644 --- a/build/OpenTelemetry.prod.ruleset +++ b/build/OpenTelemetry.prod.ruleset @@ -8,6 +8,7 @@ + diff --git a/build/RELEASING.md b/build/RELEASING.md index e1008586085..7eb5c41eaf0 100644 --- a/build/RELEASING.md +++ b/build/RELEASING.md @@ -60,9 +60,13 @@ Only for Maintainers. } ``` - 4. Submit PR with the above changes, and get it merged. + 4. Normalize PublicApi files (Stable Release Only) + Run the PowerShell script `.\build\finalize-publicapi.ps1`. + This will merge the contents of Unshipped.txt into the Shipped.txt. - 5. Tag Git with version to be released e.g.: + 5. Submit PR with the above changes, and get it merged. + + 6. Tag Git with version to be released e.g.: ```sh git tag -a 1.0.0-rc2 -m "1.0.0-rc2" @@ -83,25 +87,25 @@ Only for Maintainers. If releasing both, push both tags above. - 6. Open [Pack and publish to MyGet + 7. Open [Pack and publish to MyGet workflow](https://github.com/open-telemetry/opentelemetry-dotnet/actions/workflows/publish-packages-1.0.yml) and manually trigger a build. At the end of this, MyGet will have the packages. The package name will be the tag name used in step 5. - 7. Validate using MyGet packages. Basic sanity checks :) + 8. Validate using MyGet packages. Basic sanity checks :) - 8. From the above build, get the artifacts from the drop, which has all the + 9. From the above build, get the artifacts from the drop, which has all the NuGet packages. - 9. Copy all the NuGet files and symbols into a local folder. If only +10. Copy all the NuGet files and symbols into a local folder. If only releasing core packages, only copy them over. -10. Download latest [nuget.exe](https://www.nuget.org/downloads) into +11. Download latest [nuget.exe](https://www.nuget.org/downloads) into the same folder from step 9. -11. Obtain the API key from nuget.org (Only maintainers have access) +12. Obtain the API key from nuget.org (Only maintainers have access) -12. Run the following commands from PowerShell from local folder used in step 9: +13. Run the following commands from PowerShell from local folder used in step 9: ```powershell .\nuget.exe setApiKey @@ -111,21 +115,21 @@ Only for Maintainers. https://api.nuget.org/v3/index.json} ``` -13. Packages would be available in nuget.org in few minutes. +14. Packages would be available in nuget.org in few minutes. Validate that the package is uploaded. -14. Delete the API key generated in step 11. +15. Delete the API key generated in step 11. -15. Make the Github release with tag from Step5 +16. Make the Github release with tag from Step5 and contents of combinedchangelog from Step2. TODO: Add tagging for Metrics release. TODO: Separate version for instrumention/hosting/OTshim package. -16. Update the OpenTelemetry.io document +17. Update the OpenTelemetry.io document [here](https://github.com/open-telemetry/opentelemetry.io/tree/main/content/en/docs/net) by sending a Pull Request. -17. If a new stable version of the core packages were released, +18. If a new stable version of the core packages were released, update `OTelPreviousStableVer` in Common.props to the just released stable version. diff --git a/build/docker-compose.net5.0.yml b/build/docker-compose.net5.0.yml index fe9fe982dd4..53d82d8982d 100644 --- a/build/docker-compose.net5.0.yml +++ b/build/docker-compose.net5.0.yml @@ -1,4 +1,4 @@ -version: '3.7' +version: '3.7' services: tests: diff --git a/build/docker-compose.net6.0.yml b/build/docker-compose.net6.0.yml new file mode 100644 index 00000000000..c1a9529c513 --- /dev/null +++ b/build/docker-compose.net6.0.yml @@ -0,0 +1,8 @@ +version: '3.7' + +services: + tests: + build: + args: + PUBLISH_FRAMEWORK: net6.0 + SDK_VERSION: 6.0 diff --git a/build/docker-compose.netcoreapp3.1.yml b/build/docker-compose.netcoreapp3.1.yml index 03bac7cbe60..4d45fa1b3e3 100644 --- a/build/docker-compose.netcoreapp3.1.yml +++ b/build/docker-compose.netcoreapp3.1.yml @@ -1,4 +1,4 @@ -version: '3.7' +version: '3.7' services: tests: diff --git a/build/opentelemetry-icon-color.png b/build/opentelemetry-icon-color.png index c08946be7bc..93481dee87a 100644 Binary files a/build/opentelemetry-icon-color.png and b/build/opentelemetry-icon-color.png differ diff --git a/build/sanitycheck.py b/build/sanitycheck.py index aebb2df8c25..901d364f46e 100644 --- a/build/sanitycheck.py +++ b/build/sanitycheck.py @@ -8,7 +8,7 @@ CRLF = b'\r\n' LF = b'\n' -def sanitycheck(pattern, allow_utf8 = False, allow_eol = (CRLF, LF)): +def sanitycheck(pattern, allow_utf8 = False, allow_eol = (CRLF, LF), indent = 1): error_count = 0 for filename in glob.glob(pattern, recursive=True): @@ -26,6 +26,8 @@ def sanitycheck(pattern, allow_utf8 = False, allow_eol = (CRLF, LF)): for line in content.splitlines(True): if allow_utf8 and lineno == 1 and line.startswith(b'\xef\xbb\xbf'): line = line[3:] + if any(b == 7 for b in line): + error.append(' TAB found at Ln:{} {}'.format(lineno, line)) if any(b > 127 for b in line): error.append(' Non-ASCII character found at Ln:{} {}'.format(lineno, line)) if line[-2:] == CRLF: @@ -47,6 +49,14 @@ def sanitycheck(pattern, allow_utf8 = False, allow_eol = (CRLF, LF)): if eol not in allow_eol: error.append(' Line ending {} not allowed at Ln:{}'.format(eol, lineno)) break + if line.startswith(b' '): + spc_count = 0 + for c in line: + if c != 32: + break + spc_count += 1 + if not indent or spc_count % indent: + error.append(' {} SPC found at Ln:{} {}'.format(spc_count, lineno, line)) if line[-1:] == b' ' or line[-1:] == b'\t': error.append(' Trailing space found at Ln:{} {}'.format(lineno, line)) lineno += 1 @@ -62,19 +72,23 @@ def sanitycheck(pattern, allow_utf8 = False, allow_eol = (CRLF, LF)): return error_count retval = 0 -retval += sanitycheck('**/*.cmd', allow_eol = (CRLF,)) -retval += sanitycheck('**/*.config', allow_utf8 = True, allow_eol = (LF,)) +retval += sanitycheck('.editorconfig', allow_eol = (LF,), indent = 0) +retval += sanitycheck('**/Dockerfile', allow_eol = (LF,), indent = 2) +retval += sanitycheck('**/*.cmd', allow_eol = (CRLF,), indent = 2) +retval += sanitycheck('**/*.config', allow_utf8 = True, allow_eol = (LF,), indent = 2) retval += sanitycheck('**/*.cs', allow_utf8 = True, allow_eol = (LF,)) -retval += sanitycheck('**/*.cshtml', allow_utf8 = True, allow_eol = (LF,)) -retval += sanitycheck('**/*.csproj', allow_utf8 = True, allow_eol = (LF,)) -retval += sanitycheck('**/*.htm', allow_eol = (LF,)) -retval += sanitycheck('**/*.html', allow_eol = (LF,)) +retval += sanitycheck('**/*.cshtml', allow_utf8 = True, allow_eol = (LF,), indent = 4) +retval += sanitycheck('**/*.csproj', allow_utf8 = True, allow_eol = (LF,), indent = 2) +retval += sanitycheck('**/*.htm', allow_eol = (LF,), indent = 4) +retval += sanitycheck('**/*.html', allow_eol = (LF,), indent = 4) retval += sanitycheck('**/*.md', allow_eol = (LF,)) -retval += sanitycheck('**/*.proj', allow_eol = (LF,)) -retval += sanitycheck('**/*.props', allow_eol = (LF,)) -retval += sanitycheck('**/*.py', allow_eol = (LF,)) -retval += sanitycheck('**/*.ruleset', allow_utf8 = True, allow_eol = (LF,)) -retval += sanitycheck('**/*.sln', allow_utf8 = True, allow_eol = (LF,)) -retval += sanitycheck('**/*.xml', allow_eol = (LF,)) +retval += sanitycheck('**/*.proj', allow_eol = (LF,), indent = 2) +retval += sanitycheck('**/*.props', allow_eol = (LF,), indent = 2) +retval += sanitycheck('**/*.py', allow_eol = (LF,), indent = 4) +retval += sanitycheck('**/*.ruleset', allow_utf8 = True, allow_eol = (LF,), indent = 2) +retval += sanitycheck('**/*.sln', allow_utf8 = True, allow_eol = (LF,), indent = 4) +retval += sanitycheck('**/*.targets', allow_eol = (LF,), indent = 2) +retval += sanitycheck('**/*.xml', allow_eol = (LF,), indent = 4) +retval += sanitycheck('**/*.yml', allow_eol = (LF,), indent = 2) sys.exit(retval) diff --git a/docs/Directory.Build.props b/docs/Directory.Build.props index 95ae4329f8e..edcf6e0f7ed 100644 --- a/docs/Directory.Build.props +++ b/docs/Directory.Build.props @@ -4,7 +4,7 @@ Exe - netcoreapp3.1;net5.0 + netcoreapp3.1;net5.0;net6.0 $(TargetFrameworks);net461;net462;net47;net471;net472;net48 @@ -12,8 +12,8 @@ - [5.0.0,6.0) + [6.0.0,) diff --git a/docs/docfx.json b/docs/docfx.json index bcfb20dfd1c..5fc4dfb889f 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -29,9 +29,7 @@ { "files": [ ".editorconfig", - "docs/**.cs", - "examples/**.cs", - "src/**.cs" + "**.cs" ] } ], diff --git a/docs/logs/extending-the-sdk/README.md b/docs/logs/extending-the-sdk/README.md index 3628e77eb86..7368aea4f9e 100644 --- a/docs/logs/extending-the-sdk/README.md +++ b/docs/logs/extending-the-sdk/README.md @@ -18,11 +18,12 @@ not covered by the built-in exporters: * Exporters should derive from `OpenTelemetry.BaseExporter` (which belongs to the [OpenTelemetry](../../../src/OpenTelemetry/README.md) package) and implement the `Export` method. -* Exporters can optionally implement the `OnShutdown` method. +* Exporters can optionally implement the `OnForceFlush` and `OnShutdown` method. * Depending on user's choice and load on the application, `Export` may get called with one or more log records. * Exporters will only receive sampled-in log records. -* Exporters should not throw exceptions from `Export` and `OnShutdown`. +* Exporters should not throw exceptions from `Export`, `OnForceFlush` and + `OnShutdown`. * Exporters should not modify log records they receive (the same log records may be exported again by different exporter). * Exporters are responsible for any retry logic needed by the scenario. The SDK diff --git a/docs/logs/extending-the-sdk/extending-the-sdk.csproj b/docs/logs/extending-the-sdk/extending-the-sdk.csproj index a5ae2beeec6..b70cbddfe8d 100644 --- a/docs/logs/extending-the-sdk/extending-the-sdk.csproj +++ b/docs/logs/extending-the-sdk/extending-the-sdk.csproj @@ -1,9 +1,6 @@  - - - - + diff --git a/docs/logs/getting-started/getting-started.csproj b/docs/logs/getting-started/getting-started.csproj index 93374308e63..ce4c69320ee 100644 --- a/docs/logs/getting-started/getting-started.csproj +++ b/docs/logs/getting-started/getting-started.csproj @@ -1,9 +1,6 @@  - - - - + diff --git a/docs/logs/source-generation/FoodSupplyLogs.cs b/docs/logs/source-generation/FoodSupplyLogs.cs new file mode 100644 index 00000000000..62485fd00f7 --- /dev/null +++ b/docs/logs/source-generation/FoodSupplyLogs.cs @@ -0,0 +1,38 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using Microsoft.Extensions.Logging; + +public static partial class FoodSupplyLogs +{ + [LoggerMessage( + EventId = 1, + Level = LogLevel.Information, + Message = "Food `{name}` price changed to `{price}`.")] + public static partial void FoodPriceChanged(this ILogger logger, string name, double price); + + [LoggerMessage( + EventId = 2, + Message = "A `{productType}` recall notice was published for `{brandName} {productDescription}` produced by `{companyName}` ({recallReasonDescription}).")] + public static partial void FoodRecallNotice( + this ILogger logger, + LogLevel logLevel, + string brandName, + string productDescription, + string productType, + string recallReasonDescription, + string companyName); +} diff --git a/docs/logs/source-generation/Program.cs b/docs/logs/source-generation/Program.cs new file mode 100644 index 00000000000..de5ac143e12 --- /dev/null +++ b/docs/logs/source-generation/Program.cs @@ -0,0 +1,47 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using Microsoft.Extensions.Logging; +using OpenTelemetry.Logs; + +public class Program +{ + public static void Main() + { + using var loggerFactory = LoggerFactory.Create(builder => + { + builder.AddOpenTelemetry(options => + { + options.IncludeScopes = true; + options.ParseStateValues = true; + options.IncludeFormattedMessage = true; + options.AddConsoleExporter(); + }); + }); + + var logger = loggerFactory.CreateLogger(); + + logger.FoodPriceChanged("artichoke", 9.99); + + logger.FoodRecallNotice( + logLevel: LogLevel.Critical, + brandName: "Contoso", + productDescription: "Salads", + productType: "Food & Beverages", + recallReasonDescription: "due to a possible health risk from Listeria monocytogenes", + companyName: "Contoso Fresh Vegetables, Inc."); + } +} diff --git a/docs/logs/source-generation/README.md b/docs/logs/source-generation/README.md new file mode 100644 index 00000000000..c24f9d45e37 --- /dev/null +++ b/docs/logs/source-generation/README.md @@ -0,0 +1,8 @@ +# Compile-time logging source generation + +.NET 6 has introduced a more usable and performant logging solution with +[`LoggerMessageAttribute`](https://docs.microsoft.com/dotnet/api/microsoft.extensions.logging.loggermessageattribute). + +## References + +* [Compile-time logging source generation](https://docs.microsoft.com/dotnet/core/extensions/logger-message-generator) diff --git a/docs/trace/exception-reporting/exception-reporting.csproj b/docs/logs/source-generation/source-generation.csproj similarity index 59% rename from docs/trace/exception-reporting/exception-reporting.csproj rename to docs/logs/source-generation/source-generation.csproj index afb37b73c59..ce4c69320ee 100644 --- a/docs/trace/exception-reporting/exception-reporting.csproj +++ b/docs/logs/source-generation/source-generation.csproj @@ -1,8 +1,6 @@  - + diff --git a/docs/metrics/README.md b/docs/metrics/README.md deleted file mode 100644 index c1f16fa5116..00000000000 --- a/docs/metrics/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# Getting Started with OpenTelemetry .NET Metrics in 5 Minutes - -* [Getting started with Counter](.\getting-started\README.md) -* [Getting started with Histogram](.\getting-started-histogram\README.md) diff --git a/docs/metrics/customizing-the-sdk/Program.cs b/docs/metrics/customizing-the-sdk/Program.cs new file mode 100644 index 00000000000..b3b5c853934 --- /dev/null +++ b/docs/metrics/customizing-the-sdk/Program.cs @@ -0,0 +1,99 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Diagnostics.Metrics; +using OpenTelemetry; +using OpenTelemetry.Metrics; + +public class Program +{ + private static readonly Meter Meter1 = new Meter("CompanyA.ProductA.Library1", "1.0"); + private static readonly Meter Meter2 = new Meter("CompanyA.ProductB.Library2", "1.0"); + + public static void Main(string[] args) + { + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(Meter1.Name) + .AddMeter(Meter2.Name) + + // Rename an instrument to new name. + .AddView(instrumentName: "MyCounter", name: "MyCounterRenamed") + + // Change Histogram boundaries + .AddView(instrumentName: "MyHistogram", new ExplicitBucketHistogramConfiguration() { Boundaries = new double[] { 10, 20 } }) + + // For the instrument "MyCounterCustomTags", aggregate with only the keys "tag1", "tag2". + .AddView(instrumentName: "MyCounterCustomTags", new MetricStreamConfiguration() { TagKeys = new string[] { "tag1", "tag2" } }) + + // Drop the instrument "MyCounterDrop". + .AddView(instrumentName: "MyCounterDrop", MetricStreamConfiguration.Drop) + + // Advanced selection criteria and config via Func + .AddView((instrument) => + { + if (instrument.Meter.Name.Equals("CompanyA.ProductB.Library2") && + instrument.GetType().Name.Contains("Histogram")) + { + return new ExplicitBucketHistogramConfiguration() { Boundaries = new double[] { 10, 20 } }; + } + + return null; + }) + + // An instrument which does not match any views + // gets processed with default behavior. (SDK default) + // Uncommenting the following line will + // turn off the above default. i.e any + // instrument which does not match any views + // gets dropped. + // .AddView(instrumentName: "*", MetricStreamConfiguration.Drop) + .AddConsoleExporter() + .Build(); + + var random = new Random(); + + var counter = Meter1.CreateCounter("MyCounter"); + for (int i = 0; i < 20000; i++) + { + counter.Add(1, new("tag1", "value1"), new("tag2", "value2")); + } + + var histogram = Meter1.CreateHistogram("MyHistogram"); + for (int i = 0; i < 20000; i++) + { + histogram.Record(random.Next(1, 1000), new("tag1", "value1"), new("tag2", "value2")); + } + + var counterCustomTags = Meter1.CreateCounter("MyCounterCustomTags"); + for (int i = 0; i < 20000; i++) + { + counterCustomTags.Add(1, new("tag1", "value1"), new("tag2", "value2"), new("tag3", "value4")); + } + + var counterDrop = Meter1.CreateCounter("MyCounterDrop"); + for (int i = 0; i < 20000; i++) + { + counterDrop.Add(1, new("tag1", "value1"), new("tag2", "value2")); + } + + var histogram2 = Meter2.CreateHistogram("MyHistogram2"); + for (int i = 0; i < 20000; i++) + { + histogram2.Record(random.Next(1, 1000), new("tag1", "value1"), new("tag2", "value2")); + } + } +} diff --git a/docs/metrics/customizing-the-sdk/README.md b/docs/metrics/customizing-the-sdk/README.md new file mode 100644 index 00000000000..a235c53b918 --- /dev/null +++ b/docs/metrics/customizing-the-sdk/README.md @@ -0,0 +1,423 @@ +# Customizing OpenTelemetry .NET SDK for Metrics + +## MeterProvider + +As shown in the [getting-started](../getting-started/README.md) doc, a valid +[`MeterProvider`](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#meterprovider) +must be configured and built to collect metrics with OpenTelemetry .NET Sdk. +`MeterProvider` holds all the configuration for metrics like MetricReaders, +Views, etc. Naturally, almost all the customizations must be done on the +`MeterProvider`. + +## Building a MeterProvider + +Building a `MeterProvider` is done using `MeterProviderBuilder` which must be +obtained by calling `Sdk.CreateMeterProviderBuilder()`. `MeterProviderBuilder` +exposes various methods which configure the provider it is going to build. These +include methods like `AddMeter`, `AddView` etc, and are explained in subsequent +sections of this document. Once configuration is done, calling `Build()` on the +`MeterProviderBuilder` builds the `MeterProvider` instance. Once built, changes +to its configuration is not allowed. In most cases, a single `MeterProvider` is +created at the application startup, and is disposed when application shuts down. + +The snippet below shows how to build a basic `MeterProvider`. This will create a +provider with default configuration, and is not particularly useful. The +subsequent sections show how to build a more useful provider. + +```csharp +using OpenTelemetry; +using OpenTelemetry.Metrics; + +using var meterProvider = Sdk.CreateMeterProviderBuilder().Build(); +``` + +## MeterProvider configuration + +`MeterProvider` holds the metrics configuration, which includes the following: + +1. The list of `Meter`s from which instruments are created to report + measurements. +2. The list of instrumentations enabled via [Instrumentation + Library](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/glossary.md#instrumentation-library). +3. The list of + [MetricReaders](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#metricreader), + including exporting readers which exports metrics to + [Exporters](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#metricexporter) +4. The + [Resource](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/sdk.md) + associated with the metrics. +5. The list of + [Views](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#view) + to be used. + +### Meter + +[`Meter`](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#meter) +is used for creating +[`Instruments`](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#instrument), +which are then used to report +[Measurements](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#measurement). +The SDK follows an explicit opt-in model for listening to meters. i.e, by +default, it listens to no meters. Every meter which is used to create +instruments must be explicitly added to the meter provider. + +`AddMeter` method on `MeterProviderBuilder` can be used to add a `Meter` to the +provider. The name of the `Meter` (case-insensitive) must be provided as an +argument to this method. `AddMeter` can be called multiple times to add more +than one meters. It also supports wildcard subscription model. It is important +to note that *all* the instruments from the meter will be enabled, when a +`Meter` is added. To selectively drop some instruments from a `Meter`, use the +[View](#view) feature, as shown [here](#drop-an-instrument). + +It is **not** possible to add meters *once* the provider is built by the +`Build()` method on the `MeterProviderBuilder`. + +The snippet below shows how to add meters to the provider. + +```csharp +using OpenTelemetry; +using OpenTelemetry.Metrics; + +using var meterProvider = Sdk.CreateMeterProviderBuilder() + // The following enables instruments from Meter + // named "MyCompany.MyProduct.MyLibrary" only. + .AddMeter("MyCompany.MyProduct.MyLibrary") + // The following enables instruments from all Meters + // whose name starts with "AbcCompany.XyzProduct.". + .AddMeter("AbcCompany.XyzProduct.*") + .Build(); +``` + +See [Program.cs](./Program.cs) for complete example. + +**Note:** A common mistake while configuring `MeterProvider` is forgetting to +add the required `Meter`s to the provider. It is recommended to leverage the +wildcard subscription model where it makes sense. For example, if your +application is expecting to enable instruments from a number of libraries from a +company "Abc", the you can use `AddMeter("Abc.*")` to enable all meters whose +name starts with "Abc.". + +### View + +A +[View](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#view) +provides the ability to customize the metrics that are output by the SDK. +Following sections explains how to use this feature. Each section has two code +snippets. The first one uses an overload of `AddView` method that takes in the +name of the instrument as the first parameter. The `View` configuration is then +applied to the matching instrument name. The second code snippet shows how to +use an advanced selection criteria to achieve the same results. This requires +the user to provide a `Func` which offers +more flexibility in filtering the instruments to which the `View` should be +applied. + +#### Rename an instrument + +When SDK produces Metrics, the name of Metric is by default the name of the +instrument. View may be used to rename a metric to a different name. This is +particularly useful if there are conflicting instrument names, and you do not +own the instrument to create it with a different name. + +```csharp + // Rename an instrument to new name. + .AddView(instrumentName: "MyCounter", name: "MyCounterRenamed") +``` + +```csharp + // Advanced selection criteria and config via Func + .AddView((instrument) => + { + if (instrument.Meter.Name == "CompanyA.ProductB.LibraryC" && + instrument.Name == "MyCounter") + { + return new MetricStreamConfiguration() { Name = "MyCounterRenamed" }; + } + + return null; + }) +``` + +#### Drop an instrument + +When using `AddMeter` to add a Meter to the provider, all the instruments from +that `Meter` gets subscribed. Views can be used to selectively drop an +instrument from a Meter. If the goal is to drop every instrument from a `Meter`, +then it is recommended to simply not add that `Meter` using `AddMeter`. + +```csharp + // Drop the instrument "MyCounterDrop". + .AddView(instrumentName: "MyCounterDrop", MetricStreamConfiguration.Drop) +``` + +```csharp + // Advanced selection criteria and config via Func + .AddView((instrument) => + { + if (instrument.Meter.Name == "CompanyA.ProductB.LibraryC" && + instrument.Name == "MyCounterDrop") + { + return MetricStreamConfiguration.Drop; + } + + return null; + }) +``` + +#### Select specific tags + +When recording a measurement from an instrument, all the tags that were provided +are reported as dimensions for the given metric. Views can be used to +selectively choose a subset of dimensions to report for a given metric. This is +useful when you have a metric for which only a few of the dimensions associated +with the metric are of interest to you. + +```csharp + // Only choose "name" as the dimension for the metric "MyFruitCounter" + .AddView( + instrumentName: "MyFruitCounter", + metricStreamConfiguration: new MetricStreamConfiguration + { + TagKeys = new string[] { "name" }, + }) + + ... + // Only the dimension "name" is selected, "color" is dropped + MyFruitCounter.Add(1, new("name", "apple"), new("color", "red")); + MyFruitCounter.Add(2, new("name", "lemon"), new("color", "yellow")); + MyFruitCounter.Add(2, new("name", "apple"), new("color", "green")); + ... + + // If you provide an empty `string` array as `TagKeys` to the `MetricStreamConfiguration` + // the SDK will drop all the dimensions associated with the metric + .AddView( + instrumentName: "MyFruitCounter", + metricStreamConfiguration: new MetricStreamConfiguration + { + TagKeys = new string[] { }, + }) + + ... + // both "name" and "color" are dropped + MyFruitCounter.Add(1, new("name", "apple"), new("color", "red")); + MyFruitCounter.Add(2, new("name", "lemon"), new("color", "yellow")); + MyFruitCounter.Add(2, new("name", "apple"), new("color", "green")); + ... +``` + +```csharp + // Advanced selection criteria and config via Func + .AddView((instrument) => + { + if (instrument.Meter.Name == "CompanyA.ProductB.LibraryC" && + instrument.Name == "MyFruitCounter") + { + return new MetricStreamConfiguration + { + TagKeys = new string[] { "name" }, + }; + } + + return null; + }) +``` + +#### Specify custom boundaries for Histogram + +By default, the boundaries used for a Histogram are [`{ 0, 5, 10, 25, 50, 75, +100, 250, 500, +1000}`](https://github.com/open-telemetry/opentelemetry-specification/blob/main/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`. + + +```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 and count for the measurements. + // There are no buckets exported in this case. + .AddView( + instrumentName: "MyHistogram", + new ExplicitBucketHistogramConfiguration { Boundaries = new double[] { } }) +``` + + +```csharp + // Advanced selection criteria and config via Func + .AddView((instrument) => + { + if (instrument.Meter.Name == "CompanyA.ProductB.LibraryC" && + instrument.Name == "MyHistogram") + { + // `ExplicitBucketHistogramConfiguration` is a child class of `MetricStreamConfiguration` + return new ExplicitBucketHistogramConfiguration + { + Boundaries = new double[] { 10, 20 }, + }; + } + + return null; + }) +``` + +**NOTE:** The SDK currently does not support any changes to `Aggregation` type +by using Views. + +See [Program.cs](./Program.cs) for a complete example. + +### Changing maximum Metric Streams + +Every instrument results in the creation of a single Metric stream. With Views, +it is possible to produce more than one Metric stream from a single instrument. +To protect the SDK from unbounded memory usage, SDK limits the maximum number of +metric streams. All the measurements from the instruments created after reaching +this limit will be dropped. The default is 1000, and `SetMaxMetricStreams` can +be used to override the default. + +Consider the below example. Here we set the maximum number of `MetricStream`s +allowed to be `1`. This means that the SDK would export measurements from only +one `MetricStream`. The very first instrument that is published +(`MyFruitCounter` in this case) will create a `MetricStream` and the SDK will +thereby reach the maximum `MetricStream` limit of `1`. The measurements from any +susequent instruments added will be dropped. + +```csharp +using System.Diagnostics.Metrics; +using OpenTelemetry; +using OpenTelemetry.Metrics; + +Counter MyFruitCounter = MyMeter.CreateCounter("MyFruitCounter"); +Counter AnotherFruitCounter = MyMeter.CreateCounter("AnotherFruitCounter"); + +using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter("*") + .AddConsoleExporter() + .SetMaxMetricStreams(1) // The default value is 1000 + .Build(); + +// SDK only exports measurements from `MyFruitCounter`. +MyFruitCounter.Add(1, new("name", "apple"), new("color", "red")); + +// The measurements from `AnotherFruitCounter` are dropped as the maximum +// `MetricStream`s allowed is `1`. +AnotherFruitCounter.Add(1, new("name", "apple"), new("color", "red")); +``` + +### Changing maximum MetricPoints per MetricStream + +A Metric stream can contain as many Metric points as the number of unique +combination of keys and values. To protect the SDK from unbounded memory usage, +SDK limits the maximum number of metric points per metric stream, to a default +of 2000. Once the limit is hit, any new key/value combination for that metric is +ignored. The SDK chooses the key/value combinations in the order in which they +are emitted. `SetMaxMetricPointsPerMetricStream` can be used to override the +default. + +**NOTE**: One `MetricPoint` is reserved for every `MetricStream` for the special +case where there is no key/value pair associated with the metric. The maximum +number of `MetricPoint`s has to accommodate for this special case. + +Consider the below example. Here we set the maximum number of `MetricPoint`s +allowed to be `3`. This means that for every `MetricStream`, the SDK will export +measurements for up to `3` distinct key/value combinations of the metric. There +are two instruments published here: `MyFruitCounter` and `AnotherFruitCounter`. +There are two total `MetricStream`s created one for each of these instruments. +SDK will limit the maximum number of distinct key/value combinations for each of +these `MetricStream`s to `3`. + + +```csharp +using System.Collections.Generic; +using System.Diagnostics.Metrics; +using OpenTelemetry; +using OpenTelemetry.Metrics; + +Counter MyFruitCounter = MyMeter.CreateCounter("MyFruitCounter"); +Counter AnotherFruitCounter = MyMeter.CreateCounter("AnotherFruitCounter"); + +using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter("*") + .AddConsoleExporter() + .SetMaxMetricPointsPerMetricStream(3) // The default value is 2000 + .Build(); + +// There are four distinct key/value combinations emitted for `MyFruitCounter`: +// 1. No key/value pair +// 2. (name:apple, color:red) +// 3. (name:lemon, color:yellow) +// 4. (name:apple, color:green) + +// Since the maximum number of `MetricPoint`s allowed is `3`, the SDK will only export measurements for the following three combinations: +// 1. No key/value pair +// 2. (name:apple, color:red) +// 3. (name:lemon, color:yellow) + +MyFruitCounter.Add(1); // Exported (No key/value pair) +MyFruitCounter.Add(1, new("name", "apple"), new("color", "red")); // Exported +MyFruitCounter.Add(2, new("name", "lemon"), new("color", "yellow")); // Exported +MyFruitCounter.Add(1, new("name", "lemon"), new("color", "yellow")); // Exported +MyFruitCounter.Add(2, new("name", "apple"), new("color", "green")); // Not exported +MyFruitCounter.Add(5, new("name", "apple"), new("color", "red")); // Exported +MyFruitCounter.Add(4, new("name", "lemon"), new("color", "yellow")); // Exported + +// There are four distinct key/value combinations emitted for `AnotherFruitCounter`: +// 1. (name:kiwi) +// 2. (name:banana, color:yellow) +// 3. (name:mango, color:yellow) +// 4. (name:banana, color:green) + +// Since the maximum number of `MetricPoint`s allowed is `3`, the SDK will only export measurements for the following three combinations: +// 1. No key/value pair (This is a special case. The SDK reserves a `MetricPoint` for it even if it's not explicitly emitted.) +// 2. (name:kiwi) +// 3. (name:banana, color:yellow) + +AnotherFruitCounter.Add(4, new KeyValuePair("name", "kiwi")); // Exported +AnotherFruitCounter.Add(1, new("name", "banana"), new("color", "yellow")); // Exported +AnotherFruitCounter.Add(2, new("name", "mango"), new("color", "yellow")); // Not exported +AnotherFruitCounter.Add(1, new("name", "mango"), new("color", "yellow")); // Not exported +AnotherFruitCounter.Add(2, new("name", "banana"), new("color", "green")); // Not exported +AnotherFruitCounter.Add(5, new("name", "banana"), new("color", "yellow")); // Exported +AnotherFruitCounter.Add(4, new("name", "mango"), new("color", "yellow")); // Not exported +``` + + +**NOTE:** The above limit is *per* metric stream, and applies to all the metric +streams. There is no ability to apply different limits for each instrument at +this moment. + +### Instrumentation + +// TODO + +### MetricReader + +[MetricReader](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#metricreader) +allows collecting the pre-aggregated metrics from the SDK. They are typically +paired with a +[MetricExporter](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#metricexporter) +which does the actual export of metrics. + +Though `MetricReader` can be added by using the `AddReader` method on +`MeterProviderBuilder`, most users use the extension methods on +`MeterProviderBuilder` offered by exporter libraries, which adds the correct +`MetricReader`, that is configured to export metrics to the exporter. + +Refer to the individual exporter docs to learn how to use them: + +* [Console](../../../src/OpenTelemetry.Exporter.Console/README.md) +* [In-memory](../../../src/OpenTelemetry.Exporter.InMemory/README.md) +* [OTLP](../../../src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md) + (OpenTelemetry Protocol) +* [Prometheus](../../../src/OpenTelemetry.Exporter.Prometheus/README.md) + +### Resource + +// TODO diff --git a/docs/metrics/customizing-the-sdk/customizing-the-sdk.csproj b/docs/metrics/customizing-the-sdk/customizing-the-sdk.csproj new file mode 100644 index 00000000000..19aa9791432 --- /dev/null +++ b/docs/metrics/customizing-the-sdk/customizing-the-sdk.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/docs/metrics/extending-the-sdk/MyExporter.cs b/docs/metrics/extending-the-sdk/MyExporter.cs new file mode 100644 index 00000000000..997e73d5cfc --- /dev/null +++ b/docs/metrics/extending-the-sdk/MyExporter.cs @@ -0,0 +1,62 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Text; +using OpenTelemetry; +using OpenTelemetry.Metrics; + +internal class MyExporter : BaseExporter +{ + private readonly string name; + + public MyExporter(string name = "MyExporter") + { + this.name = name; + } + + public override ExportResult Export(in Batch batch) + { + // SuppressInstrumentationScope should be used to prevent exporter + // code from generating telemetry and causing live-loop. + using var scope = SuppressInstrumentationScope.Begin(); + + var sb = new StringBuilder(); + foreach (var record in batch) + { + if (sb.Length > 0) + { + sb.Append(", "); + } + + sb.Append($"{record}"); + } + + Console.WriteLine($"{this.name}.Export([{sb}])"); + return ExportResult.Success; + } + + protected override bool OnShutdown(int timeoutMilliseconds) + { + Console.WriteLine($"{this.name}.OnShutdown(timeoutMilliseconds={timeoutMilliseconds})"); + return true; + } + + protected override void Dispose(bool disposing) + { + Console.WriteLine($"{this.name}.Dispose({disposing})"); + } +} diff --git a/test/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.Tests/PropertyExtensions.cs b/docs/metrics/extending-the-sdk/MyExporterExtensions.cs similarity index 60% rename from test/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.Tests/PropertyExtensions.cs rename to docs/metrics/extending-the-sdk/MyExporterExtensions.cs index 135a4a97236..23e174e3eb9 100644 --- a/test/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.Tests/PropertyExtensions.cs +++ b/docs/metrics/extending-the-sdk/MyExporterExtensions.cs @@ -1,4 +1,4 @@ -// +// // Copyright The OpenTelemetry Authors // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,15 +14,18 @@ // limitations under the License. // -namespace OpenTelemetry.Instrumentation.AspNet.Tests -{ - using System.Reflection; +using System; +using OpenTelemetry.Metrics; - internal static class PropertyExtensions +internal static class MyExporterExtensions +{ + public static MeterProviderBuilder AddMyExporter(this MeterProviderBuilder builder) { - public static object GetProperty(this object obj, string propertyName) + if (builder == null) { - return obj.GetType().GetTypeInfo().GetDeclaredProperty(propertyName)?.GetValue(obj); + throw new ArgumentNullException(nameof(builder)); } + + return builder.AddReader(new BaseExportingMetricReader(new MyExporter())); } } diff --git a/docs/metrics/extending-the-sdk/Program.cs b/docs/metrics/extending-the-sdk/Program.cs new file mode 100644 index 00000000000..0da91568601 --- /dev/null +++ b/docs/metrics/extending-the-sdk/Program.cs @@ -0,0 +1,56 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.Metrics; +using OpenTelemetry; +using OpenTelemetry.Metrics; + +public class Program +{ + private static readonly Meter MyMeter = new Meter("MyCompany.MyProduct.MyLibrary", "1.0"); + private static readonly Counter MyFruitCounter = MyMeter.CreateCounter("MyFruitCounter"); + + static Program() + { + var process = Process.GetCurrentProcess(); + + MyMeter.CreateObservableGauge( + "MyProcessWorkingSetGauge", + () => new List>() + { + new(process.WorkingSet64, new("process.id", process.Id), new("process.bitness", IntPtr.Size << 3)), + }); + } + + public static void Main(string[] args) + { + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter("MyCompany.MyProduct.MyLibrary") + .AddReader(new BaseExportingMetricReader(new MyExporter("ExporterX"))) + .AddMyExporter() + .Build(); + + MyFruitCounter.Add(1, new("name", "apple"), new("color", "red")); + MyFruitCounter.Add(2, new("name", "lemon"), new("color", "yellow")); + MyFruitCounter.Add(1, new("name", "lemon"), new("color", "yellow")); + MyFruitCounter.Add(2, new("name", "apple"), new("color", "green")); + MyFruitCounter.Add(5, new("name", "apple"), new("color", "red")); + MyFruitCounter.Add(4, new("name", "lemon"), new("color", "yellow")); + } +} diff --git a/docs/metrics/extending-the-sdk/README.md b/docs/metrics/extending-the-sdk/README.md new file mode 100644 index 00000000000..58a126b2766 --- /dev/null +++ b/docs/metrics/extending-the-sdk/README.md @@ -0,0 +1,20 @@ +# Extending the OpenTelemetry .NET SDK + +* [Building your own exporter](#exporter) +* [Building your own reader](#reader) +* [Building your own exemplar](#exemplar) +* [References](#references) + +## Exporter + +TBD + +## Reader + +TBD + +## Exemplar + +TBD + +## References diff --git a/docs/metrics/extending-the-sdk/extending-the-sdk.csproj b/docs/metrics/extending-the-sdk/extending-the-sdk.csproj new file mode 100644 index 00000000000..4d96c349671 --- /dev/null +++ b/docs/metrics/extending-the-sdk/extending-the-sdk.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/docs/metrics/getting-started-histogram/Program.cs b/docs/metrics/getting-started-histogram/Program.cs deleted file mode 100644 index 9b74b5e5052..00000000000 --- a/docs/metrics/getting-started-histogram/Program.cs +++ /dev/null @@ -1,55 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -using System; -using System.Collections.Generic; -using System.Diagnostics.Metrics; -using System.Threading; -using System.Threading.Tasks; -using OpenTelemetry; -using OpenTelemetry.Metrics; - -public class Program -{ - private static readonly Meter MyMeter = new Meter("TestMeter", "0.0.1"); - private static readonly Histogram MyHistogram = MyMeter.CreateHistogram("histogram"); - private static readonly Random RandomGenerator = new Random(); - - public static async Task Main(string[] args) - { - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddSource("TestMeter") - .AddConsoleExporter() - .Build(); - - using var token = new CancellationTokenSource(); - Task writeMetricTask = new Task(() => - { - while (!token.IsCancellationRequested) - { - MyHistogram.Record( - RandomGenerator.Next(1, 1000), - new KeyValuePair("tag1", "value1"), - new KeyValuePair("tag2", "value2")); - Task.Delay(10).Wait(); - } - }); - writeMetricTask.Start(); - - token.CancelAfter(10000); - await writeMetricTask; - } -} diff --git a/docs/metrics/getting-started-histogram/README.md b/docs/metrics/getting-started-histogram/README.md deleted file mode 100644 index 0b8e9fabc9f..00000000000 --- a/docs/metrics/getting-started-histogram/README.md +++ /dev/null @@ -1,67 +0,0 @@ -# Getting Started with OpenTelemetry .NET in 5 Minutes - -First, download and install the [.NET Core -SDK](https://dotnet.microsoft.com/download) on your computer. - -Create a new console application and run it: - -```sh -dotnet new console --output getting-started-histogram -cd getting-started -dotnet run -``` - -You should see the following output: - -```text -Hello World! -``` - -Install the -[OpenTelemetry.Exporter.Console](../../../src/OpenTelemetry.Exporter.Console/README.md) -package: - -```sh -dotnet add package OpenTelemetry.Exporter.Console -``` - -Update the `Program.cs` file with the code from [Program.cs](./Program.cs): - -Run the application again (using `dotnet run`) and you should see the metric -output from the console, similar to shown below: - - -```text -Export 14:30:58.201 14:30:59.177 histogram [tag1=value1;tag2=value2] Histogram, Meter: TestMeter/0.0.1 -Value: Sum: 33862 Count: 62 -(-? - 0) : 0 -(0 - 5) : 0 -(5 - 10) : 0 -(10 - 25) : 2 -(25 - 50) : 0 -(50 - 75) : 1 -(75 - 100) : 1 -(100 - 250) : 6 -(250 - 500) : 18 -(500 - 1000) : 34 -(1000 - ?) : 0 -``` - - -Congratulations! You are now collecting histogram metrics using OpenTelemetry. - -What does the above program do? - -The program creates a -[Meter](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#meter) -instance named "TestMeter" and then creates a -[Histogram](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#histogram) -instrument from it. This histogram is used to repeatedly report random metric -measurements until exited after 10 seconds. - -An OpenTelemetry -[MeterProvider](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#meterprovider) -is configured to subscribe to instruments from the Meter `TestMeter`, and -aggregate the measurements in-memory. The pre-aggregated metrics are exported -every 1 second to a `ConsoleExporter`. `ConsoleExporter` simply displays it on -the console. diff --git a/docs/metrics/getting-started-prometheus-grafana/Program.cs b/docs/metrics/getting-started-prometheus-grafana/Program.cs new file mode 100644 index 00000000000..911adbc0004 --- /dev/null +++ b/docs/metrics/getting-started-prometheus-grafana/Program.cs @@ -0,0 +1,51 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Diagnostics.Metrics; +using System.Threading; +using OpenTelemetry; +using OpenTelemetry.Metrics; + +public class Program +{ + private static readonly Meter MyMeter = new Meter("MyCompany.MyProduct.MyLibrary", "1.0"); + private static readonly Counter MyFruitCounter = MyMeter.CreateCounter("MyFruitCounter"); + + public static void Main(string[] args) + { + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter("MyCompany.MyProduct.MyLibrary") + .AddPrometheusExporter(opt => + { + opt.StartHttpListener = true; + opt.HttpListenerPrefixes = new string[] { $"http://localhost:9184/" }; + }) + .Build(); + + Console.WriteLine("Press any key to exit"); + while (!Console.KeyAvailable) + { + Thread.Sleep(1000); + MyFruitCounter.Add(1, new("name", "apple"), new("color", "red")); + MyFruitCounter.Add(2, new("name", "lemon"), new("color", "yellow")); + MyFruitCounter.Add(1, new("name", "lemon"), new("color", "yellow")); + MyFruitCounter.Add(2, new("name", "apple"), new("color", "green")); + MyFruitCounter.Add(5, new("name", "apple"), new("color", "red")); + MyFruitCounter.Add(4, new("name", "lemon"), new("color", "yellow")); + } + } +} diff --git a/docs/metrics/getting-started-prometheus-grafana/README.md b/docs/metrics/getting-started-prometheus-grafana/README.md new file mode 100644 index 00000000000..8daca3f2bdb --- /dev/null +++ b/docs/metrics/getting-started-prometheus-grafana/README.md @@ -0,0 +1,197 @@ +# Quick start on exporting metrics to Prometheus/Grafana + +- [Quick start on exporting metrics to Prometheus/Grafana](#quick-start-on-exporting-metrics-to-prometheusgrafana) + - [Prerequisite](#prerequisite) + - [Introduction](#introduction) + - [Configure OpenTelemetry to Expose metrics via Prometheus Endpoint](#configure-opentelemetry-to-expose-metrics-via-prometheus-endpoint) + - [Check Results in the browser](#check-results-in-the-browser) + - [Download Prometheus](#download-prometheus) + - [Prometheus and Grafana](#prometheus-and-grafana) + - [Configuration](#configuration) + - [Start Prometheus](#start-prometheus) + - [View Results in Prometheus](#view-results-in-prometheus) + - [View/Query Results with Grafana](#viewquery-results-with-grafana) + +## Prerequisite + +It is highly recommended to go over the [getting-started](../getting-started/README.md) +doc before following along this document. + +## Introduction + +- [What is Prometheus?](https://prometheus.io/docs/introduction/overview/) + +- [Grafana support for + Prometheus](https://prometheus.io/docs/visualization/grafana/#creating-a-prometheus-graph) + +### Configure OpenTelemetry to Expose metrics via Prometheus Endpoint + +Create a new console application and run it: + +```sh +dotnet new console --output prometheus-http-server +cd prometheus-http-server +dotnet run +``` + +Add a reference to [prometheus +exporter](https://www.nuget.org/packages/opentelemetry.exporter.prometheus) to +this application. + +```shell +dotnet add package OpenTelemetry.Exporter.Prometheus --version 1.2.0-rc1 +``` + +Now, we are going to make some small tweaks to the example in the +getting-started metrics `Program.cs` to make the metrics available via +OpenTelemetry Prometheus Exporter. + +First, copy and paste everything from getting-started +metrics [example](../getting-started/Program.cs) to the Program.cs file of the +new console application (prometheus-http-server) we've created. + +And replace the below line: + +```csharp +.AddConsoleExporter() +``` + +with + +```csharp +.AddPrometheusExporter(opt => +{ + opt.StartHttpListener = true; + opt.HttpListenerPrefixes = new string[] { $"http://localhost:9184/" }; +}) +``` + +With `.AddPrometheusExporter()` function, OpenTelemetry `PrometheusExporter` will +export data via the endpoint defined by `HttpListenerPrefixes`. + +Also, for our learning purpose, use a while-loop to keep increasing the counter +value until any key is pressed. + +```csharp +Console.WriteLine("Press any key to exit"); +while (!Console.KeyAvailable) +{ + Thread.Sleep(1000); + MyFruitCounter.Add(1, new("name", "apple"), new("color", "red")); + MyFruitCounter.Add(2, new("name", "lemon"), new("color", "yellow")); + MyFruitCounter.Add(1, new("name", "lemon"), new("color", "yellow")); + ... + ... + ... +} +``` + +After the above modifications, now our `Program.cs` should look like [this](./Program.cs). + +### Check Results in the browser + +Start the application and leave the process running. Now we should be able to +see the metrics at the endpoint we've defined in `Program.cs`; in this case, the +endpoint is: "http://localhost:9184/". + +Check the output metrics with your favorite browser: + +![MyFruitCounter output:](https://user-images.githubusercontent.com/16979322/150242010-8bde0002-44a5-4c84-94e6-3e0ee8a6ea4f.PNG) + +Now, we understand how we can configure Opentelemetry `PrometheusExporter` to +export metrics the endpoint we specified. Next, we are going to learn about how +to use Prometheus and Grafana to view/query the metrics +visualization. + +## Download Prometheus + +Follow the [first steps]((https://prometheus.io/docs/introduction/first_steps/)) +to download the [latest release](https://prometheus.io/download/) of Prometheus. + +## Prometheus and Grafana + +### Configuration + +After finished downloading, extract it to a local location that's easy to +access. We will find the default Prometheus configuration YAML file in the +folder, named `prometheus.yml`. + +Let's create a new file in the same location as where `prometheus.yml` locates, +and named the new file as `otel.yml` for this exercise. Then, copy and paste the +entire content below into the otel.yml file we have created just now. + +```yaml +global: + scrape_interval: 10s + scrape_timeout: 10s + evaluation_interval: 10s +scrape_configs: +- job_name: MyOpenTelemetryDemo + honor_timestamps: true + scrape_interval: 1s + scrape_timeout: 1s + metrics_path: /metrics + scheme: http + follow_redirects: true + static_configs: + - targets: + # set the target to the location where metrics will be exposed by + # the OpenTelemetry Prometheus Exporter + - localhost:9184 +``` + +### Start Prometheus + +Follow the instructions from +[starting-prometheus](https://prometheus.io/docs/introduction/first_steps/#starting-prometheus) +to start the Prometheus server and verify it has been started successfully. + +Please note that we will need pass in otel.yml file as the argument: + +```console +./prometheus --config.file=otel.yml +``` + +### View Results in Prometheus + +To use the graphical interface for viewing our metrics with Prometheus, navigate +to "http://localhost:9090/graph", and type `MyFruitCounter` in the expression +bar of the UI; finally, click the execute button. + +We should be able to see the following chart from the browser: + +![Prometheus Graph:](https://user-images.githubusercontent.com/16979322/150242083-65b84f25-c95f-4e9b-a64f-699ad8816602.PNG) + +From the legend, we can see that the `instance` name and the `job` name are the +values we have set in `otel.yml`. + +Congratulations! + +Now we know how to configure Prometheus server and deploy OpenTelemetry +`PrometheusExporter` to export our metrics. Next, we are going to explore a tool +called Grafana, which has powerful visualizations for the metrics. + +### View/Query Results with Grafana + +Please [Install Grafana](https://grafana.com/docs/grafana/latest/installation/). + +For windows users, after finishing installation, start the standalone Grafana +server, grafana-server.exe located in the bin folder. Then, use the browser to +navigate to the default port of Grafana `3000`. We can confirm the port number +with the logs from the command line after starting the Grafana server as well. + +Follow the instructions in the Grafana getting started +[doc](https://grafana.com/docs/grafana/latest/getting-started/getting-started/#step-2-log-in) +to log in. + +After successfully logging in, click on the explore option on the left panel of +the website - we should be able to write some queries to explore our metrics +now! + +Feel free to find some handy PromQL +[here](https://promlabs.com/promql-cheat-sheet/). + +In the below example, the query targets to find out what is the per-second rate +of increace of myFruitCounter over the last 30 minutes: + +![Grafana dashboard with myFruitCounter metrics rate:](https://user-images.githubusercontent.com/16979322/150242148-f35165a3-ab34-4e8c-88a1-4995ceeb08e2.PNG) diff --git a/docs/metrics/getting-started-prometheus-grafana/getting-started-prometheus-grafana.csproj b/docs/metrics/getting-started-prometheus-grafana/getting-started-prometheus-grafana.csproj new file mode 100644 index 00000000000..4913a024a94 --- /dev/null +++ b/docs/metrics/getting-started-prometheus-grafana/getting-started-prometheus-grafana.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/docs/metrics/getting-started/Program.cs b/docs/metrics/getting-started/Program.cs index dec6c5a3889..6d3f3f9da3e 100644 --- a/docs/metrics/getting-started/Program.cs +++ b/docs/metrics/getting-started/Program.cs @@ -14,40 +14,27 @@ // limitations under the License. // -using System.Collections.Generic; using System.Diagnostics.Metrics; -using System.Threading; -using System.Threading.Tasks; using OpenTelemetry; using OpenTelemetry.Metrics; public class Program { - private static readonly Meter MyMeter = new Meter("TestMeter", "0.0.1"); - private static readonly Counter Counter = MyMeter.CreateCounter("counter"); + private static readonly Meter MyMeter = new Meter("MyCompany.MyProduct.MyLibrary", "1.0"); + private static readonly Counter MyFruitCounter = MyMeter.CreateCounter("MyFruitCounter"); - public static async Task Main(string[] args) + public static void Main(string[] args) { using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddSource("TestMeter") - .AddConsoleExporter() - .Build(); + .AddMeter("MyCompany.MyProduct.MyLibrary") + .AddConsoleExporter() + .Build(); - using var token = new CancellationTokenSource(); - Task writeMetricTask = new Task(() => - { - while (!token.IsCancellationRequested) - { - Counter.Add( - 10, - new KeyValuePair("tag1", "value1"), - new KeyValuePair("tag2", "value2")); - Task.Delay(10).Wait(); - } - }); - writeMetricTask.Start(); - - token.CancelAfter(10000); - await writeMetricTask; + MyFruitCounter.Add(1, new("name", "apple"), new("color", "red")); + MyFruitCounter.Add(2, new("name", "lemon"), new("color", "yellow")); + MyFruitCounter.Add(1, new("name", "lemon"), new("color", "yellow")); + MyFruitCounter.Add(2, new("name", "apple"), new("color", "green")); + MyFruitCounter.Add(5, new("name", "apple"), new("color", "red")); + MyFruitCounter.Add(4, new("name", "lemon"), new("color", "yellow")); } } diff --git a/docs/metrics/getting-started/README.md b/docs/metrics/getting-started/README.md index 07fe473fd69..bfdd6306d2d 100644 --- a/docs/metrics/getting-started/README.md +++ b/docs/metrics/getting-started/README.md @@ -22,7 +22,7 @@ Install the package: ```sh -dotnet add package OpenTelemetry.Exporter.Console +dotnet add package --prerelease OpenTelemetry.Exporter.Console ``` Update the `Program.cs` file with the code from [Program.cs](./Program.cs): @@ -32,11 +32,13 @@ output from the console, similar to shown below: ```text -Export[] 16:38:36.241 16:38:37.233 TestMeter:counter [tag1=value1;tag2=value2] SumMetricAggregator Value: 590, Details: Delta=True,Mon=True,Count=59,Sum=590 -Export[] 16:38:37.233 16:38:38.258 TestMeter:counter [tag1=value1;tag2=value2] SumMetricAggregator Value: 640, Details: Delta=True,Mon=True,Count=64,Sum=640 -Export[] 16:38:38.258 16:38:39.261 TestMeter:counter [tag1=value1;tag2=value2] SumMetricAggregator Value: 640, Details: Delta=True,Mon=True,Count=64,Sum=640 -Export[] 16:38:39.261 16:38:40.266 TestMeter:counter [tag1=value1;tag2=value2] SumMetricAggregator Value: 630, Details: Delta=True,Mon=True,Count=63,Sum=630 -Export[] 16:38:40.266 16:38:41.271 TestMeter:counter [tag1=value1;tag2=value2] SumMetricAggregator Value: 640, Details: Delta=True,Mon=True,Count=64,Sum=640 +Export MyFruitCounter, Meter: MyCompany.MyProduct.MyLibrary/1.0 +(2021-09-23T22:00:08.4399776Z, 2021-09-23T22:00:08.4510115Z] color:red name:apple LongSum +Value: 6 +(2021-09-23T22:00:08.4399776Z, 2021-09-23T22:00:08.4510115Z] color:yellow name:lemon LongSum +Value: 7 +(2021-09-23T22:00:08.4399776Z, 2021-09-23T22:00:08.4510115Z] color:green name:apple LongSum +Value: 2 ``` @@ -46,14 +48,28 @@ What does the above program do? The program creates a [Meter](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#meter) -instance named "TestMeter" and then creates a +instance named "MyCompany.MyProduct.MyLibrary" and then creates a [Counter](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#counter) -instrument from it. This counter is used to repeatedly report metric -measurements until exited after 10 seconds. +instrument from it. This counter is used to report several metric measurements. An OpenTelemetry [MeterProvider](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#meterprovider) -is configured to subscribe to instruments from the Meter `TestMeter`, and -aggregate the measurements in-memory. The pre-aggregated metrics are exported -every 1 second to a `ConsoleExporter`. `ConsoleExporter` simply displays it on -the console. +is configured to subscribe to instruments from the Meter +`MyCompany.MyProduct.MyLibrary`, and aggregate the measurements in-memory. The +pre-aggregated metrics are exported to a `ConsoleExporter`. + +## OpenTelemetry .NET special note + +Metrics in OpenTelemetry .NET is a somewhat unique implementation of the +OpenTelemetry project, as most of the Metrics API are incorporated directly +into the .NET runtime itself. From a high level, what this means is that you +can instrument your application by simply depending on +`System.Diagnostics.DiagnosticSource` package. + +## Learn more + +* If you want to learn about more instruments, refer to [learning + more about instruments](../learning-more-instruments/README.md). + +* If you want to customize the Sdk, refer to [customizing + the SDK](../customizing-the-sdk/README.md). diff --git a/docs/metrics/getting-started/getting-started.csproj b/docs/metrics/getting-started/getting-started.csproj index 9f5b6b79bc3..19aa9791432 100644 --- a/docs/metrics/getting-started/getting-started.csproj +++ b/docs/metrics/getting-started/getting-started.csproj @@ -1,6 +1,5 @@ - diff --git a/docs/metrics/learning-more-instruments/Program.cs b/docs/metrics/learning-more-instruments/Program.cs new file mode 100644 index 00000000000..1c4a91fef49 --- /dev/null +++ b/docs/metrics/learning-more-instruments/Program.cs @@ -0,0 +1,67 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.Metrics; +using OpenTelemetry; +using OpenTelemetry.Metrics; + +public class Program +{ + private static readonly Meter MyMeter = new Meter("MyCompany.MyProduct.MyLibrary", "1.0"); + private static readonly Histogram MyHistogram = MyMeter.CreateHistogram("MyHistogram"); + + static Program() + { + var process = Process.GetCurrentProcess(); + + MyMeter.CreateObservableCounter("Thread.CpuTime", () => GetThreadCpuTime(process), "ms"); + + MyMeter.CreateObservableGauge("Thread.State", () => GetThreadState(process)); + } + + public static void Main(string[] args) + { + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter("MyCompany.MyProduct.MyLibrary") + .AddConsoleExporter() + .Build(); + + var random = new Random(); + for (int i = 0; i < 1000; i++) + { + MyHistogram.Record(random.Next(1, 1000)); + } + } + + private static IEnumerable> GetThreadCpuTime(Process process) + { + foreach (ProcessThread thread in process.Threads) + { + yield return new(thread.TotalProcessorTime.TotalMilliseconds, new("ProcessId", process.Id), new("ThreadId", thread.Id)); + } + } + + private static IEnumerable> GetThreadState(Process process) + { + foreach (ProcessThread thread in process.Threads) + { + yield return new((int)thread.ThreadState, new("ProcessId", process.Id), new("ThreadId", thread.Id)); + } + } +} diff --git a/docs/metrics/learning-more-instruments/README.md b/docs/metrics/learning-more-instruments/README.md new file mode 100644 index 00000000000..768f0c52382 --- /dev/null +++ b/docs/metrics/learning-more-instruments/README.md @@ -0,0 +1,3 @@ +# Learning more about Instruments + +TBD diff --git a/docs/metrics/getting-started-histogram/getting-started-histogram.csproj b/docs/metrics/learning-more-instruments/learning-more-instruments.csproj similarity index 100% rename from docs/metrics/getting-started-histogram/getting-started-histogram.csproj rename to docs/metrics/learning-more-instruments/learning-more-instruments.csproj diff --git a/docs/trace/customizing-the-sdk/customizing-the-sdk.csproj b/docs/trace/customizing-the-sdk/customizing-the-sdk.csproj index 2e58890af03..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,5 @@ - diff --git a/docs/trace/extending-the-sdk/MyExporterHelperExtensions.cs b/docs/trace/extending-the-sdk/MyExporterExtensions.cs similarity index 88% rename from docs/trace/extending-the-sdk/MyExporterHelperExtensions.cs rename to docs/trace/extending-the-sdk/MyExporterExtensions.cs index a914718004c..db3ee915842 100644 --- a/docs/trace/extending-the-sdk/MyExporterHelperExtensions.cs +++ b/docs/trace/extending-the-sdk/MyExporterExtensions.cs @@ -1,4 +1,4 @@ -// +// // Copyright The OpenTelemetry Authors // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,7 +18,7 @@ using OpenTelemetry; using OpenTelemetry.Trace; -internal static class MyExporterHelperExtensions +internal static class MyExporterExtensions { public static TracerProviderBuilder AddMyExporter(this TracerProviderBuilder builder) { diff --git a/docs/trace/extending-the-sdk/README.md b/docs/trace/extending-the-sdk/README.md index fb93bc10be9..3234dcba210 100644 --- a/docs/trace/extending-the-sdk/README.md +++ b/docs/trace/extending-the-sdk/README.md @@ -22,11 +22,12 @@ not covered by the built-in exporters: * Exporters should derive from `OpenTelemetry.BaseExporter` (which belongs to the [OpenTelemetry](../../../src/OpenTelemetry/README.md) package) and implement the `Export` method. -* Exporters can optionally implement the `OnShutdown` method. +* Exporters can optionally implement the `OnForceFlush` and `OnShutdown` method. * Depending on user's choice and load on the application, `Export` may get called with one or more activities. * Exporters will only receive sampled-in and ended activities. -* Exporters should not throw exceptions from `Export` and `OnShutdown`. +* Exporters should not throw exceptions from `Export`, `OnForceFlush` and + `OnShutdown`. * Exporters should not modify activities they receive (the same activity may be exported again by different exporter). * Exporters are responsible for any retry logic needed by the scenario. The SDK @@ -59,8 +60,8 @@ A demo exporter which simply writes activity name to the console is shown [here](./MyExporter.cs). Apart from the exporter itself, you should also provide extension methods as -shown [here](./MyExporterHelperExtensions.cs). This allows users to add the -Exporter to the `TracerProvider` as shown in the example [here](./Program.cs). +shown [here](./MyExporterExtensions.cs). This allows users to add the Exporter +to the `TracerProvider` as shown in the example [here](./Program.cs). ## Instrumentation Library @@ -126,7 +127,7 @@ Writing an instrumentation library typically involves 3 steps. .NET Framework, which publishes events using `EventSource`. The [SqlClient instrumentation library](../../../src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlEventSourceListener.netfx.cs), - in this case subscribes to the `EventSource` callbacks + in this case subscribes to the `EventSource` callbacks. 2. Second step is to emit activities using the [ActivitySource API](../../../src/OpenTelemetry.Api/README.md#introduction-to-opentelemetry-net-tracing-api). @@ -137,6 +138,18 @@ Writing an instrumentation library typically involves 3 steps. instrumentation library (eg: "OpenTelemetry.Instrumentation.StackExchangeRedis") and *not* the instrumented library (eg: "StackExchange.Redis") + 1. [Context Propagation](../../../src/OpenTelemetry.Api/README.md#context-propagation): + If your library initiates out of process requests or + accepts them, the library needs to + [inject the `PropagationContext`](../../../examples/MicroserviceExample/Utils/Messaging/MessageSender.cs) + to outgoing requests and + [extract the context](../../../examples/MicroserviceExample/Utils/Messaging/MessageReceiver.cs) + and hydrate the Activity/Baggage upon receiving incoming requests. + This is only required if you're using your own protocol to + communicate over the wire. + (i.e. If you're using an already instrumented HttpClient or GrpcClient, + this is already provided to you and **do not require** + injecting/extracting `PropagationContext` explicitly again.) 3. Third step is an optional step, and involves providing extension methods on `TracerProviderBuilder`, to enable the instrumentation. This is optional, and @@ -238,13 +251,13 @@ When using such a filtering processor, instead of using extension method to register the exporter, they must be registered manually as shown below: ```csharp - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .SetSampler(new MySampler()) - .AddSource("OTel.Demo") - .AddProcessor(new MyFilteringProcessor( - new SimpleActivityExportProcessor(new MyExporter("ExporterX")), - (act) => true)) - .Build(); +using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .SetSampler(new MySampler()) + .AddSource("OTel.Demo") + .AddProcessor(new MyFilteringProcessor( + new SimpleActivityExportProcessor(new MyExporter("ExporterX")), + (act) => true)) + .Build(); ``` Most [instrumentation libraries](#instrumentation-library) shipped from this diff --git a/docs/trace/extending-the-sdk/extending-the-sdk.csproj b/docs/trace/extending-the-sdk/extending-the-sdk.csproj index 1a85aa6120a..85aab7a7ed3 100644 --- a/docs/trace/extending-the-sdk/extending-the-sdk.csproj +++ b/docs/trace/extending-the-sdk/extending-the-sdk.csproj @@ -1,8 +1,5 @@ - diff --git a/docs/trace/getting-started/getting-started.csproj b/docs/trace/getting-started/getting-started.csproj index afb37b73c59..f68cdb2a7fe 100644 --- a/docs/trace/getting-started/getting-started.csproj +++ b/docs/trace/getting-started/getting-started.csproj @@ -1,8 +1,5 @@  - diff --git a/docs/trace/exception-reporting/Program.cs b/docs/trace/reporting-exceptions/Program.cs similarity index 100% rename from docs/trace/exception-reporting/Program.cs rename to docs/trace/reporting-exceptions/Program.cs diff --git a/docs/trace/exception-reporting/README.md b/docs/trace/reporting-exceptions/README.md similarity index 99% rename from docs/trace/exception-reporting/README.md rename to docs/trace/reporting-exceptions/README.md index 5e397910ed6..74d2d4e1c3f 100644 --- a/docs/trace/exception-reporting/README.md +++ b/docs/trace/reporting-exceptions/README.md @@ -1,4 +1,4 @@ -# Exception Reporting +# Reporting Exceptions The following doc describes how to report Exceptions to OpenTelemetry tracing when user is manually creating Activities. If the user is using one of the diff --git a/docs/trace/reporting-exceptions/reporting-exceptions.csproj b/docs/trace/reporting-exceptions/reporting-exceptions.csproj new file mode 100644 index 00000000000..f68cdb2a7fe --- /dev/null +++ b/docs/trace/reporting-exceptions/reporting-exceptions.csproj @@ -0,0 +1,5 @@ + + + + + diff --git a/examples/AspNet/Controllers/WeatherForecastController.cs b/examples/AspNet/Controllers/WeatherForecastController.cs index 39bd26e23af..092bdf07104 100644 --- a/examples/AspNet/Controllers/WeatherForecastController.cs +++ b/examples/AspNet/Controllers/WeatherForecastController.cs @@ -20,9 +20,11 @@ using System.Linq; using System.Net; using System.Net.Http; +using System.Security.Cryptography; using System.Threading.Tasks; using System.Web.Http; using Examples.AspNet.Models; +using OpenTelemetry; namespace Examples.AspNet.Controllers { @@ -71,6 +73,57 @@ public async Task> Get(int customerId) return GetWeatherForecast(); } + /// + /// For testing large async operation which causes IIS to jump threads and results in lost AsyncLocals. + /// + [Route("data")] + [HttpGet] + public async Task GetData() + { + Baggage.SetBaggage("key1", "value1"); + + using var rng = RandomNumberGenerator.Create(); + + var requestData = new byte[1024 * 1024 * 100]; + rng.GetBytes(requestData); + + using var client = new HttpClient(); + + using var request = new HttpRequestMessage(HttpMethod.Post, this.Url.Content("~/data")); + + request.Content = new ByteArrayContent(requestData); + + using var response = await client.SendAsync(request).ConfigureAwait(false); + + response.EnsureSuccessStatusCode(); + + var responseData = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false); + + return responseData.SequenceEqual(responseData) ? "match" : "mismatch"; + } + + [Route("data")] + [HttpPost] + public async Task PostData() + { + string value1 = Baggage.GetBaggage("key1"); + if (string.IsNullOrEmpty(value1)) + { + throw new InvalidOperationException("Key1 was not found on Baggage."); + } + + var stream = await this.Request.Content.ReadAsStreamAsync().ConfigureAwait(false); + + var result = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StreamContent(stream), + }; + + result.Content.Headers.ContentType = this.Request.Content.Headers.ContentType; + + return result; + } + private static IEnumerable GetWeatherForecast() { var rng = new Random(); diff --git a/examples/AspNet/Examples.AspNet.csproj b/examples/AspNet/Examples.AspNet.csproj index b67f0c72313..50ed1730846 100644 --- a/examples/AspNet/Examples.AspNet.csproj +++ b/examples/AspNet/Examples.AspNet.csproj @@ -61,6 +61,7 @@ + diff --git a/examples/AspNet/SuppressInstrumentationHttpModule.cs b/examples/AspNet/SuppressInstrumentationHttpModule.cs new file mode 100644 index 00000000000..fb832109182 --- /dev/null +++ b/examples/AspNet/SuppressInstrumentationHttpModule.cs @@ -0,0 +1,58 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Web; +using OpenTelemetry; + +namespace Examples.AspNet +{ + /// + /// A demo which will suppress ASP.NET + /// instrumentation if a request contains "suppress=true" on the query + /// string. Suppressed spans will not be processed/exported by the + /// OpenTelemetry SDK. + /// + public class SuppressInstrumentationHttpModule : IHttpModule + { + private IDisposable suppressionScope; + + public void Init(HttpApplication context) + { + context.BeginRequest += this.Application_BeginRequest; + context.EndRequest += this.Application_EndRequest; + } + + public void Dispose() + { + } + + private void Application_BeginRequest(object sender, EventArgs e) + { + var context = ((HttpApplication)sender).Context; + + if (context.Request.QueryString["suppress"] == "true") + { + this.suppressionScope = SuppressInstrumentationScope.Begin(); + } + } + + private void Application_EndRequest(object sender, EventArgs e) + { + this.suppressionScope?.Dispose(); + } + } +} diff --git a/examples/AspNet/Web.config b/examples/AspNet/Web.config index fcf320ae33e..c81722ba16a 100644 --- a/examples/AspNet/Web.config +++ b/examples/AspNet/Web.config @@ -13,7 +13,9 @@ - + @@ -23,8 +25,14 @@ + + + + + + @@ -37,20 +45,16 @@ - - + + - - - - - - + + diff --git a/examples/AspNetCore/Examples.AspNetCore.csproj b/examples/AspNetCore/Examples.AspNetCore.csproj index e067ca3eba6..d049a3ceba9 100644 --- a/examples/AspNetCore/Examples.AspNetCore.csproj +++ b/examples/AspNetCore/Examples.AspNetCore.csproj @@ -8,17 +8,20 @@ + + + + + - - diff --git a/examples/AspNetCore/Program.cs b/examples/AspNetCore/Program.cs index fa6965e5c90..ccda3c8a3e5 100644 --- a/examples/AspNetCore/Program.cs +++ b/examples/AspNetCore/Program.cs @@ -14,11 +14,14 @@ // limitations under the License. // +using System; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using OpenTelemetry.Logs; +using OpenTelemetry.Resources; namespace Examples.AspNetCore { @@ -40,17 +43,38 @@ public static IHostBuilder CreateHostBuilder(string[] args) => builder.ClearProviders(); builder.AddConsole(); - var useLogging = context.Configuration.GetValue("UseLogging"); - if (useLogging) + var logExporter = context.Configuration.GetValue("UseLogExporter").ToLowerInvariant(); + switch (logExporter) { - builder.AddOpenTelemetry(options => - { - options.IncludeScopes = true; - options.ParseStateValues = true; - options.IncludeFormattedMessage = true; - options.AddConsoleExporter(); - }); + case "otlp": + // Adding the OtlpExporter creates a GrpcChannel. + // This switch must be set before creating a GrpcChannel when calling an insecure gRPC service. + // See: https://docs.microsoft.com/aspnet/core/grpc/troubleshoot#call-insecure-grpc-services-with-net-core-client + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + builder.AddOpenTelemetry(options => + { + options.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(context.Configuration.GetValue("Otlp:ServiceName"))); + options.AddOtlpExporter(otlpOptions => + { + otlpOptions.Endpoint = new Uri(context.Configuration.GetValue("Otlp:Endpoint")); + }); + }); + break; + + default: + builder.AddOpenTelemetry(options => + { + options.AddConsoleExporter(); + }); + break; } + + builder.Services.Configure(opt => + { + opt.IncludeScopes = true; + opt.ParseStateValues = true; + opt.IncludeFormattedMessage = true; + }); }); } } diff --git a/examples/AspNetCore/Startup.cs b/examples/AspNetCore/Startup.cs index a6ec49f9f54..c0f0cf2b360 100644 --- a/examples/AspNetCore/Startup.cs +++ b/examples/AspNetCore/Startup.cs @@ -23,7 +23,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.OpenApi.Models; -using OpenTelemetry; using OpenTelemetry.Exporter; using OpenTelemetry.Instrumentation.AspNetCore; using OpenTelemetry.Metrics; @@ -34,8 +33,6 @@ namespace Examples.AspNetCore { public class Startup { - private MeterProvider meterProvider; - public Startup(IConfiguration configuration) { this.Configuration = configuration; @@ -47,6 +44,9 @@ public void ConfigureServices(IServiceCollection services) { services.AddControllers(); + // Enable HttpClientFactory integration for customization of the HttpClient used for export calls. + services.AddHttpClient(); + services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" }); @@ -59,9 +59,9 @@ public void ConfigureServices(IServiceCollection services) } }); - // Switch between Zipkin/Jaeger by setting UseExporter in appsettings.json. - var exporter = this.Configuration.GetValue("UseExporter").ToLowerInvariant(); - switch (exporter) + // Switch between Zipkin/Jaeger/OTLP by setting UseExporter in appsettings.json. + var tracingExporter = this.Configuration.GetValue("UseTracingExporter").ToLowerInvariant(); + switch (tracingExporter) { case "jaeger": services.AddOpenTelemetryTracing((builder) => builder @@ -72,6 +72,9 @@ public void ConfigureServices(IServiceCollection services) .AddJaegerExporter()); services.Configure(this.Configuration.GetSection("Jaeger")); + + // Customize the HttpClient that will be used when JaegerExporter is configured for HTTP transport. + services.AddHttpClient("JaegerExporter", configureClient: (client) => client.DefaultRequestHeaders.Add("X-MyCustomHeader", "value")); break; case "zipkin": services.AddOpenTelemetryTracing((builder) => builder @@ -85,7 +88,7 @@ public void ConfigureServices(IServiceCollection services) break; case "otlp": // Adding the OtlpExporter creates a GrpcChannel. - // This switch must be set before creating a GrpcChannel/HttpClient when calling an insecure gRPC service. + // This switch must be set before creating a GrpcChannel when calling an insecure gRPC service. // See: https://docs.microsoft.com/aspnet/core/grpc/troubleshoot#call-insecure-grpc-services-with-net-core-client AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); @@ -121,15 +124,30 @@ public void ConfigureServices(IServiceCollection services) break; } - // TODO: Add IServiceCollection.AddOpenTelemetryMetrics extension method - var providerBuilder = Sdk.CreateMeterProviderBuilder() - .AddAspNetCoreInstrumentation(); - - // TODO: Add configuration switch for Prometheus and OTLP export - providerBuilder - .AddConsoleExporter(); + var metricsExporter = this.Configuration.GetValue("UseMetricsExporter").ToLowerInvariant(); + services.AddOpenTelemetryMetrics(builder => + { + builder.AddAspNetCoreInstrumentation(); - this.meterProvider = providerBuilder.Build(); + switch (metricsExporter) + { + case "prometheus": + builder.AddPrometheusExporter(); + break; + case "otlp": + builder.AddOtlpExporter(); + break; + default: + builder.AddConsoleExporter(options => + { + // The ConsoleMetricExporter defaults to a manual collect cycle. + // This configuration causes metrics to be exported to stdout on a 10s interval. + options.MetricReaderType = MetricReaderType.Periodic; + options.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds = 10000; + }); + break; + } + }); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) @@ -150,6 +168,12 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseRouting(); + var metricsExporter = this.Configuration.GetValue("UseMetricsExporter").ToLowerInvariant(); + if (metricsExporter == "prometheus") + { + app.UseOpenTelemetryPrometheusScrapingEndpoint(); + } + app.UseEndpoints(endpoints => { endpoints.MapControllers(); diff --git a/examples/AspNetCore/appsettings.json b/examples/AspNetCore/appsettings.json index d46306b50e6..3ae6553bb0e 100644 --- a/examples/AspNetCore/appsettings.json +++ b/examples/AspNetCore/appsettings.json @@ -8,12 +8,15 @@ } }, "AllowedHosts": "*", - "UseExporter": "console", - "UseLogging": true, + "UseTracingExporter": "console", + "UseMetricsExporter": "console", + "UseLogExporter": "console", "Jaeger": { "ServiceName": "jaeger-test", "AgentHost": "localhost", - "AgentPort": 6831 + "AgentPort": 6831, + "Endpoint": "http://localhost:14268", + "Protocol": "UdpCompactThrift" }, "Zipkin": { "ServiceName": "zipkin-test", diff --git a/examples/Console/Examples.Console.csproj b/examples/Console/Examples.Console.csproj index 84ecf4f5b71..9a8217606ca 100644 --- a/examples/Console/Examples.Console.csproj +++ b/examples/Console/Examples.Console.csproj @@ -1,8 +1,8 @@ - + Exe - netcoreapp3.1 + net6.0 false $(NoWarn),CS0618 @@ -37,5 +37,6 @@ + diff --git a/examples/Console/InstrumentationWithActivitySource.cs b/examples/Console/InstrumentationWithActivitySource.cs index 0e2b320e25e..ee7927f8c0d 100644 --- a/examples/Console/InstrumentationWithActivitySource.cs +++ b/examples/Console/InstrumentationWithActivitySource.cs @@ -30,8 +30,8 @@ namespace Examples.Console internal class InstrumentationWithActivitySource : IDisposable { private const string RequestPath = "/api/request"; - private SampleServer server = new SampleServer(); - private SampleClient client = new SampleClient(); + private readonly SampleServer server = new SampleServer(); + private readonly SampleClient client = new SampleClient(); public void Start(ushort port = 19999) { @@ -48,7 +48,7 @@ public void Dispose() private class SampleServer : IDisposable { - private HttpListener listener = new HttpListener(); + private readonly HttpListener listener = new HttpListener(); public void Start(string url) { diff --git a/examples/Console/Program.cs b/examples/Console/Program.cs index fd78e75b864..d00d11db443 100644 --- a/examples/Console/Program.cs +++ b/examples/Console/Program.cs @@ -31,8 +31,8 @@ public class Program /// dotnet run -p Examples.Console.csproj inmemory /// dotnet run -p Examples.Console.csproj zipkin -u http://localhost:9411/api/v2/spans /// dotnet run -p Examples.Console.csproj jaeger -h localhost -p 6831 - /// dotnet run -p Examples.Console.csproj prometheus -i 15 -p 9184 -d 2 - /// dotnet run -p Examples.Console.csproj otlp -e "http://localhost:4317" + /// dotnet run -p Examples.Console.csproj prometheus -p 9184 -d 2 + /// dotnet run -p Examples.Console.csproj otlp -e "http://localhost:4317" -p "grpc" /// dotnet run -p Examples.Console.csproj zpages /// dotnet run -p Examples.Console.csproj metrics --help /// @@ -42,12 +42,13 @@ public class Program /// Arguments from command line. public static void Main(string[] args) { - Parser.Default.ParseArguments(args) + Parser.Default.ParseArguments(args) .MapResult( (JaegerOptions options) => TestJaegerExporter.Run(options.Host, options.Port), (ZipkinOptions options) => TestZipkinExporter.Run(options.Uri), - (PrometheusOptions options) => TestPrometheusExporter.Run(options.Port, options.DurationInMins), + (PrometheusOptions options) => TestPrometheusExporter.Run(options.Port), (MetricsOptions options) => TestMetrics.Run(options), + (LogsOptions options) => TestLogs.Run(options), (GrpcNetClientOptions options) => TestGrpcNetClient.Run(), (HttpClientOptions options) => TestHttpClient.Run(), (RedisOptions options) => TestRedis.Run(options.Uri), @@ -55,7 +56,7 @@ public static void Main(string[] args) (ConsoleOptions options) => TestConsoleExporter.Run(options), (OpenTelemetryShimOptions options) => TestOTelShimWithConsoleExporter.Run(options), (OpenTracingShimOptions options) => TestOpenTracingShim.Run(options), - (OtlpOptions options) => TestOtlpExporter.Run(options.Endpoint), + (OtlpOptions options) => TestOtlpExporter.Run(options.Endpoint, options.Protocol), (InMemoryOptions options) => TestInMemoryExporter.Run(options), errs => 1); } @@ -83,32 +84,26 @@ internal class ZipkinOptions [Verb("prometheus", HelpText = "Specify the options required to test Prometheus")] internal class PrometheusOptions { - [Option('p', "port", Default = 9184, 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)] + [Option('p', "port", Default = 9184, HelpText = "The port to expose metrics. The endpoint will be http://localhost:port/metrics/ (this is the port from which your Prometheus server scraps metrics from.)", Required = false)] public int Port { get; set; } - - [Option('d', "duration", Default = 2, HelpText = "Total duration in minutes to run the demo.", Required = false)] - public int DurationInMins { get; set; } } [Verb("metrics", HelpText = "Specify the options required to test Metrics")] internal class MetricsOptions { - [Option('d', "IsDelta", HelpText = "Export Delta metrics", Required = false, Default = true)] + [Option('d', "IsDelta", HelpText = "Export Delta metrics", Required = false, Default = false)] public bool IsDelta { get; set; } - [Option('g', "Gauge", HelpText = "Include Observable Gauge.", Required = false)] + [Option('g', "Gauge", HelpText = "Include Observable Gauge.", Required = false, Default = false)] public bool? FlagGauge { get; set; } - [Option('u', "UpDownCounter", HelpText = "Include Observable Up/Down Counter.", Required = false)] - public bool? FlagUpDownCounter { get; set; } - - [Option('c', "Counter", HelpText = "Include Counter.", Required = false)] + [Option('c', "Counter", HelpText = "Include Counter.", Required = false, Default = true)] public bool? FlagCounter { get; set; } - [Option('h', "Histogram", HelpText = "Include Histogram.", Required = false)] + [Option('h', "Histogram", HelpText = "Include Histogram.", Required = false, Default = false)] public bool? FlagHistogram { get; set; } - [Option("defaultCollection", Default = 500, HelpText = "Default collection period in milliseconds.", Required = false)] + [Option("defaultCollection", Default = 1000, HelpText = "Default collection period in milliseconds.", Required = false)] public int DefaultCollectionPeriodMilliseconds { get; set; } [Option("runtime", Default = 5000, HelpText = "Run time in milliseconds.", Required = false)] @@ -166,6 +161,16 @@ internal class OtlpOptions { [Option('e', "endpoint", HelpText = "Target to which the exporter is going to send traces or metrics", Default = "http://localhost:4317")] 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; } + } + + [Verb("logs", HelpText = "Specify the options required to test Logs")] + internal class LogsOptions + { + [Option("useExporter", Default = "otlp", HelpText = "Options include otlp or console.", Required = false)] + public string UseExporter { get; set; } } [Verb("inmemory", HelpText = "Specify the options required to test InMemory Exporter")] diff --git a/examples/Console/TestConsoleExporter.cs b/examples/Console/TestConsoleExporter.cs index 934236ae9f7..9929beab33b 100644 --- a/examples/Console/TestConsoleExporter.cs +++ b/examples/Console/TestConsoleExporter.cs @@ -37,7 +37,7 @@ private static object RunWithActivitySource() { // Enable OpenTelemetry for the sources "Samples.SampleServer" and "Samples.SampleClient" // and use Console exporter. - using var openTelemetry = Sdk.CreateTracerProviderBuilder() + using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource("Samples.SampleClient", "Samples.SampleServer") .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("console-test")) .AddProcessor(new MyProcessor()) // This must be added before ConsoleExporter diff --git a/examples/Console/TestGrpcNetClient.cs b/examples/Console/TestGrpcNetClient.cs index a15bd6fbde3..b1d884aa789 100644 --- a/examples/Console/TestGrpcNetClient.cs +++ b/examples/Console/TestGrpcNetClient.cs @@ -41,7 +41,7 @@ internal static object Run() // // dotnet run grpc - using var openTelemetry = Sdk.CreateTracerProviderBuilder() + using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddGrpcClientInstrumentation() .AddSource("grpc-net-client-test") .AddConsoleExporter() diff --git a/examples/Console/TestHttpClient.cs b/examples/Console/TestHttpClient.cs index 78d0047a993..734cae06b58 100644 --- a/examples/Console/TestHttpClient.cs +++ b/examples/Console/TestHttpClient.cs @@ -32,7 +32,7 @@ internal static object Run() { System.Console.WriteLine("Hello World!"); - using var openTelemetry = Sdk.CreateTracerProviderBuilder() + using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddHttpClientInstrumentation() .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("http-service-example")) .AddSource("http-client-test") diff --git a/examples/Console/TestInMemoryExporter.cs b/examples/Console/TestInMemoryExporter.cs index bbebdaf46a0..b7f0f956747 100644 --- a/examples/Console/TestInMemoryExporter.cs +++ b/examples/Console/TestInMemoryExporter.cs @@ -49,7 +49,7 @@ private static void RunWithActivitySource(ICollection exportedItems) { // Enable OpenTelemetry for the sources "Samples.SampleServer" and "Samples.SampleClient" // and use InMemory exporter. - using var openTelemetry = Sdk.CreateTracerProviderBuilder() + using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource("Samples.SampleClient", "Samples.SampleServer") .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("inmemory-test")) .AddInMemoryExporter(exportedItems) diff --git a/examples/Console/TestJaegerExporter.cs b/examples/Console/TestJaegerExporter.cs index dc8aad47ee2..2c67e9bff66 100644 --- a/examples/Console/TestJaegerExporter.cs +++ b/examples/Console/TestJaegerExporter.cs @@ -55,7 +55,7 @@ internal static object RunWithActivity(string host, int port) { // Enable OpenTelemetry for the sources "Samples.SampleServer" and "Samples.SampleClient" // and use the Jaeger exporter. - using var openTelemetry = Sdk.CreateTracerProviderBuilder() + using var tracerProvider = Sdk.CreateTracerProviderBuilder() .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("jaeger-test")) .AddSource("Samples.SampleClient", "Samples.SampleServer") .AddJaegerExporter(o => @@ -87,8 +87,8 @@ internal static object RunWithActivity(string host, int port) { sample.Start(); - System.Console.WriteLine("Traces are being created and exported" + - "to Jaeger in the background. Use Jaeger to view them." + + System.Console.WriteLine("Traces are being created and exported " + + "to Jaeger in the background. Use Jaeger to view them. " + "Press ENTER to stop."); System.Console.ReadLine(); } diff --git a/examples/Console/TestLogs.cs b/examples/Console/TestLogs.cs new file mode 100644 index 00000000000..195fe41fefc --- /dev/null +++ b/examples/Console/TestLogs.cs @@ -0,0 +1,75 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using Microsoft.Extensions.Logging; +using OpenTelemetry.Logs; + +namespace Examples.Console +{ + internal class TestLogs + { + internal static object Run(LogsOptions options) + { + using var loggerFactory = LoggerFactory.Create(builder => + { + builder.AddOpenTelemetry((opt) => + { + opt.IncludeFormattedMessage = true; + if (options.UseExporter.Equals("otlp", StringComparison.OrdinalIgnoreCase)) + { + /* + * Prerequisite to run this example: + * Set up an OpenTelemetry Collector to run on local docker. + * + * Open a terminal window at the examples/Console/ directory and + * launch the OpenTelemetry Collector with an OTLP receiver, by running: + * + * - On Unix based systems use: + * docker run --rm -it -p 4317:4317 -v $(pwd):/cfg otel/opentelemetry-collector:0.33.0 --config=/cfg/otlp-collector-example/config.yaml + * + * - On Windows use: + * docker run --rm -it -p 4317:4317 -v "%cd%":/cfg otel/opentelemetry-collector:0.33.0 --config=/cfg/otlp-collector-example/config.yaml + * + * Open another terminal window at the examples/Console/ directory and + * launch the OTLP example by running: + * + * dotnet run logs --useExporter otlp + * + * The OpenTelemetry Collector will output all received metrics to the stdout of its terminal. + * + */ + + // Adding the OtlpExporter creates a GrpcChannel. + // This switch must be set before creating a GrpcChannel when calling an insecure gRPC service. + // See: https://docs.microsoft.com/aspnet/core/grpc/troubleshoot#call-insecure-grpc-services-with-net-core-client + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + + opt.AddOtlpExporter(); + } + else + { + opt.AddConsoleExporter(); + } + }); + }); + + var logger = loggerFactory.CreateLogger(); + logger.LogInformation("Hello from {name} {price}.", "tomato", 2.99); + return null; + } + } +} diff --git a/examples/Console/TestMetrics.cs b/examples/Console/TestMetrics.cs index ba381838b64..1f41b5380f3 100644 --- a/examples/Console/TestMetrics.cs +++ b/examples/Console/TestMetrics.cs @@ -30,11 +30,13 @@ internal class TestMetrics { internal static object Run(MetricsOptions options) { + using var meter = new Meter("TestMeter"); + var providerBuilder = Sdk.CreateMeterProviderBuilder() .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("myservice")) - .AddSource("TestMeter"); // All instruments from this meter are enabled. + .AddMeter(meter.Name); // All instruments from this meter are enabled. - if (options.UseExporter.ToLower() == "otlp") + if (options.UseExporter.Equals("otlp", StringComparison.OrdinalIgnoreCase)) { /* * Prerequisite to run this example: @@ -44,10 +46,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.28.0 --config=/cfg/otlp-collector-example/config.yaml + * docker run --rm -it -p 4317:4317 -v $(pwd):/cfg otel/opentelemetry-collector:0.33.0 --config=/cfg/otlp-collector-example/config.yaml * * - On Windows use: - * docker run --rm -it -p 4317:4317 -v "%cd%":/cfg otel/opentelemetry-collector:0.28.0 --config=/cfg/otlp-collector-example/config.yaml + * docker run --rm -it -p 4317:4317 -v "%cd%":/cfg otel/opentelemetry-collector:0.33.0 --config=/cfg/otlp-collector-example/config.yaml * * Open another terminal window at the examples/Console/ directory and * launch the OTLP example by running: @@ -59,15 +61,16 @@ internal static object Run(MetricsOptions options) */ // Adding the OtlpExporter creates a GrpcChannel. - // This switch must be set before creating a GrpcChannel/HttpClient when calling an insecure gRPC service. + // This switch must be set before creating a GrpcChannel when calling an insecure gRPC service. // See: https://docs.microsoft.com/aspnet/core/grpc/troubleshoot#call-insecure-grpc-services-with-net-core-client AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); providerBuilder .AddOtlpExporter(o => { - o.MetricExportIntervalMilliseconds = options.DefaultCollectionPeriodMilliseconds; - o.IsDelta = options.IsDelta; + o.MetricReaderType = MetricReaderType.Periodic; + o.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds = options.DefaultCollectionPeriodMilliseconds; + o.AggregationTemporality = options.IsDelta ? AggregationTemporality.Delta : AggregationTemporality.Cumulative; }); } else @@ -75,15 +78,14 @@ internal static object Run(MetricsOptions options) providerBuilder .AddConsoleExporter(o => { - o.MetricExportIntervalMilliseconds = options.DefaultCollectionPeriodMilliseconds; - o.IsDelta = options.IsDelta; + o.MetricReaderType = MetricReaderType.Periodic; + o.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds = options.DefaultCollectionPeriodMilliseconds; + o.AggregationTemporality = options.IsDelta ? AggregationTemporality.Delta : AggregationTemporality.Cumulative; }); } using var provider = providerBuilder.Build(); - using var meter = new Meter("TestMeter", "0.0.1"); - Counter counter = null; if (options.FlagCounter ?? true) { @@ -98,20 +100,7 @@ internal static object Run(MetricsOptions options) if (options.FlagGauge ?? false) { - var observableCounter = meter.CreateObservableGauge("gauge", () => - { - return new List>() - { - new Measurement( - (int)Process.GetCurrentProcess().PrivateMemorySize64, - new KeyValuePair("tag1", "value1")), - }; - }); - } - - if (options.FlagUpDownCounter ?? true) - { - var observableCounter = meter.CreateObservableCounter("updown", () => + var observableCounter = meter.CreateObservableGauge("gauge", () => { return new List>() { diff --git a/examples/Console/TestOpenTracingShim.cs b/examples/Console/TestOpenTracingShim.cs index a5d75eac7dc..a450a9c0360 100644 --- a/examples/Console/TestOpenTracingShim.cs +++ b/examples/Console/TestOpenTracingShim.cs @@ -29,7 +29,7 @@ internal static object Run(OpenTracingShimOptions options) { // Enable OpenTelemetry for the source "MyCompany.MyProduct.MyWebServer" // and use Console exporter. - using var openTelemetry = Sdk.CreateTracerProviderBuilder() + using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource("MyCompany.MyProduct.MyWebServer") .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("MyServiceName")) .AddConsoleExporter() diff --git a/examples/Console/TestOtlpExporter.cs b/examples/Console/TestOtlpExporter.cs index d8336cc9dab..de9f3fae92e 100644 --- a/examples/Console/TestOtlpExporter.cs +++ b/examples/Console/TestOtlpExporter.cs @@ -16,6 +16,7 @@ using System; using OpenTelemetry; +using OpenTelemetry.Exporter; using OpenTelemetry.Resources; using OpenTelemetry.Trace; @@ -23,7 +24,7 @@ namespace Examples.Console { internal static class TestOtlpExporter { - internal static object Run(string endpoint) + internal static object Run(string endpoint, string protocol) { /* * Prerequisite to run this example: @@ -33,10 +34,10 @@ internal static object Run(string endpoint) * 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.19.0 --config=/cfg/otlp-collector-example/config.yaml + * docker run --rm -it -p 4317:4317 -v $(pwd):/cfg otel/opentelemetry-collector:0.33.0 --config=/cfg/otlp-collector-example/config.yaml * * - On Windows use: - * docker run --rm -it -p 4317:4317 -v "%cd%":/cfg otel/opentelemetry-collector:0.19.0 --config=/cfg/otlp-collector-example/config.yaml + * docker run --rm -it -p 4317:4317 -v "%cd%":/cfg otel/opentelemetry-collector:0.33.0 --config=/cfg/otlp-collector-example/config.yaml * * Open another terminal window at the examples/Console/ directory and * launch the OTLP example by running: @@ -49,22 +50,33 @@ internal static object Run(string endpoint) * For more information about the OpenTelemetry Collector go to https://github.com/open-telemetry/opentelemetry-collector * */ - return RunWithActivitySource(endpoint); + return RunWithActivitySource(endpoint, protocol); } - private static object RunWithActivitySource(string endpoint) + private static object RunWithActivitySource(string endpoint, string protocol) { // Adding the OtlpExporter creates a GrpcChannel. - // This switch must be set before creating a GrpcChannel/HttpClient when calling an insecure gRPC service. + // This switch must be set before creating a GrpcChannel when calling an insecure gRPC service. // See: https://docs.microsoft.com/aspnet/core/grpc/troubleshoot#call-insecure-grpc-services-with-net-core-client AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + var otlpExportProtocol = ToOtlpExportProtocol(protocol); + if (!otlpExportProtocol.HasValue) + { + System.Console.WriteLine($"Export protocol {protocol} is not supported. Default protocol 'grpc' will be used."); + otlpExportProtocol = OtlpExportProtocol.Grpc; + } + // Enable OpenTelemetry for the sources "Samples.SampleServer" and "Samples.SampleClient" // and use OTLP exporter. - using var openTelemetry = Sdk.CreateTracerProviderBuilder() + using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource("Samples.SampleClient", "Samples.SampleServer") .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("otlp-test")) - .AddOtlpExporter(opt => opt.Endpoint = new Uri(endpoint)) + .AddOtlpExporter(opt => + { + opt.Endpoint = new Uri(endpoint); + opt.Protocol = otlpExportProtocol.Value; + }) .Build(); // The above line is required only in Applications @@ -73,7 +85,7 @@ private static object RunWithActivitySource(string endpoint) { sample.Start(); - System.Console.WriteLine("Traces are being created and exported" + + System.Console.WriteLine("Traces are being created and exported " + "to the OpenTelemetry Collector in the background. " + "Press ENTER to stop."); System.Console.ReadLine(); @@ -81,5 +93,13 @@ private static object RunWithActivitySource(string endpoint) return null; } + + private static OtlpExportProtocol? ToOtlpExportProtocol(string protocol) => + protocol.Trim().ToLower() switch + { + "grpc" => OtlpExportProtocol.Grpc, + "http/protobuf" => OtlpExportProtocol.HttpProtobuf, + _ => null, + }; } } diff --git a/examples/Console/TestPrometheusExporter.cs b/examples/Console/TestPrometheusExporter.cs index 17fbe05e435..0d3a917b6e1 100644 --- a/examples/Console/TestPrometheusExporter.cs +++ b/examples/Console/TestPrometheusExporter.cs @@ -14,7 +14,9 @@ // limitations under the License. // +using System; using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.Metrics; using System.Threading; using System.Threading.Tasks; @@ -22,60 +24,78 @@ using OpenTelemetry.Metrics; using OpenTelemetry.Trace; -namespace Examples.Console +namespace Examples.Console; + +internal class TestPrometheusExporter { - internal class TestPrometheusExporter + private static readonly Meter MyMeter = new Meter("MyMeter"); + private static readonly Meter MyMeter2 = new Meter("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 ThreadLocal(() => new Random()); + + internal static object Run(int port) { - private static readonly Meter MyMeter = new Meter("TestMeter", "0.0.1"); - private static readonly Counter Counter = MyMeter.CreateCounter("counter"); + /* prometheus.yml - internal static object Run(int port, int totalDurationInMins) - { - /* - Following is sample prometheus.yml config. Adjust port,interval as needed. + global: + scrape_interval: 1s + evaluation_interval: 1s + + scrape_configs: + - job_name: "opentelemetry" + static_configs: + - targets: ["localhost:9184"] + */ + + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(MyMeter.Name) + .AddMeter(MyMeter2.Name) + .AddPrometheusExporter(options => + { + options.StartHttpListener = true; + options.HttpListenerPrefixes = new string[] { $"http://localhost:{port}/" }; + options.ScrapeResponseCacheDurationMilliseconds = 0; + }) + .Build(); - scrape_configs: - # The job name is added as a label `job=` to any timeseries scraped from this config. - - job_name: 'OpenTelemetryTest' + var process = Process.GetCurrentProcess(); + MyMeter.CreateObservableCounter("thread.cpu_time", () => GetThreadCpuTime(process), "ms"); - # metrics_path defaults to '/metrics' - # scheme defaults to 'http'. + // If the same Instrument name+unit combination happened under different Meters, PrometheusExporter + // exporter will output duplicated metric names. Related issues and PRs: + // * https://github.com/open-telemetry/opentelemetry-specification/pull/2017 + // * https://github.com/open-telemetry/opentelemetry-specification/pull/2035 + // * https://github.com/open-telemetry/opentelemetry-dotnet/pull/2593 + // + // MyMeter2.CreateObservableCounter("thread.cpu_time", () => GetThreadCpuTime(process), "ms"); - static_configs: - - targets: ['localhost:9184'] - */ - using var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddSource("TestMeter") - .AddPrometheusExporter(opt => opt.Url = $"http://localhost:{port}/metrics/") - .Build(); + using var token = new CancellationTokenSource(); - using var token = new CancellationTokenSource(); - Task writeMetricTask = new Task(() => + Task.Run(() => + { + while (!token.IsCancellationRequested) { - while (!token.IsCancellationRequested) - { - Counter.Add( - 10, - new KeyValuePair("tag1", "value1"), - new KeyValuePair("tag2", "value2")); + 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")); + Task.Delay(10).Wait(); + } + }); - Counter.Add( - 100, - new KeyValuePair("tag1", "anothervalue"), - new KeyValuePair("tag2", "somethingelse")); - Task.Delay(10).Wait(); - } - }); - writeMetricTask.Start(); + System.Console.WriteLine($"PrometheusExporter is listening on http://localhost:{port}/metrics/"); + System.Console.WriteLine($"Press any key to exit..."); + System.Console.ReadKey(); + token.Cancel(); - token.CancelAfter(totalDurationInMins * 60 * 1000); + return null; + } - System.Console.WriteLine($"OpenTelemetry Prometheus Exporter is making metrics available at http://localhost:{port}/metrics/"); - System.Console.WriteLine($"Press Enter key to exit now or will exit automatically after {totalDurationInMins} minutes."); - System.Console.ReadLine(); - token.Cancel(); - System.Console.WriteLine("Exiting..."); - return null; + private static IEnumerable> GetThreadCpuTime(Process process) + { + foreach (ProcessThread thread in process.Threads) + { + yield return new(thread.TotalProcessorTime.TotalMilliseconds, new("ProcessId", process.Id), new("ThreadId", thread.Id)); } } } diff --git a/examples/Console/TestRedis.cs b/examples/Console/TestRedis.cs index 12981cc7928..3306449810c 100644 --- a/examples/Console/TestRedis.cs +++ b/examples/Console/TestRedis.cs @@ -42,7 +42,7 @@ internal static object Run(string zipkinUri) var connection = ConnectionMultiplexer.Connect("localhost"); // Configure exporter to export traces to Zipkin - using var openTelemetry = Sdk.CreateTracerProviderBuilder() + using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddZipkinExporter(o => { o.Endpoint = new Uri(zipkinUri); diff --git a/examples/Console/TestZPagesExporter.cs b/examples/Console/TestZPagesExporter.cs index 3d70f4b447f..9d80e361f36 100644 --- a/examples/Console/TestZPagesExporter.cs +++ b/examples/Console/TestZPagesExporter.cs @@ -33,7 +33,7 @@ internal static object Run() // Start the server httpServer.Start(); - using var openTelemetry = Sdk.CreateTracerProviderBuilder() + using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource("zpages-test") .AddZPagesExporter(o => { diff --git a/examples/Console/TestZipkinExporter.cs b/examples/Console/TestZipkinExporter.cs index 0d5ac8bb5f6..40c44551b84 100644 --- a/examples/Console/TestZipkinExporter.cs +++ b/examples/Console/TestZipkinExporter.cs @@ -37,7 +37,7 @@ internal static object Run(string zipkinUri) // Enable OpenTelemetry for the sources "Samples.SampleServer" and "Samples.SampleClient" // and use the Zipkin exporter. - using var openTelemetry = Sdk.CreateTracerProviderBuilder() + using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource("Samples.SampleClient", "Samples.SampleServer") .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("zipkin-test")) .AddZipkinExporter(o => @@ -50,7 +50,7 @@ internal static object Run(string zipkinUri) { sample.Start(); - System.Console.WriteLine("Traces are being created and exported" + + 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(); diff --git a/examples/Console/otlp-collector-example/config.yaml b/examples/Console/otlp-collector-example/config.yaml index 3c72edb7e38..10b24b00a3d 100644 --- a/examples/Console/otlp-collector-example/config.yaml +++ b/examples/Console/otlp-collector-example/config.yaml @@ -18,3 +18,9 @@ service: traces: receivers: [otlp] exporters: [logging] + metrics: + receivers: [otlp] + exporters: [logging] + logs: + receivers: [otlp] + exporters: [logging] diff --git a/examples/GrpcService/Examples.GrpcService.csproj b/examples/GrpcService/Examples.GrpcService.csproj index adef4b5508b..2a9028e8650 100644 --- a/examples/GrpcService/Examples.GrpcService.csproj +++ b/examples/GrpcService/Examples.GrpcService.csproj @@ -1,7 +1,7 @@ - netcoreapp3.1 + net6.0 diff --git a/examples/MicroserviceExample/README.md b/examples/MicroserviceExample/README.md index f19e6e1c8ee..e8d37e1ccaa 100644 --- a/examples/MicroserviceExample/README.md +++ b/examples/MicroserviceExample/README.md @@ -45,15 +45,17 @@ With everything running: * [Invoke the Web API](http://localhost:5000/SendMessage) to send a message. * If you have run RabbitMQ and Zipkin with default settings: - * View your traces with Zipkin [here](http://localhost:9411/zipkin) - * Manage RabbitMQ [here](http://localhost:15672/) + * Manage RabbitMQ by accessing the local endpoint + [http://localhost:15672/](http://localhost:15672/) * user = guest * password = guest + * View your traces with Zipkin by accessing the local endpoint + [http://localhost:9411/zipkin](http://localhost:9411/zipkin). ## References * [Docker Desktop](https://www.docker.com/products/docker-desktop) * [OpenTelemetry Project](https://opentelemetry.io/) * [RabbitMQ](https://www.rabbitmq.com/) -* [Worker Service](https://docs.microsoft.com/en-us/azure/azure-monitor/app/worker-service) +* [Worker Service](https://docs.microsoft.com/azure/azure-monitor/app/worker-service) * [Zipkin](https://zipkin.io) diff --git a/examples/MicroserviceExample/WebApi/Dockerfile b/examples/MicroserviceExample/WebApi/Dockerfile index b399fe8ca8e..b0b19f28b72 100644 --- a/examples/MicroserviceExample/WebApi/Dockerfile +++ b/examples/MicroserviceExample/WebApi/Dockerfile @@ -1,10 +1,10 @@ -FROM mcr.microsoft.com/dotnet/core/sdk:3.1-alpine AS build +FROM mcr.microsoft.com/dotnet/sdk:6.0-alpine AS build WORKDIR /app COPY . ./ RUN dotnet publish ./examples/MicroserviceExample/WebApi -c Release -o /out -p:IntegrationBuild=true -FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-alpine AS runtime +FROM mcr.microsoft.com/dotnet/aspnet:6.0-alpine AS runtime WORKDIR /app COPY --from=build /out ./ ENTRYPOINT ["dotnet", "WebApi.dll"] diff --git a/examples/MicroserviceExample/WebApi/WebApi.csproj b/examples/MicroserviceExample/WebApi/WebApi.csproj index 7d6c76474fc..876450b7176 100644 --- a/examples/MicroserviceExample/WebApi/WebApi.csproj +++ b/examples/MicroserviceExample/WebApi/WebApi.csproj @@ -1,6 +1,6 @@ - netcoreapp3.1 + net6.0 diff --git a/examples/MicroserviceExample/WorkerService/Dockerfile b/examples/MicroserviceExample/WorkerService/Dockerfile index 197b6d3485b..21e7222ee1b 100644 --- a/examples/MicroserviceExample/WorkerService/Dockerfile +++ b/examples/MicroserviceExample/WorkerService/Dockerfile @@ -1,10 +1,10 @@ -FROM mcr.microsoft.com/dotnet/core/sdk:3.1-alpine AS build +FROM mcr.microsoft.com/dotnet/sdk:6.0-alpine AS build WORKDIR /app COPY . ./ RUN dotnet publish ./examples/MicroserviceExample/WorkerService -c Release -o /out -p:IntegrationBuild=true -FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-alpine AS runtime +FROM mcr.microsoft.com/dotnet/aspnet:6.0-alpine AS runtime WORKDIR /app COPY --from=build /out ./ ENTRYPOINT ["dotnet", "WorkerService.dll"] diff --git a/examples/MicroserviceExample/WorkerService/WorkerService.csproj b/examples/MicroserviceExample/WorkerService/WorkerService.csproj index 8c3d83faf13..926e55dd6fd 100644 --- a/examples/MicroserviceExample/WorkerService/WorkerService.csproj +++ b/examples/MicroserviceExample/WorkerService/WorkerService.csproj @@ -1,6 +1,6 @@ - netcoreapp3.1 + net6.0 diff --git a/examples/MicroserviceExample/docker-compose.yml b/examples/MicroserviceExample/docker-compose.yml index 96915f48ab7..35385fae591 100644 --- a/examples/MicroserviceExample/docker-compose.yml +++ b/examples/MicroserviceExample/docker-compose.yml @@ -8,9 +8,6 @@ services: rabbitmq: image: rabbitmq:3-management-alpine - environment: - - RABBITMQ_DEFAULT_USER=guest - - RABBITMQ_DEFAULT_PASS=guest ports: - 5672:5672 - 15672:15672 diff --git a/src/OpenTelemetry.Api/.publicApi/net461/PublicAPI.Shipped.txt b/src/OpenTelemetry.Api/.publicApi/net461/PublicAPI.Shipped.txt index afb32227a29..f213ac4c665 100644 --- a/src/OpenTelemetry.Api/.publicApi/net461/PublicAPI.Shipped.txt +++ b/src/OpenTelemetry.Api/.publicApi/net461/PublicAPI.Shipped.txt @@ -178,9 +178,7 @@ static OpenTelemetry.Context.Propagation.Propagators.DefaultTextMapPropagator.ge 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 name) -> T static OpenTelemetry.Context.RuntimeContext.RegisterSlot(string slotName) -> OpenTelemetry.Context.RuntimeContextSlot -static OpenTelemetry.Context.RuntimeContext.SetValue(string name, T value) -> void static OpenTelemetry.Trace.ActivityExtensions.GetStatus(this System.Diagnostics.Activity activity) -> OpenTelemetry.Trace.Status static OpenTelemetry.Trace.ActivityExtensions.RecordException(this System.Diagnostics.Activity activity, System.Exception ex) -> void static OpenTelemetry.Trace.ActivityExtensions.SetStatus(this System.Diagnostics.Activity activity, OpenTelemetry.Trace.Status status) -> void diff --git a/src/OpenTelemetry.Api/.publicApi/net461/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Api/.publicApi/net461/PublicAPI.Unshipped.txt index edd1a3f6ec6..735591e1be1 100644 --- a/src/OpenTelemetry.Api/.publicApi/net461/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Api/.publicApi/net461/PublicAPI.Unshipped.txt @@ -1,4 +1,26 @@ +abstract OpenTelemetry.Metrics.MeterProviderBuilder.AddInstrumentation(System.Func instrumentationFactory) -> OpenTelemetry.Metrics.MeterProviderBuilder +abstract OpenTelemetry.Metrics.MeterProviderBuilder.AddMeter(params string[] names) -> OpenTelemetry.Metrics.MeterProviderBuilder +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.RemotingRuntimeContextSlot.Value.get -> object +OpenTelemetry.Context.RemotingRuntimeContextSlot.Value.set -> void +OpenTelemetry.Context.ThreadLocalRuntimeContextSlot.Value.get -> object +OpenTelemetry.Context.ThreadLocalRuntimeContextSlot.Value.set -> void +OpenTelemetry.Metrics.IDeferredMeterProviderBuilder +OpenTelemetry.Metrics.IDeferredMeterProviderBuilder.Configure(System.Action configure) -> OpenTelemetry.Metrics.MeterProviderBuilder +OpenTelemetry.Metrics.MeterProvider +OpenTelemetry.Metrics.MeterProvider.MeterProvider() -> void +OpenTelemetry.Metrics.MeterProviderBuilder +OpenTelemetry.Metrics.MeterProviderBuilder.MeterProviderBuilder() -> void override OpenTelemetry.Context.AsyncLocalRuntimeContextSlot.Get() -> T override OpenTelemetry.Context.AsyncLocalRuntimeContextSlot.Set(T value) -> void OpenTelemetry.Context.AsyncLocalRuntimeContextSlot OpenTelemetry.Context.AsyncLocalRuntimeContextSlot.AsyncLocalRuntimeContextSlot(string name) -> void +static OpenTelemetry.Context.RuntimeContext.GetValue(string slotName) -> object +static OpenTelemetry.Context.RuntimeContext.GetValue(string slotName) -> T +static OpenTelemetry.Context.RuntimeContext.SetValue(string slotName, object value) -> void +static OpenTelemetry.Context.RuntimeContext.SetValue(string slotName, T value) -> void +OpenTelemetry.Trace.TelemetrySpan.ParentSpanId.get -> System.Diagnostics.ActivitySpanId diff --git a/src/OpenTelemetry.Api/.publicApi/netstandard2.0/PublicAPI.Shipped.txt b/src/OpenTelemetry.Api/.publicApi/netstandard2.0/PublicAPI.Shipped.txt index 3ebfa521557..ab7a66bc6fc 100644 --- a/src/OpenTelemetry.Api/.publicApi/netstandard2.0/PublicAPI.Shipped.txt +++ b/src/OpenTelemetry.Api/.publicApi/netstandard2.0/PublicAPI.Shipped.txt @@ -178,9 +178,7 @@ static OpenTelemetry.Context.Propagation.Propagators.DefaultTextMapPropagator.ge 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 name) -> T static OpenTelemetry.Context.RuntimeContext.RegisterSlot(string slotName) -> OpenTelemetry.Context.RuntimeContextSlot -static OpenTelemetry.Context.RuntimeContext.SetValue(string name, T value) -> void static OpenTelemetry.Trace.ActivityExtensions.GetStatus(this System.Diagnostics.Activity activity) -> OpenTelemetry.Trace.Status static OpenTelemetry.Trace.ActivityExtensions.RecordException(this System.Diagnostics.Activity activity, System.Exception ex) -> void static OpenTelemetry.Trace.ActivityExtensions.SetStatus(this System.Diagnostics.Activity activity, OpenTelemetry.Trace.Status status) -> void diff --git a/src/OpenTelemetry.Api/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Api/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt index e69de29bb2d..5685843e09d 100644 --- a/src/OpenTelemetry.Api/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Api/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt @@ -0,0 +1,20 @@ +abstract OpenTelemetry.Metrics.MeterProviderBuilder.AddInstrumentation(System.Func instrumentationFactory) -> OpenTelemetry.Metrics.MeterProviderBuilder +abstract OpenTelemetry.Metrics.MeterProviderBuilder.AddMeter(params string[] names) -> OpenTelemetry.Metrics.MeterProviderBuilder +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.ThreadLocalRuntimeContextSlot.Value.get -> object +OpenTelemetry.Context.ThreadLocalRuntimeContextSlot.Value.set -> void +OpenTelemetry.Metrics.IDeferredMeterProviderBuilder +OpenTelemetry.Metrics.IDeferredMeterProviderBuilder.Configure(System.Action configure) -> OpenTelemetry.Metrics.MeterProviderBuilder +OpenTelemetry.Metrics.MeterProvider +OpenTelemetry.Metrics.MeterProvider.MeterProvider() -> void +OpenTelemetry.Metrics.MeterProviderBuilder +OpenTelemetry.Metrics.MeterProviderBuilder.MeterProviderBuilder() -> void +static OpenTelemetry.Context.RuntimeContext.GetValue(string slotName) -> object +static OpenTelemetry.Context.RuntimeContext.GetValue(string slotName) -> T +static OpenTelemetry.Context.RuntimeContext.SetValue(string slotName, object value) -> void +static OpenTelemetry.Context.RuntimeContext.SetValue(string slotName, T value) -> void +OpenTelemetry.Trace.TelemetrySpan.ParentSpanId.get -> System.Diagnostics.ActivitySpanId \ No newline at end of file diff --git a/src/OpenTelemetry.Api/Baggage.cs b/src/OpenTelemetry.Api/Baggage.cs index 2b4f40ba20f..b9804921964 100644 --- a/src/OpenTelemetry.Api/Baggage.cs +++ b/src/OpenTelemetry.Api/Baggage.cs @@ -18,6 +18,7 @@ using System.Collections.Generic; using System.Linq; using OpenTelemetry.Context; +using OpenTelemetry.Internal; namespace OpenTelemetry { @@ -29,7 +30,7 @@ namespace OpenTelemetry /// public readonly struct Baggage : IEquatable { - private static readonly RuntimeContextSlot RuntimeContextSlot = RuntimeContext.RegisterSlot("otel.baggage"); + private static readonly RuntimeContextSlot RuntimeContextSlot = RuntimeContext.RegisterSlot("otel.baggage"); private static readonly Dictionary EmptyBaggage = new Dictionary(); private readonly Dictionary baggage; @@ -46,10 +47,35 @@ internal Baggage(Dictionary baggage) /// /// Gets or sets the current . /// + /// + /// Note: returns a forked version of the current + /// Baggage. Changes to the forked version will not automatically be + /// reflected back on . To update either use one of the static methods that target + /// as the default source or set to a new instance of . + /// Examples: + /// + /// Baggage.SetBaggage("newKey1", "newValue1"); // Updates Baggage.Current with 'newKey1' + /// Baggage.SetBaggage("newKey2", "newValue2"); // Updates Baggage.Current with 'newKey2' + /// + /// Or: + /// + /// var baggageCopy = Baggage.Current; + /// Baggage.SetBaggage("newKey1", "newValue1"); // Updates Baggage.Current with 'newKey1' + /// var newBaggage = baggageCopy + /// .SetBaggage("newKey2", "newValue2"); + /// .SetBaggage("newKey3", "newValue3"); + /// // Sets Baggage.Current to 'newBaggage' which will override any + /// // changes made to Baggage.Current after the copy was made. For example + /// // the 'newKey1' change is lost. + /// Baggage.Current = newBaggage; + /// + /// public static Baggage Current { - get => RuntimeContextSlot.Get(); - set => RuntimeContextSlot.Set(value); + get => RuntimeContextSlot.Get()?.Baggage ?? default; + set => EnsureBaggageHolder().Baggage = value; } /// @@ -83,7 +109,7 @@ public static Baggage Create(Dictionary baggageItems = null) return default; } - Dictionary baggageCopy = new Dictionary(StringComparer.OrdinalIgnoreCase); + Dictionary baggageCopy = new Dictionary(baggageItems.Count, StringComparer.OrdinalIgnoreCase); foreach (KeyValuePair baggageItem in baggageItems) { if (string.IsNullOrEmpty(baggageItem.Value)) @@ -132,9 +158,18 @@ public static string GetBaggage(string name, Baggage baggage = default) /// Baggage item value. /// Optional . is used if not specified. /// New containing the key/value pair. + /// Note: The returned will be set as the new instance. [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "This was agreed on to be the friendliest API surface")] public static Baggage SetBaggage(string name, string value, Baggage baggage = default) - => baggage == default ? Current.SetBaggage(name, value) : baggage.SetBaggage(name, value); + { + var baggageHolder = EnsureBaggageHolder(); + lock (baggageHolder) + { + return baggageHolder.Baggage = baggage == default + ? baggageHolder.Baggage.SetBaggage(name, value) + : baggage.SetBaggage(name, value); + } + } /// /// Returns a new which contains the new key/value pair. @@ -142,9 +177,18 @@ public static Baggage SetBaggage(string name, string value, Baggage baggage = de /// Baggage key/value pairs. /// Optional . is used if not specified. /// New containing the key/value pair. + /// Note: The returned will be set as the new instance. [System.Diagnostics.CodeAnalysis.SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "This was agreed on to be the friendliest API surface")] public static Baggage SetBaggage(IEnumerable> baggageItems, Baggage baggage = default) - => baggage == default ? Current.SetBaggage(baggageItems) : baggage.SetBaggage(baggageItems); + { + var baggageHolder = EnsureBaggageHolder(); + lock (baggageHolder) + { + return baggageHolder.Baggage = baggage == default + ? baggageHolder.Baggage.SetBaggage(baggageItems) + : baggage.SetBaggage(baggageItems); + } + } /// /// Returns a new with the key/value pair removed. @@ -152,16 +196,34 @@ public static Baggage SetBaggage(IEnumerable> bagga /// Baggage item name. /// Optional . is used if not specified. /// New containing the key/value pair. + /// Note: The returned will be set as the new instance. public static Baggage RemoveBaggage(string name, Baggage baggage = default) - => baggage == default ? Current.RemoveBaggage(name) : baggage.RemoveBaggage(name); + { + var baggageHolder = EnsureBaggageHolder(); + lock (baggageHolder) + { + return baggageHolder.Baggage = baggage == default + ? baggageHolder.Baggage.RemoveBaggage(name) + : baggage.RemoveBaggage(name); + } + } /// /// Returns a new with all the key/value pairs removed. /// /// Optional . is used if not specified. /// New containing the key/value pair. + /// Note: The returned will be set as the new instance. public static Baggage ClearBaggage(Baggage baggage = default) - => baggage == default ? Current.ClearBaggage() : baggage.ClearBaggage(); + { + var baggageHolder = EnsureBaggageHolder(); + lock (baggageHolder) + { + return baggageHolder.Baggage = baggage == default + ? baggageHolder.Baggage.ClearBaggage() + : baggage.ClearBaggage(); + } + } /// /// Returns the name/value pairs in the . @@ -177,10 +239,7 @@ public IReadOnlyDictionary GetBaggage() /// Baggage item or if nothing was found. public string GetBaggage(string name) { - if (string.IsNullOrEmpty(name)) - { - throw new ArgumentNullException(nameof(name)); - } + Guard.ThrowIfNullOrEmpty(name, nameof(name)); return this.baggage != null && this.baggage.TryGetValue(name, out string value) ? value @@ -200,7 +259,7 @@ public Baggage SetBaggage(string name, string value) return this.RemoveBaggage(name); } - return Current = new Baggage( + return new Baggage( new Dictionary(this.baggage ?? EmptyBaggage, StringComparer.OrdinalIgnoreCase) { [name] = value, @@ -222,7 +281,7 @@ public Baggage SetBaggage(params KeyValuePair[] baggageItems) /// New containing the key/value pair. public Baggage SetBaggage(IEnumerable> baggageItems) { - if ((baggageItems?.Count() ?? 0) <= 0) + if (baggageItems?.Any() != true) { return this; } @@ -241,7 +300,7 @@ public Baggage SetBaggage(IEnumerable> baggageItems } } - return Current = new Baggage(newBaggage); + return new Baggage(newBaggage); } /// @@ -254,7 +313,7 @@ public Baggage RemoveBaggage(string name) var baggage = new Dictionary(this.baggage ?? EmptyBaggage, StringComparer.OrdinalIgnoreCase); baggage.Remove(name); - return Current = new Baggage(baggage); + return new Baggage(baggage); } /// @@ -262,7 +321,7 @@ public Baggage RemoveBaggage(string name) /// /// New containing the key/value pair. public Baggage ClearBaggage() - => Current = default; + => default; /// /// Returns an enumerator that iterates through the . @@ -305,5 +364,22 @@ public override int GetHashCode() return res; } } + + private static BaggageHolder EnsureBaggageHolder() + { + var baggageHolder = RuntimeContextSlot.Get(); + if (baggageHolder == null) + { + baggageHolder = new BaggageHolder(); + RuntimeContextSlot.Set(baggageHolder); + } + + return baggageHolder; + } + + private class BaggageHolder + { + public Baggage Baggage; + } } } diff --git a/src/OpenTelemetry.Api/CHANGELOG.md b/src/OpenTelemetry.Api/CHANGELOG.md index db9fc530363..6db2b5bcc56 100644 --- a/src/OpenTelemetry.Api/CHANGELOG.md +++ b/src/OpenTelemetry.Api/CHANGELOG.md @@ -2,6 +2,48 @@ ## Unreleased +* Added `ParentSpanId` to `TelemetrySpan` ([#2740](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2740)) + +## 1.2.0-rc1 + +Released 2021-Nov-29 + +## 1.2.0-beta2 + +Released 2021-Nov-19 + +* Updated System.Diagnostics.DiagnosticSource to version 6.0.0. + ([#2582](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2582)) + +## 1.2.0-beta1 + +Released 2021-Oct-08 + +* Added `IDeferredMeterProviderBuilder` + ([#2412](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2412)) + +* Breaking: Renamed `AddSource` to `AddMeter` on MeterProviderBuilder + to better reflect the intent of the method. + +## 1.2.0-alpha4 + +Released 2021-Sep-23 + +* Updated System.Diagnostics.DiagnosticSource to version 6.0.0-rc.1.21451.13 + +## 1.2.0-alpha3 + +Released 2021-Sep-13 + +* Static Baggage operations (`SetBaggage`, `RemoveBaggage`, & `ClearBaggage`) + are now thread-safe. Instance-based Baggage operations no longer mutate + `Baggage.Current` (breaking behavior change). For details see: + ([#2298](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2298)) + +## 1.2.0-alpha2 + +Released 2021-Aug-24 + ## 1.2.0-alpha1 Released 2021-Jul-23 diff --git a/src/OpenTelemetry.Api/Context/AsyncLocalRuntimeContextSlot.cs b/src/OpenTelemetry.Api/Context/AsyncLocalRuntimeContextSlot.cs index de5c5083a13..f38af6d6cf5 100644 --- a/src/OpenTelemetry.Api/Context/AsyncLocalRuntimeContextSlot.cs +++ b/src/OpenTelemetry.Api/Context/AsyncLocalRuntimeContextSlot.cs @@ -23,7 +23,7 @@ namespace OpenTelemetry.Context /// The async local implementation of context slot. /// /// The type of the underlying value. - public class AsyncLocalRuntimeContextSlot : RuntimeContextSlot + public class AsyncLocalRuntimeContextSlot : RuntimeContextSlot, IRuntimeContextSlotValueAccessor { private readonly AsyncLocal slot; @@ -37,6 +37,13 @@ public AsyncLocalRuntimeContextSlot(string name) this.slot = new AsyncLocal(); } + /// + public object Value + { + get => this.slot.Value; + set => this.slot.Value = (T)value; + } + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public override T Get() diff --git a/src/OpenTelemetry.Api/Context/IRuntimeContextSlotValueAccessor.cs b/src/OpenTelemetry.Api/Context/IRuntimeContextSlotValueAccessor.cs new file mode 100644 index 00000000000..eab76adfca1 --- /dev/null +++ b/src/OpenTelemetry.Api/Context/IRuntimeContextSlotValueAccessor.cs @@ -0,0 +1,29 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace OpenTelemetry.Context +{ + /// + /// Describes a type of which can expose its value as an . + /// + public interface IRuntimeContextSlotValueAccessor + { + /// + /// Gets or sets the value of the slot as an . + /// + object Value { get; set; } + } +} diff --git a/src/OpenTelemetry.Api/Context/Propagation/CompositeTextMapPropagator.cs b/src/OpenTelemetry.Api/Context/Propagation/CompositeTextMapPropagator.cs index 35e7d972113..5523db486ac 100644 --- a/src/OpenTelemetry.Api/Context/Propagation/CompositeTextMapPropagator.cs +++ b/src/OpenTelemetry.Api/Context/Propagation/CompositeTextMapPropagator.cs @@ -16,6 +16,7 @@ using System; using System.Collections.Generic; +using OpenTelemetry.Internal; namespace OpenTelemetry.Context.Propagation { @@ -34,7 +35,9 @@ public class CompositeTextMapPropagator : TextMapPropagator /// List of wire context propagator. public CompositeTextMapPropagator(IEnumerable propagators) { - this.propagators = new List(propagators ?? throw new ArgumentNullException(nameof(propagators))); + Guard.ThrowIfNull(propagators, nameof(propagators)); + + this.propagators = new List(propagators); } /// diff --git a/src/OpenTelemetry.Api/Context/Propagation/NoopTextMapPropagator.cs b/src/OpenTelemetry.Api/Context/Propagation/NoopTextMapPropagator.cs index 8060df2977e..e21dd369e08 100644 --- a/src/OpenTelemetry.Api/Context/Propagation/NoopTextMapPropagator.cs +++ b/src/OpenTelemetry.Api/Context/Propagation/NoopTextMapPropagator.cs @@ -19,7 +19,7 @@ namespace OpenTelemetry.Context.Propagation { - internal class NoopTextMapPropagator : TextMapPropagator + internal sealed class NoopTextMapPropagator : TextMapPropagator { private static readonly PropagationContext DefaultPropagationContext = default; diff --git a/src/OpenTelemetry.Api/Context/Propagation/TraceContextPropagator.cs b/src/OpenTelemetry.Api/Context/Propagation/TraceContextPropagator.cs index 58b9804a261..fc18d3a7851 100644 --- a/src/OpenTelemetry.Api/Context/Propagation/TraceContextPropagator.cs +++ b/src/OpenTelemetry.Api/Context/Propagation/TraceContextPropagator.cs @@ -330,7 +330,7 @@ private static byte HexCharToByte(char c) return (byte)(c - 'a' + 10); } - throw new ArgumentOutOfRangeException(nameof(c), c, $"Invalid character: {c}."); + throw new ArgumentOutOfRangeException(nameof(c), c, "Must be within: [0-9] or [a-f]"); } private static bool ValidateKey(ReadOnlySpan key) diff --git a/src/OpenTelemetry.Api/Context/RemotingRuntimeContextSlot.cs b/src/OpenTelemetry.Api/Context/RemotingRuntimeContextSlot.cs index f589d9900bc..c40dae89356 100644 --- a/src/OpenTelemetry.Api/Context/RemotingRuntimeContextSlot.cs +++ b/src/OpenTelemetry.Api/Context/RemotingRuntimeContextSlot.cs @@ -26,7 +26,7 @@ namespace OpenTelemetry.Context /// The .NET Remoting implementation of context slot. /// /// The type of the underlying value. - public class RemotingRuntimeContextSlot : RuntimeContextSlot + public class RemotingRuntimeContextSlot : RuntimeContextSlot, IRuntimeContextSlotValueAccessor { // A special workaround to suppress context propagation cross AppDomains. // @@ -50,24 +50,26 @@ public RemotingRuntimeContextSlot(string name) { } + /// + public object Value + { + get => this.Get(); + set => this.Set((T)value); + } + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public override T Get() { - var wrapper = CallContext.LogicalGetData(this.Name) as BitArray; - - if (wrapper == null) + if (!(CallContext.LogicalGetData(this.Name) is BitArray wrapper)) { - return default(T); + return default; } var value = WrapperField.GetValue(wrapper); - if (value is T) - { - return (T)value; - } - - return default(T); + return value is T t + ? t + : default; } /// diff --git a/src/OpenTelemetry.Api/Context/RuntimeContext.cs b/src/OpenTelemetry.Api/Context/RuntimeContext.cs index b0110e5ac3c..9f0b022f951 100644 --- a/src/OpenTelemetry.Api/Context/RuntimeContext.cs +++ b/src/OpenTelemetry.Api/Context/RuntimeContext.cs @@ -17,6 +17,7 @@ using System; using System.Collections.Concurrent; using System.Runtime.CompilerServices; +using OpenTelemetry.Internal; namespace OpenTelemetry.Context { @@ -40,16 +41,13 @@ public static class RuntimeContext /// The slot registered. public static RuntimeContextSlot RegisterSlot(string slotName) { - if (string.IsNullOrEmpty(slotName)) - { - throw new ArgumentException($"{nameof(slotName)} cannot be null or empty string."); - } + Guard.ThrowIfNullOrEmpty(slotName, nameof(slotName)); lock (Slots) { if (Slots.ContainsKey(slotName)) { - throw new InvalidOperationException($"The context slot {slotName} is already registered."); + throw new InvalidOperationException($"Context slot already registered: '{slotName}'"); } var type = ContextSlotType.MakeGenericType(typeof(T)); @@ -68,13 +66,10 @@ public static RuntimeContextSlot RegisterSlot(string slotName) /// The slot previously registered. public static RuntimeContextSlot GetSlot(string slotName) { - if (string.IsNullOrEmpty(slotName)) - { - throw new ArgumentException($"{nameof(slotName)} cannot be null or empty string."); - } - - Slots.TryGetValue(slotName, out var slot); - return slot as RuntimeContextSlot ?? throw new ArgumentException($"The context slot {slotName} is not found."); + Guard.ThrowIfNullOrEmpty(slotName, nameof(slotName)); + var slot = GuardNotFound(slotName); + var contextSlot = Guard.ThrowIfNotOfType>(slot, nameof(slot)); + return contextSlot; } /* @@ -104,27 +99,51 @@ public static IDictionary Snapshot() /// /// Sets the value to a registered slot. /// - /// The name of the context slot. + /// The name of the context slot. /// The value to be set. /// The type of the value. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void SetValue(string name, T value) + public static void SetValue(string slotName, T value) { - var slot = (RuntimeContextSlot)Slots[name]; - slot.Set(value); + GetSlot(slotName).Set(value); } /// /// Gets the value from a registered slot. /// - /// The name of the context slot. + /// The name of the context slot. /// The type of the value. /// The value retrieved from the context slot. [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static T GetValue(string name) + public static T GetValue(string slotName) + { + return GetSlot(slotName).Get(); + } + + /// + /// Sets the value to a registered slot. + /// + /// The name of the context slot. + /// The value to be set. + public static void SetValue(string slotName, object value) { - var slot = (RuntimeContextSlot)Slots[name]; - return slot.Get(); + Guard.ThrowIfNullOrEmpty(slotName, nameof(slotName)); + var slot = GuardNotFound(slotName); + var runtimeContextSlotValueAccessor = Guard.ThrowIfNotOfType(slot, nameof(slot)); + runtimeContextSlotValueAccessor.Value = value; + } + + /// + /// Gets the value from a registered slot. + /// + /// The name of the context slot. + /// The value retrieved from the context slot. + public static object GetValue(string slotName) + { + Guard.ThrowIfNullOrEmpty(slotName, nameof(slotName)); + var slot = GuardNotFound(slotName); + var runtimeContextSlotValueAccessor = Guard.ThrowIfNotOfType(slot, nameof(slot)); + return runtimeContextSlotValueAccessor.Value; } // For testing purpose @@ -132,5 +151,15 @@ internal static void Clear() { Slots.Clear(); } + + private static object GuardNotFound(string slotName) + { + if (!Slots.TryGetValue(slotName, out var slot)) + { + throw new ArgumentException($"Context slot not found: '{slotName}'"); + } + + return slot; + } } } diff --git a/src/OpenTelemetry.Api/Context/ThreadLocalRuntimeContextSlot.cs b/src/OpenTelemetry.Api/Context/ThreadLocalRuntimeContextSlot.cs index efd06d31dae..7da2c606ca9 100644 --- a/src/OpenTelemetry.Api/Context/ThreadLocalRuntimeContextSlot.cs +++ b/src/OpenTelemetry.Api/Context/ThreadLocalRuntimeContextSlot.cs @@ -23,10 +23,10 @@ namespace OpenTelemetry.Context /// The thread local (TLS) implementation of context slot. /// /// The type of the underlying value. - public class ThreadLocalRuntimeContextSlot : RuntimeContextSlot + public class ThreadLocalRuntimeContextSlot : RuntimeContextSlot, IRuntimeContextSlotValueAccessor { private readonly ThreadLocal slot; - private bool disposedValue; + private bool disposed; /// /// Initializes a new instance of the class. @@ -38,6 +38,13 @@ public ThreadLocalRuntimeContextSlot(string name) this.slot = new ThreadLocal(); } + /// + public object Value + { + get => this.slot.Value; + set => this.slot.Value = (T)value; + } + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public override T Get() @@ -55,16 +62,17 @@ public override void Set(T value) /// protected override void Dispose(bool disposing) { - base.Dispose(true); - if (!this.disposedValue) + if (!this.disposed) { if (disposing) { this.slot.Dispose(); } - this.disposedValue = true; + this.disposed = true; } + + base.Dispose(disposing); } } } diff --git a/src/OpenTelemetry.Api/Internal/EnumerationHelper.cs b/src/OpenTelemetry.Api/Internal/EnumerationHelper.cs index 0c9abc43c95..319d87f2d26 100644 --- a/src/OpenTelemetry.Api/Internal/EnumerationHelper.cs +++ b/src/OpenTelemetry.Api/Internal/EnumerationHelper.cs @@ -14,6 +14,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // + using System; using System.Collections; using System.Collections.Generic; diff --git a/src/OpenTelemetry.Api/Internal/Guard.cs b/src/OpenTelemetry.Api/Internal/Guard.cs new file mode 100644 index 00000000000..46d57c03aa5 --- /dev/null +++ b/src/OpenTelemetry.Api/Internal/Guard.cs @@ -0,0 +1,171 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Threading; + +namespace OpenTelemetry.Internal +{ + /// + /// Methods for guarding against exception throwing values. + /// + internal static class Guard + { + private const string DefaultParamName = "N/A"; + + /// + /// Throw an exception if the value is null. + /// + /// The value to check. + /// The parameter name to use in the thrown exception. + [DebuggerHidden] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ThrowIfNull(object value, string paramName = DefaultParamName) + { + if (value is null) + { + throw new ArgumentNullException(paramName, "Must not be null"); + } + } + + /// + /// Throw an exception if the value is null or empty. + /// + /// The value to check. + /// The parameter name to use in the thrown exception. + [DebuggerHidden] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ThrowIfNullOrEmpty(string value, string paramName = DefaultParamName) + { + if (string.IsNullOrEmpty(value)) + { + throw new ArgumentException("Must not be null or empty", paramName); + } + } + + /// + /// Throw an exception if the value is null or whitespace. + /// + /// The value to check. + /// The parameter name to use in the thrown exception. + [DebuggerHidden] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ThrowIfNullOrWhitespace(string value, string paramName = DefaultParamName) + { + if (string.IsNullOrWhiteSpace(value)) + { + throw new ArgumentException("Must not be null or whitespace", paramName); + } + } + + /// + /// Throw an exception if the value is zero. + /// + /// The value to check. + /// The message to use in the thrown exception. + /// 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", string paramName = DefaultParamName) + { + if (value == 0) + { + throw new ArgumentException(message, paramName); + } + } + + /// + /// Throw an exception if the value is not considered a valid timeout. + /// + /// The value to check. + /// The parameter name to use in the thrown exception. + [DebuggerHidden] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ThrowIfInvalidTimeout(int value, string paramName = DefaultParamName) + { + ThrowIfOutOfRange(value, paramName, min: Timeout.Infinite, message: $"Must be non-negative or '{nameof(Timeout)}.{nameof(Timeout.Infinite)}'"); + } + + /// + /// Throw an exception if the value is not within the given range. + /// + /// The value to check. + /// The parameter name to use in the thrown exception. + /// The inclusive lower bound. + /// The inclusive upper bound. + /// The name of the lower bound. + /// The name of the upper bound. + /// An optional custom message to use in the thrown exception. + [DebuggerHidden] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ThrowIfOutOfRange(int value, string paramName = DefaultParamName, 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); + } + + /// + /// Throw an exception if the value is not within the given range. + /// + /// The value to check. + /// The parameter name to use in the thrown exception. + /// The inclusive lower bound. + /// The inclusive upper bound. + /// The name of the lower bound. + /// The name of the upper bound. + /// An optional custom message to use in the thrown exception. + [DebuggerHidden] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ThrowIfOutOfRange(double value, string paramName = DefaultParamName, 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); + } + + /// + /// Throw an exception if the value is not of the expected type. + /// + /// The value to check. + /// The parameter name to use in the thrown exception. + /// The type attempted to convert to. + /// The value casted to the specified type. + [DebuggerHidden] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T ThrowIfNotOfType(object value, string paramName = DefaultParamName) + { + if (value is not T result) + { + throw new InvalidCastException($"Cannot cast '{paramName}' from '{value.GetType().Name}' to '{typeof(T).Name}'"); + } + + return result; + } + + [DebuggerHidden] + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Range(T value, string paramName, T min, T max, string minName, string maxName, string message) + where T : IComparable + { + if (value.CompareTo(min) < 0 || value.CompareTo(max) > 0) + { + var minMessage = minName != null ? $": {minName}" : string.Empty; + var maxMessage = maxName != null ? $": {maxName}" : string.Empty; + var exMessage = message ?? $"Must be in the range: [{min}{minMessage}, {max}{maxMessage}]"; + throw new ArgumentOutOfRangeException(paramName, value, exMessage); + } + } + } +} diff --git a/src/OpenTelemetry.Api/Internal/SemanticConventions.cs b/src/OpenTelemetry.Api/Internal/SemanticConventions.cs index 14889bf3b39..eeab30af2ad 100644 --- a/src/OpenTelemetry.Api/Internal/SemanticConventions.cs +++ b/src/OpenTelemetry.Api/Internal/SemanticConventions.cs @@ -25,7 +25,6 @@ internal static class SemanticConventions // The set of constants matches the specification as of this commit. // https://github.com/open-telemetry/opentelemetry-specification/tree/main/specification/trace/semantic_conventions // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/exceptions.md -#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member public const string AttributeNetTransport = "net.transport"; public const string AttributeNetPeerIp = "net.peer.ip"; public const string AttributeNetPeerPort = "net.peer.port"; @@ -49,8 +48,6 @@ internal static class SemanticConventions public const string AttributeHttpStatusText = "http.status_text"; public const string AttributeHttpFlavor = "http.flavor"; public const string AttributeHttpServerName = "http.server_name"; - public const string AttributeHttpHostName = "host.name"; - public const string AttributeHttpHostPort = "host.port"; public const string AttributeHttpRoute = "http.route"; public const string AttributeHttpClientIP = "http.client_ip"; public const string AttributeHttpUserAgent = "http.user_agent"; @@ -110,6 +107,5 @@ internal static class SemanticConventions public const string AttributeExceptionType = "exception.type"; public const string AttributeExceptionMessage = "exception.message"; public const string AttributeExceptionStacktrace = "exception.stacktrace"; -#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member } } diff --git a/src/OpenTelemetry.Api/Internal/SpanAttributeConstants.cs b/src/OpenTelemetry.Api/Internal/SpanAttributeConstants.cs index c7ca70c2e59..5640fd3decb 100644 --- a/src/OpenTelemetry.Api/Internal/SpanAttributeConstants.cs +++ b/src/OpenTelemetry.Api/Internal/SpanAttributeConstants.cs @@ -21,14 +21,8 @@ namespace OpenTelemetry.Trace /// internal static class SpanAttributeConstants { -#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member public const string StatusCodeKey = "otel.status_code"; public const string StatusDescriptionKey = "otel.status_description"; - - public const string HttpPathKey = "http.path"; - public const string DatabaseStatementTypeKey = "db.statement_type"; - -#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member } } diff --git a/src/OpenTelemetry.Api/Metrics/IDeferredMeterProviderBuilder.cs b/src/OpenTelemetry.Api/Metrics/IDeferredMeterProviderBuilder.cs new file mode 100644 index 00000000000..d0a98a05db8 --- /dev/null +++ b/src/OpenTelemetry.Api/Metrics/IDeferredMeterProviderBuilder.cs @@ -0,0 +1,36 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; + +namespace OpenTelemetry.Metrics +{ + /// + /// Describes a meter provider builder that supports deferred initialization + /// using an to perform dependency injection. + /// + public interface IDeferredMeterProviderBuilder + { + /// + /// Register a callback action to configure the once the application is available. + /// + /// Configuration callback. + /// The supplied for chaining. + MeterProviderBuilder Configure(Action configure); + } +} diff --git a/src/OpenTelemetry.Api/Metrics/MeterProviderBuilder.cs b/src/OpenTelemetry.Api/Metrics/MeterProviderBuilder.cs index f5bb6dc9ff2..4fa833ff3c9 100644 --- a/src/OpenTelemetry.Api/Metrics/MeterProviderBuilder.cs +++ b/src/OpenTelemetry.Api/Metrics/MeterProviderBuilder.cs @@ -18,7 +18,7 @@ namespace OpenTelemetry.Metrics { /// - /// TracerProviderBuilder base class. + /// MeterProviderBuilder base class. /// public abstract class MeterProviderBuilder { @@ -40,10 +40,10 @@ public abstract MeterProviderBuilder AddInstrumentation( where TInstrumentation : class; /// - /// Adds given Meter names to the list of subscribed sources. + /// Adds given Meter names to the list of subscribed meters. /// - /// Meter source names. + /// Meter names. /// Returns for chaining. - public abstract MeterProviderBuilder AddSource(params string[] names); + public abstract MeterProviderBuilder AddMeter(params string[] names); } } diff --git a/src/OpenTelemetry.Api/OpenTelemetry.Api.csproj b/src/OpenTelemetry.Api/OpenTelemetry.Api.csproj index d5d3e19631f..9bfe70f1994 100644 --- a/src/OpenTelemetry.Api/OpenTelemetry.Api.csproj +++ b/src/OpenTelemetry.Api/OpenTelemetry.Api.csproj @@ -8,7 +8,7 @@ core- - false @@ -18,5 +18,4 @@ - diff --git a/src/OpenTelemetry.Api/README.md b/src/OpenTelemetry.Api/README.md index 5d53ae4cb46..4396cbbab31 100644 --- a/src/OpenTelemetry.Api/README.md +++ b/src/OpenTelemetry.Api/README.md @@ -69,11 +69,6 @@ allows users to capture measurements about the execution of a computer program at runtime. The Metrics API is designed to process raw measurements, generally with the intent to produce continuous summaries of those measurements. -_**Warning:** OpenTelemetry .NET has a prototype Metrics API implementation only -and is not recommended for any production use. The API is expected to change -heavily. Please check the [Metric support -plan](https://github.com/open-telemetry/opentelemetry-dotnet/issues/1501)._ - ### Baggage API [Baggage @@ -84,27 +79,26 @@ propagated out of proc using OpenTelemetry SDK ships a BaggagePropagator and enables it by default. ```csharp -// Use Baggage.Current to get all the key/value pairs present in Baggage -foreach (var item in Baggage.Current) +// Use GetBaggage to get all the key/value pairs present in Baggage +foreach (var item in Baggage.GetBaggage()) { Console.WriteLine(item.Key); Console.WriteLine(item.Value); } // Use SetBaggage method to add a key/value pair in Baggage -Baggage.Current.SetBaggage("AppName", "MyApp"); -Baggage.Current.SetBaggage("Region", "West US"); +Baggage.SetBaggage("AppName", "MyApp"); +Baggage.SetBaggage("Region", "West US"); // Use RemoveBaggage method to remove a key/value pair in Baggage -Baggage.Current.RemoveBaggage("AppName"); +Baggage.RemoveBaggage("AppName"); // Use ClearBaggage method to remove all the key/value pairs in Baggage -Baggage.Current.ClearBaggage(); +Baggage.ClearBaggage(); ``` -The recommended way to add Baggage is to use `SetBaggage()` on -`Baggage.Current`. OpenTelemetry users should not use the method `AddBaggage` on -`Activity`. +The recommended way to add Baggage is to use the `Baggage.SetBaggage()` API. +OpenTelemetry users should not use the `Activity.AddBaggage` method. ## Introduction to OpenTelemetry .NET Tracing API @@ -440,6 +434,74 @@ and [extract](../../examples/MicroserviceExample/Utils/Messaging/MessageReceiver.cs) context. +## Introduction to OpenTelemetry .NET Metrics API + +Metrics in OpenTelemetry .NET are a somewhat unique implementation of the +OpenTelemetry project, as the Metrics API is incorporated directly into the .NET +runtime itself, as part of the +[`System.Diagnostics.DiagnosticSource`](https://www.nuget.org/packages/System.Diagnostics.DiagnosticSource/6.0.0) +package. This means, users can instrument their applications/libraries to emit +metrics by simply using the `System.Diagnostics.DiagnosticSource` package. This +package can be used in applications targeting any of the officially supported +versions of [.NET Core](https://dotnet.microsoft.com/download/dotnet-core), and +[.NET Framework](https://dotnet.microsoft.com/download/dotnet-framework) except +for versions lower than `.NET Framework 4.6.1`. + +## Instrumenting a library/application with .NET Metrics API + +### Basic metric usage + +1. Install the `System.Diagnostics.DiagnosticSource` package version + `6.0.0` or above to your application or library. + + + ```xml + + + + ``` + + +2. Create a `Meter`, providing the name and version of the library/application + doing the instrumentation. The `Meter` instance is typically created once and + is reused throughout the application/library. + + ```csharp + static Meter meter = new Meter( + "companyname.product.instrumentationlibrary", + "semver1.0.0"); + ``` + + The above requires import of the `System.Diagnostics.Metrics` namespace. + + **Note:** + It is important to note that `Meter` instances are created by using its + constructor, and *not* by calling a `GetMeter` method on the + `MeterProvider`. This is an important distinction from the [OpenTelemetry + specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#get-a-meter), + where `Meter`s are obtained from `MeterProvider`. + +3. Use the `Meter` instance from above to create instruments, which can be used + to report measurements. Just like meter instances, the instrument instances + are to be created once and reused throughout the application/library. + + ```csharp + static Counter MyFruitCounter = meter.CreateCounter("MyFruitCounter"); + ``` + +4. Use the instruments to report measurements, along with the attributes. + + ```csharp + MyFruitCounter.Add(1, new("name", "apple"), new("color", "red")); + ``` + +The above showed the usage of a `Counter` instrument. The following sections +describes more kinds of instruments. + +### Instrument types + +// TODO - add all instruments. + ## References * [OpenTelemetry Project](https://opentelemetry.io/) diff --git a/src/OpenTelemetry.Api/Trace/SpanAttributes.cs b/src/OpenTelemetry.Api/Trace/SpanAttributes.cs index 3722651e9e2..f247d291cea 100644 --- a/src/OpenTelemetry.Api/Trace/SpanAttributes.cs +++ b/src/OpenTelemetry.Api/Trace/SpanAttributes.cs @@ -14,9 +14,9 @@ // limitations under the License. // -using System; using System.Collections.Generic; using System.Diagnostics; +using OpenTelemetry.Internal; namespace OpenTelemetry.Trace { @@ -41,10 +41,7 @@ public SpanAttributes() public SpanAttributes(IEnumerable> attributes) : this() { - if (attributes == null) - { - throw new ArgumentNullException(nameof(attributes)); - } + Guard.ThrowIfNull(attributes, nameof(attributes)); foreach (KeyValuePair kvp in attributes) { @@ -136,10 +133,7 @@ public void Add(string key, double[] values) private void AddInternal(string key, object value) { - if (key == null) - { - throw new ArgumentNullException(key); - } + Guard.ThrowIfNull(key, nameof(key)); this.Attributes[key] = value; } diff --git a/src/OpenTelemetry.Api/Trace/SpanContext.cs b/src/OpenTelemetry.Api/Trace/SpanContext.cs index 8ce88246b89..d22742ac950 100644 --- a/src/OpenTelemetry.Api/Trace/SpanContext.cs +++ b/src/OpenTelemetry.Api/Trace/SpanContext.cs @@ -21,7 +21,7 @@ namespace OpenTelemetry.Trace { /// - /// A class that represents a span context. A span context contains the portion of a span + /// A struct that represents a span context. A span context contains the portion of a span /// that must propagate to child and across process boundaries. /// It contains the identifiers and /// associated with the along with a set of diff --git a/src/OpenTelemetry.Api/Trace/TelemetrySpan.cs b/src/OpenTelemetry.Api/Trace/TelemetrySpan.cs index 2d74f1487a7..e7f439a0592 100644 --- a/src/OpenTelemetry.Api/Trace/TelemetrySpan.cs +++ b/src/OpenTelemetry.Api/Trace/TelemetrySpan.cs @@ -64,6 +64,24 @@ public bool IsRecording } } + /// + /// Gets the identity of the parent span id, if any. + /// + public ActivitySpanId ParentSpanId + { + get + { + if (this.Activity == null) + { + return default; + } + else + { + return this.Activity.ParentSpanId; + } + } + } + /// /// Sets the status of the span execution. /// diff --git a/src/OpenTelemetry.Api/Trace/TracerProviderBuilder.cs b/src/OpenTelemetry.Api/Trace/TracerProviderBuilder.cs index e902a38722e..f63564ec9bd 100644 --- a/src/OpenTelemetry.Api/Trace/TracerProviderBuilder.cs +++ b/src/OpenTelemetry.Api/Trace/TracerProviderBuilder.cs @@ -51,7 +51,7 @@ public abstract TracerProviderBuilder AddInstrumentation( /// Adds a listener for objects created with the given operation name to the . /// /// - /// This is provided to capture legacy objects created without using the API. + /// This is provided to capture legacy objects created without using the API. /// /// Operation name of the objects to capture. /// Returns for chaining. diff --git a/src/OpenTelemetry.Exporter.Console/.publicApi/net452/PublicAPI.Shipped.txt b/src/OpenTelemetry.Exporter.Console/.publicApi/net452/PublicAPI.Shipped.txt deleted file mode 100644 index 6b050c7cd5a..00000000000 --- a/src/OpenTelemetry.Exporter.Console/.publicApi/net452/PublicAPI.Shipped.txt +++ /dev/null @@ -1,15 +0,0 @@ -OpenTelemetry.Exporter.ConsoleActivityExporter -OpenTelemetry.Exporter.ConsoleActivityExporter.ConsoleActivityExporter(OpenTelemetry.Exporter.ConsoleExporterOptions options) -> void -OpenTelemetry.Exporter.ConsoleExporter -OpenTelemetry.Exporter.ConsoleExporter.ConsoleExporter(OpenTelemetry.Exporter.ConsoleExporterOptions options) -> void -OpenTelemetry.Exporter.ConsoleExporter.WriteLine(string message) -> void -OpenTelemetry.Exporter.ConsoleExporterOptions -OpenTelemetry.Exporter.ConsoleExporterOptions.ConsoleExporterOptions() -> void -OpenTelemetry.Exporter.ConsoleExporterOptions.Targets.get -> OpenTelemetry.Exporter.ConsoleExporterOutputTargets -OpenTelemetry.Exporter.ConsoleExporterOptions.Targets.set -> void -OpenTelemetry.Exporter.ConsoleExporterOutputTargets -OpenTelemetry.Exporter.ConsoleExporterOutputTargets.Console = 1 -> OpenTelemetry.Exporter.ConsoleExporterOutputTargets -OpenTelemetry.Exporter.ConsoleExporterOutputTargets.Debug = 2 -> OpenTelemetry.Exporter.ConsoleExporterOutputTargets -OpenTelemetry.Trace.ConsoleExporterHelperExtensions -override OpenTelemetry.Exporter.ConsoleActivityExporter.Export(in OpenTelemetry.Batch batch) -> OpenTelemetry.ExportResult -static OpenTelemetry.Trace.ConsoleExporterHelperExtensions.AddConsoleExporter(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action configure = null) -> OpenTelemetry.Trace.TracerProviderBuilder diff --git a/src/OpenTelemetry.Exporter.Console/.publicApi/net461/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.Console/.publicApi/net461/PublicAPI.Unshipped.txt index e69de29bb2d..1e1b5bf2eb4 100644 --- a/src/OpenTelemetry.Exporter.Console/.publicApi/net461/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Exporter.Console/.publicApi/net461/PublicAPI.Unshipped.txt @@ -0,0 +1,11 @@ +OpenTelemetry.Exporter.ConsoleExporterOptions.AggregationTemporality.get -> OpenTelemetry.Metrics.AggregationTemporality +OpenTelemetry.Exporter.ConsoleExporterOptions.AggregationTemporality.set -> void +OpenTelemetry.Exporter.ConsoleExporterOptions.MetricReaderType.get -> OpenTelemetry.Metrics.MetricReaderType +OpenTelemetry.Exporter.ConsoleExporterOptions.MetricReaderType.set -> void +OpenTelemetry.Exporter.ConsoleExporterOptions.PeriodicExportingMetricReaderOptions.get -> OpenTelemetry.Metrics.PeriodicExportingMetricReaderOptions +OpenTelemetry.Exporter.ConsoleExporterOptions.PeriodicExportingMetricReaderOptions.set -> void +OpenTelemetry.Exporter.ConsoleMetricExporter +OpenTelemetry.Exporter.ConsoleMetricExporter.ConsoleMetricExporter(OpenTelemetry.Exporter.ConsoleExporterOptions options) -> void +OpenTelemetry.Metrics.ConsoleExporterMetricsExtensions +override OpenTelemetry.Exporter.ConsoleMetricExporter.Export(in OpenTelemetry.Batch batch) -> OpenTelemetry.ExportResult +static OpenTelemetry.Metrics.ConsoleExporterMetricsExtensions.AddConsoleExporter(this OpenTelemetry.Metrics.MeterProviderBuilder builder, System.Action configure = null) -> OpenTelemetry.Metrics.MeterProviderBuilder \ No newline at end of file diff --git a/src/OpenTelemetry.Exporter.Console/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.Console/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt index e69de29bb2d..1e1b5bf2eb4 100644 --- a/src/OpenTelemetry.Exporter.Console/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Exporter.Console/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt @@ -0,0 +1,11 @@ +OpenTelemetry.Exporter.ConsoleExporterOptions.AggregationTemporality.get -> OpenTelemetry.Metrics.AggregationTemporality +OpenTelemetry.Exporter.ConsoleExporterOptions.AggregationTemporality.set -> void +OpenTelemetry.Exporter.ConsoleExporterOptions.MetricReaderType.get -> OpenTelemetry.Metrics.MetricReaderType +OpenTelemetry.Exporter.ConsoleExporterOptions.MetricReaderType.set -> void +OpenTelemetry.Exporter.ConsoleExporterOptions.PeriodicExportingMetricReaderOptions.get -> OpenTelemetry.Metrics.PeriodicExportingMetricReaderOptions +OpenTelemetry.Exporter.ConsoleExporterOptions.PeriodicExportingMetricReaderOptions.set -> void +OpenTelemetry.Exporter.ConsoleMetricExporter +OpenTelemetry.Exporter.ConsoleMetricExporter.ConsoleMetricExporter(OpenTelemetry.Exporter.ConsoleExporterOptions options) -> void +OpenTelemetry.Metrics.ConsoleExporterMetricsExtensions +override OpenTelemetry.Exporter.ConsoleMetricExporter.Export(in OpenTelemetry.Batch batch) -> OpenTelemetry.ExportResult +static OpenTelemetry.Metrics.ConsoleExporterMetricsExtensions.AddConsoleExporter(this OpenTelemetry.Metrics.MeterProviderBuilder builder, System.Action configure = null) -> OpenTelemetry.Metrics.MeterProviderBuilder \ No newline at end of file diff --git a/src/OpenTelemetry.Exporter.Console/CHANGELOG.md b/src/OpenTelemetry.Exporter.Console/CHANGELOG.md index 8de5ce58c91..343846afe6d 100644 --- a/src/OpenTelemetry.Exporter.Console/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Console/CHANGELOG.md @@ -2,6 +2,39 @@ ## Unreleased +Fix MetricExporter to respect Console and Debug flags. + +## 1.2.0-rc1 + +Released 2021-Nov-29 + +* Added configuration options for `MetricReaderType` to allow for configuring + the `ConsoleMetricExporter` to export either manually or periodically. + ([#2648](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2648)) + +## 1.2.0-beta2 + +Released 2021-Nov-19 + +## 1.2.0-beta1 + +Released 2021-Oct-08 + +## 1.2.0-alpha4 + +Released 2021-Sep-23 + +## 1.2.0-alpha3 + +Released 2021-Sep-13 + +## 1.2.0-alpha2 + +Released 2021-Aug-24 + +* Add Histogram Metrics support. +* Changed default temporality to be cumulative. + ## 1.2.0-alpha1 Released 2021-Jul-23 diff --git a/src/OpenTelemetry.Exporter.Console/ConsoleExporterHelperExtensions.cs b/src/OpenTelemetry.Exporter.Console/ConsoleExporterHelperExtensions.cs index a73e9a4e76d..4a0c8a6017d 100644 --- a/src/OpenTelemetry.Exporter.Console/ConsoleExporterHelperExtensions.cs +++ b/src/OpenTelemetry.Exporter.Console/ConsoleExporterHelperExtensions.cs @@ -16,6 +16,7 @@ using System; using OpenTelemetry.Exporter; +using OpenTelemetry.Internal; namespace OpenTelemetry.Trace { @@ -30,10 +31,7 @@ public static class ConsoleExporterHelperExtensions [System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "The objects should not be disposed.")] public static TracerProviderBuilder AddConsoleExporter(this TracerProviderBuilder builder, Action configure = null) { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } + Guard.ThrowIfNull(builder, nameof(builder)); if (builder is IDeferredTracerProviderBuilder deferredTracerProviderBuilder) { diff --git a/src/OpenTelemetry.Exporter.Console/ConsoleExporterLoggingExtensions.cs b/src/OpenTelemetry.Exporter.Console/ConsoleExporterLoggingExtensions.cs index acd1b29430b..1c9a2d9bce5 100644 --- a/src/OpenTelemetry.Exporter.Console/ConsoleExporterLoggingExtensions.cs +++ b/src/OpenTelemetry.Exporter.Console/ConsoleExporterLoggingExtensions.cs @@ -16,6 +16,7 @@ using System; using OpenTelemetry.Exporter; +using OpenTelemetry.Internal; namespace OpenTelemetry.Logs { @@ -29,10 +30,7 @@ public static class ConsoleExporterLoggingExtensions /// The instance of to chain the calls. public static OpenTelemetryLoggerOptions AddConsoleExporter(this OpenTelemetryLoggerOptions loggerOptions, Action configure = null) { - if (loggerOptions == null) - { - throw new ArgumentNullException(nameof(loggerOptions)); - } + Guard.ThrowIfNull(loggerOptions, nameof(loggerOptions)); var options = new ConsoleExporterOptions(); configure?.Invoke(options); diff --git a/src/OpenTelemetry.Exporter.Console/ConsoleExporterMetricHelperExtensions.cs b/src/OpenTelemetry.Exporter.Console/ConsoleExporterMetricsExtensions.cs similarity index 68% rename from src/OpenTelemetry.Exporter.Console/ConsoleExporterMetricHelperExtensions.cs rename to src/OpenTelemetry.Exporter.Console/ConsoleExporterMetricsExtensions.cs index 211add536cc..b07ed46366d 100644 --- a/src/OpenTelemetry.Exporter.Console/ConsoleExporterMetricHelperExtensions.cs +++ b/src/OpenTelemetry.Exporter.Console/ConsoleExporterMetricsExtensions.cs @@ -1,4 +1,4 @@ -// +// // Copyright The OpenTelemetry Authors // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,10 +16,11 @@ using System; using OpenTelemetry.Exporter; +using OpenTelemetry.Internal; namespace OpenTelemetry.Metrics { - public static class ConsoleExporterMetricHelperExtensions + public static class ConsoleExporterMetricsExtensions { /// /// Adds Console exporter to the TracerProvider. @@ -30,14 +31,20 @@ public static class ConsoleExporterMetricHelperExtensions [System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "The objects should not be disposed.")] public static MeterProviderBuilder AddConsoleExporter(this MeterProviderBuilder builder, Action configure = null) { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } + Guard.ThrowIfNull(builder, nameof(builder)); var options = new ConsoleExporterOptions(); configure?.Invoke(options); - return builder.AddMetricProcessor(new PushMetricProcessor(new ConsoleMetricExporter(options), options.MetricExportIntervalMilliseconds, options.IsDelta)); + + var exporter = new ConsoleMetricExporter(options); + + var reader = options.MetricReaderType == MetricReaderType.Manual + ? new BaseExportingMetricReader(exporter) + : new PeriodicExportingMetricReader(exporter, options.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds); + + reader.Temporality = options.AggregationTemporality; + + return builder.AddReader(reader); } } } diff --git a/src/OpenTelemetry.Exporter.Console/ConsoleExporterOptions.cs b/src/OpenTelemetry.Exporter.Console/ConsoleExporterOptions.cs index 777a11dd327..93c7e3422c5 100644 --- a/src/OpenTelemetry.Exporter.Console/ConsoleExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.Console/ConsoleExporterOptions.cs @@ -14,6 +14,8 @@ // limitations under the License. // +using OpenTelemetry.Metrics; + namespace OpenTelemetry.Exporter { public class ConsoleExporterOptions @@ -24,14 +26,19 @@ public class ConsoleExporterOptions public ConsoleExporterOutputTargets Targets { get; set; } = ConsoleExporterOutputTargets.Console; /// - /// Gets or sets the metric export interval in milliseconds. The default value is 1000 milliseconds. + /// Gets or sets the to use. Defaults to MetricReaderType.Manual. + /// + public MetricReaderType MetricReaderType { get; set; } = MetricReaderType.Manual; + + /// + /// Gets or sets the options. Ignored unless MetricReaderType is Periodic. /// - public int MetricExportIntervalMilliseconds { get; set; } = 1000; + public PeriodicExportingMetricReaderOptions PeriodicExportingMetricReaderOptions { get; set; } = new PeriodicExportingMetricReaderOptions(); /// - /// Gets or sets a value indicating whether to export Delta - /// values or not (Cumulative). + /// Gets or sets the AggregationTemporality used for Histogram + /// and Sum metrics. /// - public bool IsDelta { get; set; } = true; + public AggregationTemporality AggregationTemporality { get; set; } = AggregationTemporality.Delta; } } diff --git a/src/OpenTelemetry.Exporter.Console/ConsoleLogRecordExporter.cs b/src/OpenTelemetry.Exporter.Console/ConsoleLogRecordExporter.cs index f21b695c33f..9203badddf1 100644 --- a/src/OpenTelemetry.Exporter.Console/ConsoleLogRecordExporter.cs +++ b/src/OpenTelemetry.Exporter.Console/ConsoleLogRecordExporter.cs @@ -36,7 +36,8 @@ public override ExportResult Export(in Batch batch) this.WriteLine($"{"LogRecord.TraceId:".PadRight(RightPaddingLength)}{logRecord.TraceId}"); this.WriteLine($"{"LogRecord.SpanId:".PadRight(RightPaddingLength)}{logRecord.SpanId}"); this.WriteLine($"{"LogRecord.Timestamp:".PadRight(RightPaddingLength)}{logRecord.Timestamp:yyyy-MM-ddTHH:mm:ss.fffffffZ}"); - this.WriteLine($"{"LogRecord.EventId:".PadRight(RightPaddingLength)}{logRecord.EventId}"); + this.WriteLine($"{"LogRecord.EventId:".PadRight(RightPaddingLength)}{logRecord.EventId.Id}"); + this.WriteLine($"{"LogRecord.EventName:".PadRight(RightPaddingLength)}{logRecord.EventId.Name}"); this.WriteLine($"{"LogRecord.CategoryName:".PadRight(RightPaddingLength)}{logRecord.CategoryName}"); this.WriteLine($"{"LogRecord.LogLevel:".PadRight(RightPaddingLength)}{logRecord.LogLevel}"); this.WriteLine($"{"LogRecord.TraceFlags:".PadRight(RightPaddingLength)}{logRecord.TraceFlags}"); diff --git a/src/OpenTelemetry.Exporter.Console/ConsoleMetricExporter.cs b/src/OpenTelemetry.Exporter.Console/ConsoleMetricExporter.cs index f07e7480fcd..cd94194913f 100644 --- a/src/OpenTelemetry.Exporter.Console/ConsoleMetricExporter.cs +++ b/src/OpenTelemetry.Exporter.Console/ConsoleMetricExporter.cs @@ -14,16 +14,14 @@ // limitations under the License. // -using System; using System.Globalization; -using System.Linq; using System.Text; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; namespace OpenTelemetry.Exporter { - public class ConsoleMetricExporter : ConsoleExporter + public class ConsoleMetricExporter : ConsoleExporter { private Resource resource; @@ -32,7 +30,7 @@ public ConsoleMetricExporter(ConsoleExporterOptions options) { } - public override ExportResult Export(in Batch batch) + public override ExportResult Export(in Batch batch) { if (this.resource == null) { @@ -43,98 +41,139 @@ public override ExportResult Export(in Batch batch) { if (resourceAttribute.Key.Equals("service.name")) { - Console.WriteLine("Service.Name" + resourceAttribute.Value); + this.WriteLine("Service.Name" + resourceAttribute.Value); } } } } - foreach (var metricItem in batch) + foreach (var metric in batch) { - foreach (var metric in metricItem.Metrics) + var msg = new StringBuilder($"\nExport "); + msg.Append(metric.Name); + if (!string.IsNullOrEmpty(metric.Description)) { - var tags = metric.Attributes.ToArray().Select(k => $"{k.Key}={k.Value?.ToString()}"); + msg.Append(", "); + msg.Append(metric.Description); + } - string valueDisplay = string.Empty; + if (!string.IsNullOrEmpty(metric.Unit)) + { + msg.Append($", Unit: {metric.Unit}"); + } + + if (!string.IsNullOrEmpty(metric.Meter.Name)) + { + msg.Append($", Meter: {metric.Meter.Name}"); - // Switch would be faster than the if.else ladder - // of try and cast. - switch (metric.MetricType) + if (!string.IsNullOrEmpty(metric.Meter.Version)) { - case MetricType.LongSum: - { - valueDisplay = (metric as ISumMetricLong).LongSum.ToString(CultureInfo.InvariantCulture); - break; - } + msg.Append($"/{metric.Meter.Version}"); + } + } - case MetricType.DoubleSum: - { - valueDisplay = (metric as ISumMetricDouble).DoubleSum.ToString(CultureInfo.InvariantCulture); - break; - } + this.WriteLine(msg.ToString()); - case MetricType.LongGauge: - { - // TODOs - break; - } + foreach (ref readonly var metricPoint in metric.GetMetricPoints()) + { + string valueDisplay = string.Empty; + StringBuilder tagsBuilder = new StringBuilder(); + foreach (var tag in metricPoint.Tags) + { + tagsBuilder.Append(tag.Key); + tagsBuilder.Append(':'); + tagsBuilder.Append(tag.Value); + tagsBuilder.Append(' '); + } + + var tags = tagsBuilder.ToString().TrimEnd(); + + var metricType = metric.MetricType; - case MetricType.DoubleGauge: + if (metricType.IsHistogram()) + { + var bucketsBuilder = new StringBuilder(); + var sum = metricPoint.GetHistogramSum(); + var count = metricPoint.GetHistogramCount(); + bucketsBuilder.Append($"Sum: {sum} Count: {count} \n"); + + bool isFirstIteration = true; + double previousExplicitBound = default; + foreach (var histogramMeasurement in metricPoint.GetHistogramBuckets()) + { + if (isFirstIteration) { - // TODOs - break; + bucketsBuilder.Append("(-Infinity,"); + bucketsBuilder.Append(histogramMeasurement.ExplicitBound); + bucketsBuilder.Append(']'); + bucketsBuilder.Append(':'); + bucketsBuilder.Append(histogramMeasurement.BucketCount); + previousExplicitBound = histogramMeasurement.ExplicitBound; + isFirstIteration = false; } - - case MetricType.Histogram: + else { - var histogramMetric = metric as IHistogramMetric; - var bucketsBuilder = new StringBuilder(); - bucketsBuilder.Append($"Sum: {histogramMetric.PopulationSum} Count: {histogramMetric.PopulationCount} \n"); - foreach (var bucket in histogramMetric.Buckets) + bucketsBuilder.Append('('); + bucketsBuilder.Append(previousExplicitBound); + bucketsBuilder.Append(','); + if (histogramMeasurement.ExplicitBound != double.PositiveInfinity) + { + bucketsBuilder.Append(histogramMeasurement.ExplicitBound); + } + else { - bucketsBuilder.Append($"({bucket.LowBoundary} - {bucket.HighBoundary}) : {bucket.Count}"); - bucketsBuilder.AppendLine(); + bucketsBuilder.Append("+Infinity"); } - valueDisplay = bucketsBuilder.ToString(); - break; + bucketsBuilder.Append(']'); + bucketsBuilder.Append(':'); + bucketsBuilder.Append(histogramMeasurement.BucketCount); } - case MetricType.Summary: - { - var summaryMetric = metric as ISummaryMetric; - valueDisplay = string.Format("Sum: {0} Count: {1}", summaryMetric.PopulationSum, summaryMetric.PopulationCount); - break; - } - } - - string time = $"{metric.StartTimeExclusive.ToLocalTime().ToString("HH:mm:ss.fff")} {metric.EndTimeInclusive.ToLocalTime().ToString("HH:mm:ss.fff")}"; - - var msg = new StringBuilder($"Export {time} {metric.Name} [{string.Join(";", tags)}] {metric.MetricType}"); + bucketsBuilder.AppendLine(); + } - if (!string.IsNullOrEmpty(metric.Description)) - { - msg.Append($", Description: {metric.Description}"); + valueDisplay = bucketsBuilder.ToString(); } - - if (!string.IsNullOrEmpty(metric.Unit)) + else if (metricType.IsDouble()) { - msg.Append($", Unit: {metric.Unit}"); + if (metricType.IsSum()) + { + valueDisplay = metricPoint.GetSumDouble().ToString(CultureInfo.InvariantCulture); + } + else + { + valueDisplay = metricPoint.GetGaugeLastValueDouble().ToString(CultureInfo.InvariantCulture); + } } - - if (!string.IsNullOrEmpty(metric.Meter.Name)) + else if (metricType.IsLong()) { - msg.Append($", Meter: {metric.Meter.Name}"); - - if (!string.IsNullOrEmpty(metric.Meter.Version)) + if (metricType.IsSum()) { - msg.Append($"/{metric.Meter.Version}"); + valueDisplay = metricPoint.GetSumLong().ToString(CultureInfo.InvariantCulture); } + else + { + valueDisplay = metricPoint.GetGaugeLastValueLong().ToString(CultureInfo.InvariantCulture); + } + } + + msg = new StringBuilder(); + msg.Append('('); + msg.Append(metricPoint.StartTime.ToString("yyyy-MM-ddTHH:mm:ss.fffffffZ", CultureInfo.InvariantCulture)); + msg.Append(", "); + msg.Append(metricPoint.EndTime.ToString("yyyy-MM-ddTHH:mm:ss.fffffffZ", CultureInfo.InvariantCulture)); + msg.Append("] "); + msg.Append(tags); + if (tags != string.Empty) + { + msg.Append(' '); } + msg.Append(metric.MetricType); msg.AppendLine(); msg.Append($"Value: {valueDisplay}"); - Console.WriteLine(msg); + this.WriteLine(msg.ToString()); } } diff --git a/src/OpenTelemetry.Exporter.Console/OpenTelemetry.Exporter.Console.csproj b/src/OpenTelemetry.Exporter.Console/OpenTelemetry.Exporter.Console.csproj index 1336a810fa8..aa23bb337a1 100644 --- a/src/OpenTelemetry.Exporter.Console/OpenTelemetry.Exporter.Console.csproj +++ b/src/OpenTelemetry.Exporter.Console/OpenTelemetry.Exporter.Console.csproj @@ -17,6 +17,7 @@ + diff --git a/src/OpenTelemetry.Exporter.Console/README.md b/src/OpenTelemetry.Exporter.Console/README.md index dccf623de29..309f7718d81 100644 --- a/src/OpenTelemetry.Exporter.Console/README.md +++ b/src/OpenTelemetry.Exporter.Console/README.md @@ -4,7 +4,7 @@ [![NuGet](https://img.shields.io/nuget/dt/OpenTelemetry.Exporter.Console.svg)](https://www.nuget.org/packages/OpenTelemetry.Exporter.Console) The console exporter prints data to the Console window. -ConsoleExporter supports exporting both traces and logs. +ConsoleExporter supports exporting logs, metrics and traces. **Note:** this exporter is intended to be used during learning how telemetry data are created and exported. It is not recommended for any production @@ -16,12 +16,12 @@ environment. dotnet add package OpenTelemetry.Exporter.Console ``` -See the -[`TestConsoleExporter.cs`](../../examples/Console/TestConsoleExporter.cs) for an -example of how to use the exporter for exporting traces. +See the individual "getting started" examples depending on the signal being +used: -See the [Program](../../docs/logs/getting-started/Program.cs) for -an example of how to use the exporter for exporting logs. +* [Logs](../../docs/logs/getting-started/Program.cs) +* [Metrics](../../docs/metrics/getting-started/Program.cs) +* [Traces](../../docs/trace/getting-started/Program.cs) ## References diff --git a/src/OpenTelemetry.Exporter.InMemory/.publicApi/net452/PublicAPI.Shipped.txt b/src/OpenTelemetry.Exporter.InMemory/.publicApi/net452/PublicAPI.Shipped.txt deleted file mode 100644 index 88e363cc43a..00000000000 --- a/src/OpenTelemetry.Exporter.InMemory/.publicApi/net452/PublicAPI.Shipped.txt +++ /dev/null @@ -1,5 +0,0 @@ -OpenTelemetry.Exporter.InMemoryExporter -OpenTelemetry.Exporter.InMemoryExporter.InMemoryExporter(System.Collections.Generic.ICollection exportedItems) -> void -OpenTelemetry.Trace.InMemoryExporterHelperExtensions -override OpenTelemetry.Exporter.InMemoryExporter.Export(in OpenTelemetry.Batch batch) -> OpenTelemetry.ExportResult -static OpenTelemetry.Trace.InMemoryExporterHelperExtensions.AddInMemoryExporter(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Collections.Generic.ICollection exportedItems) -> OpenTelemetry.Trace.TracerProviderBuilder diff --git a/src/OpenTelemetry.Exporter.InMemory/.publicApi/net46/PublicAPI.Shipped.txt b/src/OpenTelemetry.Exporter.InMemory/.publicApi/net46/PublicAPI.Shipped.txt deleted file mode 100644 index 88e363cc43a..00000000000 --- a/src/OpenTelemetry.Exporter.InMemory/.publicApi/net46/PublicAPI.Shipped.txt +++ /dev/null @@ -1,5 +0,0 @@ -OpenTelemetry.Exporter.InMemoryExporter -OpenTelemetry.Exporter.InMemoryExporter.InMemoryExporter(System.Collections.Generic.ICollection exportedItems) -> void -OpenTelemetry.Trace.InMemoryExporterHelperExtensions -override OpenTelemetry.Exporter.InMemoryExporter.Export(in OpenTelemetry.Batch batch) -> OpenTelemetry.ExportResult -static OpenTelemetry.Trace.InMemoryExporterHelperExtensions.AddInMemoryExporter(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Collections.Generic.ICollection exportedItems) -> OpenTelemetry.Trace.TracerProviderBuilder diff --git a/src/OpenTelemetry.Exporter.InMemory/.publicApi/net461/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.InMemory/.publicApi/net461/PublicAPI.Unshipped.txt index e69de29bb2d..8e3bd117f68 100644 --- a/src/OpenTelemetry.Exporter.InMemory/.publicApi/net461/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Exporter.InMemory/.publicApi/net461/PublicAPI.Unshipped.txt @@ -0,0 +1,2 @@ +OpenTelemetry.Metrics.InMemoryExporterMetricsExtensions +static OpenTelemetry.Metrics.InMemoryExporterMetricsExtensions.AddInMemoryExporter(this OpenTelemetry.Metrics.MeterProviderBuilder builder, System.Collections.Generic.ICollection exportedItems) -> OpenTelemetry.Metrics.MeterProviderBuilder \ No newline at end of file diff --git a/src/OpenTelemetry.Exporter.InMemory/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.InMemory/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt index e69de29bb2d..8e3bd117f68 100644 --- a/src/OpenTelemetry.Exporter.InMemory/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Exporter.InMemory/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt @@ -0,0 +1,2 @@ +OpenTelemetry.Metrics.InMemoryExporterMetricsExtensions +static OpenTelemetry.Metrics.InMemoryExporterMetricsExtensions.AddInMemoryExporter(this OpenTelemetry.Metrics.MeterProviderBuilder builder, System.Collections.Generic.ICollection exportedItems) -> OpenTelemetry.Metrics.MeterProviderBuilder \ No newline at end of file diff --git a/src/OpenTelemetry.Exporter.InMemory/CHANGELOG.md b/src/OpenTelemetry.Exporter.InMemory/CHANGELOG.md index e9367ea4bd7..2087d30b552 100644 --- a/src/OpenTelemetry.Exporter.InMemory/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.InMemory/CHANGELOG.md @@ -2,6 +2,30 @@ ## Unreleased +## 1.2.0-rc1 + +Released 2021-Nov-29 + +## 1.2.0-beta2 + +Released 2021-Nov-19 + +## 1.2.0-beta1 + +Released 2021-Oct-08 + +## 1.2.0-alpha4 + +Released 2021-Sep-23 + +## 1.2.0-alpha3 + +Released 2021-Sep-13 + +## 1.2.0-alpha2 + +Released 2021-Aug-24 + * Add Metrics support.([#2192](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2192)) diff --git a/src/OpenTelemetry.Exporter.InMemory/InMemoryExporterHelperExtensions.cs b/src/OpenTelemetry.Exporter.InMemory/InMemoryExporterHelperExtensions.cs index 47babe1f435..3a49d915075 100644 --- a/src/OpenTelemetry.Exporter.InMemory/InMemoryExporterHelperExtensions.cs +++ b/src/OpenTelemetry.Exporter.InMemory/InMemoryExporterHelperExtensions.cs @@ -14,10 +14,10 @@ // limitations under the License. // -using System; using System.Collections.Generic; using System.Diagnostics; using OpenTelemetry.Exporter; +using OpenTelemetry.Internal; namespace OpenTelemetry.Trace { @@ -32,15 +32,8 @@ public static class InMemoryExporterHelperExtensions [System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "The objects should not be disposed.")] public static TracerProviderBuilder AddInMemoryExporter(this TracerProviderBuilder builder, ICollection exportedItems) { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } - - if (exportedItems == null) - { - throw new ArgumentNullException(nameof(exportedItems)); - } + Guard.ThrowIfNull(builder, nameof(builder)); + Guard.ThrowIfNull(exportedItems, nameof(exportedItems)); if (builder is IDeferredTracerProviderBuilder deferredTracerProviderBuilder) { diff --git a/src/OpenTelemetry.Exporter.InMemory/InMemoryExporterLoggingExtensions.cs b/src/OpenTelemetry.Exporter.InMemory/InMemoryExporterLoggingExtensions.cs index c500949d602..4a64390f303 100644 --- a/src/OpenTelemetry.Exporter.InMemory/InMemoryExporterLoggingExtensions.cs +++ b/src/OpenTelemetry.Exporter.InMemory/InMemoryExporterLoggingExtensions.cs @@ -14,9 +14,9 @@ // limitations under the License. // -using System; using System.Collections.Generic; using OpenTelemetry.Exporter; +using OpenTelemetry.Internal; namespace OpenTelemetry.Logs { @@ -24,15 +24,8 @@ public static class InMemoryExporterLoggingExtensions { public static OpenTelemetryLoggerOptions AddInMemoryExporter(this OpenTelemetryLoggerOptions loggerOptions, ICollection exportedItems) { - if (loggerOptions == null) - { - throw new ArgumentNullException(nameof(loggerOptions)); - } - - if (exportedItems == null) - { - throw new ArgumentNullException(nameof(exportedItems)); - } + Guard.ThrowIfNull(loggerOptions, nameof(loggerOptions)); + Guard.ThrowIfNull(exportedItems, nameof(exportedItems)); return loggerOptions.AddProcessor(new SimpleLogRecordExportProcessor(new InMemoryExporter(exportedItems))); } diff --git a/src/OpenTelemetry.Exporter.InMemory/InMemoryExporterMetricHelperExtensions.cs b/src/OpenTelemetry.Exporter.InMemory/InMemoryExporterMetricsExtensions.cs similarity index 52% rename from src/OpenTelemetry.Exporter.InMemory/InMemoryExporterMetricHelperExtensions.cs rename to src/OpenTelemetry.Exporter.InMemory/InMemoryExporterMetricsExtensions.cs index 8240113e77f..97d3a3eabf8 100644 --- a/src/OpenTelemetry.Exporter.InMemory/InMemoryExporterMetricHelperExtensions.cs +++ b/src/OpenTelemetry.Exporter.InMemory/InMemoryExporterMetricsExtensions.cs @@ -1,4 +1,4 @@ -// +// // Copyright The OpenTelemetry Authors // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,37 +14,26 @@ // limitations under the License. // -using System; using System.Collections.Generic; using OpenTelemetry.Exporter; +using OpenTelemetry.Internal; namespace OpenTelemetry.Metrics { - public static class InMemoryExporterMetricHelperExtensions + public static class InMemoryExporterMetricsExtensions { /// /// Adds InMemory exporter to the TracerProvider. /// /// builder to use. /// Collection which will be populated with the exported MetricItem. - /// Exporter configuration options. /// The instance of to chain the calls. - [System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "The objects should not be disposed.")] - public static MeterProviderBuilder AddInMemoryExporter(this MeterProviderBuilder builder, ICollection exportedItems, Action configure = null) + public static MeterProviderBuilder AddInMemoryExporter(this MeterProviderBuilder builder, ICollection exportedItems) { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } + Guard.ThrowIfNull(builder, nameof(builder)); + Guard.ThrowIfNull(exportedItems, nameof(exportedItems)); - if (exportedItems == null) - { - throw new ArgumentNullException(nameof(exportedItems)); - } - - var options = new InMemoryExporterOptions(); - configure?.Invoke(options); - return builder.AddMetricProcessor(new PushMetricProcessor(new InMemoryExporter(exportedItems), options.MetricExportIntervalMilliseconds, options.IsDelta)); + return builder.AddReader(new BaseExportingMetricReader(new InMemoryExporter(exportedItems))); } } } diff --git a/src/OpenTelemetry.Exporter.InMemory/OpenTelemetry.Exporter.InMemory.csproj b/src/OpenTelemetry.Exporter.InMemory/OpenTelemetry.Exporter.InMemory.csproj index 8eb494b7f2a..404dc1b16d2 100644 --- a/src/OpenTelemetry.Exporter.InMemory/OpenTelemetry.Exporter.InMemory.csproj +++ b/src/OpenTelemetry.Exporter.InMemory/OpenTelemetry.Exporter.InMemory.csproj @@ -15,4 +15,8 @@ + + + + diff --git a/src/OpenTelemetry.Exporter.Jaeger/.publicApi/net461/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.Jaeger/.publicApi/net461/PublicAPI.Unshipped.txt index e69de29bb2d..c4866a84476 100644 --- a/src/OpenTelemetry.Exporter.Jaeger/.publicApi/net461/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Exporter.Jaeger/.publicApi/net461/PublicAPI.Unshipped.txt @@ -0,0 +1,9 @@ +OpenTelemetry.Exporter.JaegerExporterOptions.Endpoint.get -> System.Uri +OpenTelemetry.Exporter.JaegerExporterOptions.Endpoint.set -> void +OpenTelemetry.Exporter.JaegerExporterOptions.HttpClientFactory.get -> System.Func +OpenTelemetry.Exporter.JaegerExporterOptions.HttpClientFactory.set -> void +OpenTelemetry.Exporter.JaegerExporterOptions.Protocol.get -> OpenTelemetry.Exporter.JaegerExportProtocol +OpenTelemetry.Exporter.JaegerExporterOptions.Protocol.set -> void +OpenTelemetry.Exporter.JaegerExportProtocol +OpenTelemetry.Exporter.JaegerExportProtocol.HttpBinaryThrift = 1 -> OpenTelemetry.Exporter.JaegerExportProtocol +OpenTelemetry.Exporter.JaegerExportProtocol.UdpCompactThrift = 0 -> OpenTelemetry.Exporter.JaegerExportProtocol \ No newline at end of file diff --git a/src/OpenTelemetry.Exporter.Jaeger/.publicApi/net5.0/PublicAPI.Shipped.txt b/src/OpenTelemetry.Exporter.Jaeger/.publicApi/net5.0/PublicAPI.Shipped.txt new file mode 100644 index 00000000000..5d512bee8da --- /dev/null +++ b/src/OpenTelemetry.Exporter.Jaeger/.publicApi/net5.0/PublicAPI.Shipped.txt @@ -0,0 +1,18 @@ +OpenTelemetry.Exporter.JaegerExporter +OpenTelemetry.Exporter.JaegerExporter.JaegerExporter(OpenTelemetry.Exporter.JaegerExporterOptions options) -> void +OpenTelemetry.Exporter.JaegerExporterOptions +OpenTelemetry.Exporter.JaegerExporterOptions.AgentHost.get -> string +OpenTelemetry.Exporter.JaegerExporterOptions.AgentHost.set -> void +OpenTelemetry.Exporter.JaegerExporterOptions.AgentPort.get -> int +OpenTelemetry.Exporter.JaegerExporterOptions.AgentPort.set -> void +OpenTelemetry.Exporter.JaegerExporterOptions.BatchExportProcessorOptions.get -> OpenTelemetry.BatchExportProcessorOptions +OpenTelemetry.Exporter.JaegerExporterOptions.BatchExportProcessorOptions.set -> void +OpenTelemetry.Exporter.JaegerExporterOptions.ExportProcessorType.get -> OpenTelemetry.ExportProcessorType +OpenTelemetry.Exporter.JaegerExporterOptions.ExportProcessorType.set -> void +OpenTelemetry.Exporter.JaegerExporterOptions.JaegerExporterOptions() -> void +OpenTelemetry.Exporter.JaegerExporterOptions.MaxPayloadSizeInBytes.get -> int? +OpenTelemetry.Exporter.JaegerExporterOptions.MaxPayloadSizeInBytes.set -> void +OpenTelemetry.Trace.JaegerExporterHelperExtensions +override OpenTelemetry.Exporter.JaegerExporter.Dispose(bool disposing) -> void +override OpenTelemetry.Exporter.JaegerExporter.Export(in OpenTelemetry.Batch activityBatch) -> OpenTelemetry.ExportResult +static OpenTelemetry.Trace.JaegerExporterHelperExtensions.AddJaegerExporter(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action configure = null) -> OpenTelemetry.Trace.TracerProviderBuilder diff --git a/src/OpenTelemetry.Exporter.Jaeger/.publicApi/net5.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.Jaeger/.publicApi/net5.0/PublicAPI.Unshipped.txt new file mode 100644 index 00000000000..c4866a84476 --- /dev/null +++ b/src/OpenTelemetry.Exporter.Jaeger/.publicApi/net5.0/PublicAPI.Unshipped.txt @@ -0,0 +1,9 @@ +OpenTelemetry.Exporter.JaegerExporterOptions.Endpoint.get -> System.Uri +OpenTelemetry.Exporter.JaegerExporterOptions.Endpoint.set -> void +OpenTelemetry.Exporter.JaegerExporterOptions.HttpClientFactory.get -> System.Func +OpenTelemetry.Exporter.JaegerExporterOptions.HttpClientFactory.set -> void +OpenTelemetry.Exporter.JaegerExporterOptions.Protocol.get -> OpenTelemetry.Exporter.JaegerExportProtocol +OpenTelemetry.Exporter.JaegerExporterOptions.Protocol.set -> void +OpenTelemetry.Exporter.JaegerExportProtocol +OpenTelemetry.Exporter.JaegerExportProtocol.HttpBinaryThrift = 1 -> OpenTelemetry.Exporter.JaegerExportProtocol +OpenTelemetry.Exporter.JaegerExportProtocol.UdpCompactThrift = 0 -> OpenTelemetry.Exporter.JaegerExportProtocol \ No newline at end of file diff --git a/src/OpenTelemetry.Exporter.Jaeger/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.Jaeger/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt index e69de29bb2d..c4866a84476 100644 --- a/src/OpenTelemetry.Exporter.Jaeger/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Exporter.Jaeger/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt @@ -0,0 +1,9 @@ +OpenTelemetry.Exporter.JaegerExporterOptions.Endpoint.get -> System.Uri +OpenTelemetry.Exporter.JaegerExporterOptions.Endpoint.set -> void +OpenTelemetry.Exporter.JaegerExporterOptions.HttpClientFactory.get -> System.Func +OpenTelemetry.Exporter.JaegerExporterOptions.HttpClientFactory.set -> void +OpenTelemetry.Exporter.JaegerExporterOptions.Protocol.get -> OpenTelemetry.Exporter.JaegerExportProtocol +OpenTelemetry.Exporter.JaegerExporterOptions.Protocol.set -> void +OpenTelemetry.Exporter.JaegerExportProtocol +OpenTelemetry.Exporter.JaegerExportProtocol.HttpBinaryThrift = 1 -> OpenTelemetry.Exporter.JaegerExportProtocol +OpenTelemetry.Exporter.JaegerExportProtocol.UdpCompactThrift = 0 -> OpenTelemetry.Exporter.JaegerExportProtocol \ No newline at end of file diff --git a/src/OpenTelemetry.Exporter.Jaeger/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.Jaeger/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt index e69de29bb2d..c4866a84476 100644 --- a/src/OpenTelemetry.Exporter.Jaeger/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Exporter.Jaeger/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt @@ -0,0 +1,9 @@ +OpenTelemetry.Exporter.JaegerExporterOptions.Endpoint.get -> System.Uri +OpenTelemetry.Exporter.JaegerExporterOptions.Endpoint.set -> void +OpenTelemetry.Exporter.JaegerExporterOptions.HttpClientFactory.get -> System.Func +OpenTelemetry.Exporter.JaegerExporterOptions.HttpClientFactory.set -> void +OpenTelemetry.Exporter.JaegerExporterOptions.Protocol.get -> OpenTelemetry.Exporter.JaegerExportProtocol +OpenTelemetry.Exporter.JaegerExporterOptions.Protocol.set -> void +OpenTelemetry.Exporter.JaegerExportProtocol +OpenTelemetry.Exporter.JaegerExportProtocol.HttpBinaryThrift = 1 -> OpenTelemetry.Exporter.JaegerExportProtocol +OpenTelemetry.Exporter.JaegerExportProtocol.UdpCompactThrift = 0 -> OpenTelemetry.Exporter.JaegerExportProtocol \ No newline at end of file diff --git a/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/TBinaryProtocol.cs b/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/TBinaryProtocol.cs new file mode 100644 index 00000000000..820f5ee51a5 --- /dev/null +++ b/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/TBinaryProtocol.cs @@ -0,0 +1,231 @@ +// (Turns off StyleCop analysis in this file.) + +// Licensed to the Apache Software Foundation(ASF) under one +// or more contributor license agreements.See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership.The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +using System; +using Thrift.Protocol.Entities; + +namespace Thrift.Protocol +{ + // ReSharper disable once InconsistentNaming + internal sealed class TBinaryProtocol : TProtocol + { + private const uint VersionMask = 0xffff0000; + private const uint Version1 = 0x80010000; + + private readonly bool StrictRead; + private readonly bool StrictWrite; + + // minimize memory allocations by means of an preallocated bytes buffer + // The value of 128 is arbitrarily chosen, the required minimum size must be sizeof(long) + private readonly byte[] PreAllocatedBuffer = new byte[128]; + + private static readonly TStruct AnonymousStruct = new TStruct(string.Empty); + private static readonly TField StopField = new TField() { Type = TType.Stop }; + + public TBinaryProtocol(int initialCapacity = 8192) + : this(false, true, initialCapacity) + { + } + + public TBinaryProtocol(bool strictRead, bool strictWrite, int initialCapacity = 8192) + : base(initialCapacity) + { + StrictRead = strictRead; + StrictWrite = strictWrite; + } + + public override void WriteMessageBegin(TMessage message) + { + if (StrictWrite) + { + var version = Version1 | (uint)message.Type; + WriteI32((int)version); + WriteString(message.Name); + WriteI32(message.SeqID); + } + else + { + WriteString(message.Name); + WriteByte((sbyte)message.Type); + WriteI32(message.SeqID); + } + } + + public override void WriteMessageBegin(TMessage message, out int seqIdPosition) + { + if (StrictWrite) + { + var version = Version1 | (uint)message.Type; + WriteI32((int)version); + WriteString(message.Name); + seqIdPosition = (int)Transport.Position; + WriteI32(message.SeqID); + } + else + { + WriteString(message.Name); + WriteByte((sbyte)message.Type); + seqIdPosition = (int)Transport.Position; + WriteI32(message.SeqID); + } + } + + public override void WriteMessageEnd() + { + } + + public override void WriteStructBegin(TStruct @struct) + { + } + + public override void WriteStructEnd() + { + } + + public override void WriteFieldBegin(TField field) + { + WriteByte((sbyte)field.Type); + WriteI16(field.ID); + } + + public override void WriteFieldEnd() + { + } + + public override void WriteFieldStop() + { + WriteByte((sbyte)TType.Stop); + } + + public override void WriteListBegin(TList list) + { + WriteByte((sbyte)list.ElementType); + WriteI32(list.Count); + } + + public override void WriteListBegin(TList list, out int countPosition) + { + WriteByte((sbyte)list.ElementType); + countPosition = (int)Transport.Position; + WriteI32(list.Count); + } + + public override void WriteListEnd() + { + } + + public override void WriteBool(bool b) + { + WriteByte(b ? (sbyte)1 : (sbyte)0); + } + + public override void WriteByte(sbyte b) + { + PreAllocatedBuffer[0] = (byte)b; + + Transport.Write(PreAllocatedBuffer, 0, 1); + } + + public override void WriteI16(short i16) + { + PreAllocatedBuffer[0] = (byte)(0xff & (i16 >> 8)); + PreAllocatedBuffer[1] = (byte)(0xff & i16); + + Transport.Write(PreAllocatedBuffer, 0, 2); + } + + public override int WriteUI32(uint ui32, Span buffer) + { + if (buffer.Length < 4) + return 0; + + buffer[0] = (byte)(0xff & (ui32 >> 24)); + buffer[1] = (byte)(0xff & (ui32 >> 16)); + buffer[2] = (byte)(0xff & (ui32 >> 8)); + buffer[3] = (byte)(0xff & ui32); + + return 4; + } + + public override void WriteI32(int i32) + { + PreAllocatedBuffer[0] = (byte)(0xff & (i32 >> 24)); + PreAllocatedBuffer[1] = (byte)(0xff & (i32 >> 16)); + PreAllocatedBuffer[2] = (byte)(0xff & (i32 >> 8)); + PreAllocatedBuffer[3] = (byte)(0xff & i32); + + Transport.Write(PreAllocatedBuffer, 0, 4); + } + + public override void WriteI64(long i64) + { + PreAllocatedBuffer[0] = (byte)(0xff & (i64 >> 56)); + PreAllocatedBuffer[1] = (byte)(0xff & (i64 >> 48)); + PreAllocatedBuffer[2] = (byte)(0xff & (i64 >> 40)); + PreAllocatedBuffer[3] = (byte)(0xff & (i64 >> 32)); + PreAllocatedBuffer[4] = (byte)(0xff & (i64 >> 24)); + PreAllocatedBuffer[5] = (byte)(0xff & (i64 >> 16)); + PreAllocatedBuffer[6] = (byte)(0xff & (i64 >> 8)); + PreAllocatedBuffer[7] = (byte)(0xff & i64); + + Transport.Write(PreAllocatedBuffer, 0, 8); + } + + public override void WriteDouble(double d) + { + WriteI64(BitConverter.DoubleToInt64Bits(d)); + } + +#if NETSTANDARD2_1_OR_GREATER + public override void WriteBinary(ReadOnlySpan bytes) + { + WriteI32(bytes.Length); + Transport.Write(bytes); + } +#endif + + public override void WriteBinary(byte[] bytes, int offset, int count) + { + WriteI32(count); + Transport.Write(bytes, offset, count); + } + + public sealed class Factory : TProtocolFactory + { + private readonly bool StrictRead; + private readonly bool StrictWrite; + + public Factory() + : this(false, true) + { + } + + public Factory(bool strictRead, bool strictWrite) + { + StrictRead = strictRead; + StrictWrite = strictWrite; + } + + public override TProtocol GetProtocol(int initialCapacity = 8192) + { + return new TBinaryProtocol(StrictRead, StrictWrite, initialCapacity); + } + } + } +} diff --git a/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/TCompactProtocol.cs b/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/TCompactProtocol.cs index 2d00e4e017e..29155a4eec1 100644 --- a/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/TCompactProtocol.cs +++ b/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/TCompactProtocol.cs @@ -18,17 +18,14 @@ // under the License. using System; -using System.Buffers; using System.Collections.Generic; using System.Diagnostics; -using System.Text; using Thrift.Protocol.Entities; -using Thrift.Transport; namespace Thrift.Protocol { // ReSharper disable once InconsistentNaming - internal class TCompactProtocol : TProtocol + internal sealed class TCompactProtocol : TProtocol { private const byte ProtocolId = 0x82; private const byte Version = 1; @@ -74,8 +71,10 @@ private struct VarInt count = 0 }; - public TCompactProtocol(TTransport trans) - : base(trans) + private readonly byte[] EmptyUInt32Buffer = new byte[5]; + + public TCompactProtocol(int initialCapacity = 8192) + : base(initialCapacity) { TTypeToCompactType[(int)TType.Stop] = Types.Stop; TTypeToCompactType[(int)TType.Bool] = Types.BooleanTrue; @@ -105,20 +104,35 @@ public TCompactProtocol(TTransport trans) CompactTypeToTType[Types.Struct] = TType.Struct; } - public void Reset() + public override void Reset() { _lastField.Clear(); _lastFieldId = 0; + + base.Reset(); } public override void WriteMessageBegin(TMessage message) { PreAllocatedBuffer[0] = ProtocolId; PreAllocatedBuffer[1] = (byte)((Version & VersionMask) | (((uint)message.Type << TypeShiftAmount) & TypeMask)); - Trans.Write(PreAllocatedBuffer, 0, 2); + Transport.Write(PreAllocatedBuffer, 0, 2); Int32ToVarInt((uint)message.SeqID, ref PreAllocatedVarInt); - Trans.Write(PreAllocatedVarInt.bytes, 0, PreAllocatedVarInt.count); + Transport.Write(PreAllocatedVarInt.bytes, 0, PreAllocatedVarInt.count); + + WriteString(message.Name); + } + + public override void WriteMessageBegin(TMessage message, out int seqIdPosition) + { + PreAllocatedBuffer[0] = ProtocolId; + PreAllocatedBuffer[1] = (byte)((Version & VersionMask) | (((uint)message.Type << TypeShiftAmount) & TypeMask)); + Transport.Write(PreAllocatedBuffer, 0, 2); + + seqIdPosition = (int)Transport.Position; + // Write empty bytes to reserve the space for the seqId to be written later on. + Transport.Write(EmptyUInt32Buffer, 0, 5); WriteString(message.Name); } @@ -149,7 +163,6 @@ private void WriteFieldBeginInternal(TField field, byte fieldType) if (fieldType == NoTypeOverride) fieldType = GetCompactType(field.Type); - // check if we can use delta encoding for the field id if (field.ID > _lastFieldId) { @@ -158,7 +171,7 @@ private void WriteFieldBeginInternal(TField field, byte fieldType) { // Write them together PreAllocatedBuffer[0] = (byte)((delta << 4) | fieldType); - Trans.Write(PreAllocatedBuffer, 0, 1); + Transport.Write(PreAllocatedBuffer, 0, 1); _lastFieldId = field.ID; return; } @@ -166,7 +179,7 @@ private void WriteFieldBeginInternal(TField field, byte fieldType) // Write them separate PreAllocatedBuffer[0] = fieldType; - Trans.Write(PreAllocatedBuffer, 0, 1); + Transport.Write(PreAllocatedBuffer, 0, 1); WriteI16(field.ID); _lastFieldId = field.ID; } @@ -190,46 +203,47 @@ public override void WriteFieldEnd() public override void WriteFieldStop() { PreAllocatedBuffer[0] = Types.Stop; - Trans.Write(PreAllocatedBuffer, 0, 1); + Transport.Write(PreAllocatedBuffer, 0, 1); } - protected void WriteCollectionBegin(TType elemType, int size) + public override void WriteListBegin(TList list) { - /* - Abstract method for writing the start of lists and sets. List and sets on - the wire differ only by the exType indicator. - */ - - if (size <= 14) + if (list.Count <= 14) { - PreAllocatedBuffer[0] = (byte)((size << 4) | GetCompactType(elemType)); - Trans.Write(PreAllocatedBuffer, 0, 1); + PreAllocatedBuffer[0] = (byte)((list.Count << 4) | GetCompactType(list.ElementType)); + Transport.Write(PreAllocatedBuffer, 0, 1); } else { - PreAllocatedBuffer[0] = (byte)(0xf0 | GetCompactType(elemType)); - Trans.Write(PreAllocatedBuffer, 0, 1); + PreAllocatedBuffer[0] = (byte)(0xf0 | GetCompactType(list.ElementType)); + Transport.Write(PreAllocatedBuffer, 0, 1); - Int32ToVarInt((uint)size, ref PreAllocatedVarInt); - Trans.Write(PreAllocatedVarInt.bytes, 0, PreAllocatedVarInt.count); + Int32ToVarInt((uint)list.Count, ref PreAllocatedVarInt); + Transport.Write(PreAllocatedVarInt.bytes, 0, PreAllocatedVarInt.count); } } - public override void WriteListBegin(TList list) - { - WriteCollectionBegin(list.ElementType, list.Count); - } - - public override void WriteListEnd() + public override void WriteListBegin(TList list, out int countPosition) { - } + /* + Note: Compact sizing disabled because size might not be known initially. + if (list.Count <= 14) + { + PreAllocatedBuffer[0] = (byte)((list.Count << 4) | GetCompactType(list.ElementType)); + Transport.Write(PreAllocatedBuffer, 0, 1); + } + else*/ + { + PreAllocatedBuffer[0] = (byte)(0xf0 | GetCompactType(list.ElementType)); + Transport.Write(PreAllocatedBuffer, 0, 1); - public override void WriteSetBegin(TSet set) - { - WriteCollectionBegin(set.ElementType, set.Count); + countPosition = (int)Transport.Position; + // Write empty bytes to reserve the space for the count to be written later on. + Transport.Write(EmptyUInt32Buffer, 0, 5); + } } - public override void WriteSetEnd() + public override void WriteListEnd() { } @@ -253,20 +267,20 @@ public override void WriteBool(bool b) { // we're not part of a field, so just write the value. PreAllocatedBuffer[0] = b ? Types.BooleanTrue : Types.BooleanFalse; - Trans.Write(PreAllocatedBuffer, 0, 1); + Transport.Write(PreAllocatedBuffer, 0, 1); } } public override void WriteByte(sbyte b) { PreAllocatedBuffer[0] = (byte)b; - Trans.Write(PreAllocatedBuffer, 0, 1); + Transport.Write(PreAllocatedBuffer, 0, 1); } public override void WriteI16(short i16) { Int32ToVarInt(IntToZigzag(i16), ref PreAllocatedVarInt); - Trans.Write(PreAllocatedVarInt.bytes, 0, PreAllocatedVarInt.count); + Transport.Write(PreAllocatedVarInt.bytes, 0, PreAllocatedVarInt.count); } private static void Int32ToVarInt(uint n, ref VarInt varint) @@ -291,7 +305,25 @@ private static void Int32ToVarInt(uint n, ref VarInt varint) public override void WriteI32(int i32) { Int32ToVarInt(IntToZigzag(i32), ref PreAllocatedVarInt); - Trans.Write(PreAllocatedVarInt.bytes, 0, PreAllocatedVarInt.count); + Transport.Write(PreAllocatedVarInt.bytes, 0, PreAllocatedVarInt.count); + } + + public override int WriteUI32(uint ui32, Span buffer) + { + if (buffer.Length < 5) + return 0; + + buffer[0] = (byte)(0x80 | (ui32 & 0x7F)); + ui32 >>= 7; + buffer[1] = (byte)(0x80 | (ui32 & 0x7F)); + ui32 >>= 7; + buffer[2] = (byte)(0x80 | (ui32 & 0x7F)); + ui32 >>= 7; + buffer[3] = (byte)(0x80 | (ui32 & 0x7F)); + ui32 >>= 7; + buffer[4] = (byte)ui32; + + return 5; } static private void Int64ToVarInt(ulong n, ref VarInt varint) @@ -315,58 +347,29 @@ static private void Int64ToVarInt(ulong n, ref VarInt varint) public override void WriteI64(long i64) { Int64ToVarInt(LongToZigzag(i64), ref PreAllocatedVarInt); - Trans.Write(PreAllocatedVarInt.bytes, 0, PreAllocatedVarInt.count); + Transport.Write(PreAllocatedVarInt.bytes, 0, PreAllocatedVarInt.count); } public override void WriteDouble(double d) { FixedLongToBytes(BitConverter.DoubleToInt64Bits(d), PreAllocatedBuffer, 0); - Trans.Write(PreAllocatedBuffer, 0, 8); + Transport.Write(PreAllocatedBuffer, 0, 8); } - public override void WriteString(string str) +#if NETSTANDARD2_1_OR_GREATER + public override void WriteBinary(ReadOnlySpan bytes) { - var buf = ArrayPool.Shared.Rent(Encoding.UTF8.GetByteCount(str)); - try - { - var numberOfBytes = Encoding.UTF8.GetBytes(str, 0, str.Length, buf, 0); - - Int32ToVarInt((uint)numberOfBytes, ref PreAllocatedVarInt); - Trans.Write(PreAllocatedVarInt.bytes, 0, PreAllocatedVarInt.count); - Trans.Write(buf, 0, numberOfBytes); - } - finally - { - ArrayPool.Shared.Return(buf); - } + Int32ToVarInt((uint)bytes.Length, ref PreAllocatedVarInt); + Transport.Write(PreAllocatedVarInt.bytes, 0, PreAllocatedVarInt.count); + Transport.Write(bytes); } +#endif public override void WriteBinary(byte[] bytes, int offset, int count) { Int32ToVarInt((uint)count, ref PreAllocatedVarInt); - Trans.Write(PreAllocatedVarInt.bytes, 0, PreAllocatedVarInt.count); - Trans.Write(bytes, offset, count); - } - - public override void WriteMapBegin(TMap map) - { - if (map.Count == 0) - { - PreAllocatedBuffer[0] = 0; - Trans.Write(PreAllocatedBuffer, 0, 1); - } - else - { - Int32ToVarInt((uint)map.Count, ref PreAllocatedVarInt); - Trans.Write(PreAllocatedVarInt.bytes, 0, PreAllocatedVarInt.count); - - PreAllocatedBuffer[0] = (byte)((GetCompactType(map.KeyType) << 4) | GetCompactType(map.ValueType)); - Trans.Write(PreAllocatedBuffer, 0, 1); - } - } - - public override void WriteMapEnd() - { + Transport.Write(PreAllocatedVarInt.bytes, 0, PreAllocatedVarInt.count); + Transport.Write(bytes, offset, count); } private static byte GetCompactType(TType ttype) @@ -400,11 +403,11 @@ private static void FixedLongToBytes(long n, byte[] buf, int off) buf[off + 7] = (byte)((n >> 56) & 0xff); } - public class Factory : TProtocolFactory + public sealed class Factory : TProtocolFactory { - public override TProtocol GetProtocol(TTransport trans) + public override TProtocol GetProtocol(int initialCapacity = 8192) { - return new TCompactProtocol(trans); + return new TCompactProtocol(initialCapacity); } } diff --git a/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/TProtocol.cs b/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/TProtocol.cs index 86bc82eb231..fdba44886d7 100644 --- a/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/TProtocol.cs +++ b/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/TProtocol.cs @@ -19,9 +19,9 @@ using System; using System.Buffers; +using System.IO; using System.Text; using Thrift.Protocol.Entities; -using Thrift.Transport; namespace Thrift.Protocol { @@ -29,37 +29,63 @@ namespace Thrift.Protocol internal abstract class TProtocol : IDisposable { public const int DefaultRecursionDepth = 64; - private bool _isDisposed; - protected int RecursionDepth; - protected TTransport Trans; + private bool _isDisposed; - protected TProtocol(TTransport trans) + protected TProtocol(int initialCapacity = 8192) { - Trans = trans; + Transport = new MemoryStream(initialCapacity); RecursionLimit = DefaultRecursionDepth; RecursionDepth = 0; } - public TTransport Transport => Trans; + protected MemoryStream Transport { get; } + + protected int RecursionDepth { get; set; } protected int RecursionLimit { get; set; } + public ArraySegment WrittenData + { + get => new ArraySegment(Transport.GetBuffer(), 0, (int)Transport.Length); + } + + public int Position + { + get => (int)Transport.Position; + set => Transport.Position = value; + } + + public int Length => (int)Transport.Length; + + public void Clear(int offset = 0) + { + if (offset > Transport.Length) + throw new ArgumentOutOfRangeException(nameof(offset)); + + Transport.Position = offset; + Transport.SetLength(offset); + } + + public virtual void Reset() + { + RecursionDepth = 0; + } + public void Dispose() { Dispose(true); + GC.SuppressFinalize(this); } public void IncrementRecursionDepth() { - if (RecursionDepth < RecursionLimit) - { - ++RecursionDepth; - } - else + if (RecursionDepth >= RecursionLimit) { - throw new TProtocolException(TProtocolException.DEPTH_LIMIT, "Depth limit exceeded"); + throw new TProtocolException(TProtocolException.DEPTH_LIMIT, $"Depth of recursion exceeded the limit: {RecursionLimit}"); } + + ++RecursionDepth; } public void DecrementRecursionDepth() @@ -73,7 +99,7 @@ protected virtual void Dispose(bool disposing) { if (disposing) { - (Trans as IDisposable)?.Dispose(); + Transport.Dispose(); } } _isDisposed = true; @@ -81,6 +107,8 @@ protected virtual void Dispose(bool disposing) public abstract void WriteMessageBegin(TMessage message); + public abstract void WriteMessageBegin(TMessage message, out int seqIdPosition); + public abstract void WriteMessageEnd(); public abstract void WriteStructBegin(TStruct @struct); @@ -93,17 +121,11 @@ protected virtual void Dispose(bool disposing) public abstract void WriteFieldStop(); - public abstract void WriteMapBegin(TMap map); - - public abstract void WriteMapEnd(); - public abstract void WriteListBegin(TList list); - public abstract void WriteListEnd(); - - public abstract void WriteSetBegin(TSet set); + public abstract void WriteListBegin(TList list, out int countPosition); - public abstract void WriteSetEnd(); + public abstract void WriteListEnd(); public abstract void WriteBool(bool b); @@ -113,12 +135,23 @@ protected virtual void Dispose(bool disposing) public abstract void WriteI32(int i32); + public abstract int WriteUI32(uint ui32, Span buffer); + public abstract void WriteI64(long i64); public abstract void WriteDouble(double d); public virtual void WriteString(string s) { +#if NETSTANDARD2_1_OR_GREATER + if (s.Length <= 128) + { + Span buffer = stackalloc byte[256]; + int numberOfBytes = Encoding.UTF8.GetBytes(s, buffer); + WriteBinary(buffer.Slice(0, numberOfBytes)); + return; + } +#endif var buf = ArrayPool.Shared.Rent(Encoding.UTF8.GetByteCount(s)); try { @@ -132,6 +165,25 @@ public virtual void WriteString(string s) } } +#if NETSTANDARD2_1_OR_GREATER + public abstract void WriteBinary(ReadOnlySpan bytes); +#endif + public abstract void WriteBinary(byte[] bytes, int offset, int count); + + public void WriteRaw(byte[] bytes) + { + this.Transport.Write(bytes, 0, bytes.Length); + } + + public void WriteRaw(byte[] bytes, int offset, int count) + { + this.Transport.Write(bytes, offset, count); + } + + public void WriteRaw(ArraySegment bytes) + { + this.Transport.Write(bytes.Array, bytes.Offset, bytes.Count); + } } } diff --git a/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/TProtocolFactory.cs b/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/TProtocolFactory.cs index 003121ae8a5..8f6d2a4b50d 100644 --- a/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/TProtocolFactory.cs +++ b/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Protocol/TProtocolFactory.cs @@ -1,4 +1,4 @@ -// (Turns off StyleCop analysis in this file.) +// (Turns off StyleCop analysis in this file.) // Licensed to the Apache Software Foundation(ASF) under one // or more contributor license agreements.See the NOTICE file @@ -17,13 +17,11 @@ // specific language governing permissions and limitations // under the License. -using Thrift.Transport; - namespace Thrift.Protocol { // ReSharper disable once InconsistentNaming internal abstract class TProtocolFactory { - public abstract TProtocol GetProtocol(TTransport trans); + public abstract TProtocol GetProtocol(int initialCapacity = 8192); } } diff --git a/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/README.md b/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/README.md index 9032a1fe8f7..cc82e705b51 100644 --- a/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/README.md +++ b/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/README.md @@ -1,31 +1,9 @@ # OpenTelemetry - Jaeger Exporter - Apache Thrift -This folder contains a stripped-down fork of the [ApacheThrift +This folder contains a stripped-down and customized fork of the [ApacheThrift 0.13.0.1](https://www.nuget.org/packages/ApacheThrift/0.13.0.1) library from the [apache/thrift](https://github.com/apache/thrift/tree/0.13.0) repo. Only the client bits we need to transmit spans to Jaeger using the compact Thrift -protocol over UDP are included. Removing the other stuff (mainly the server -bits) allowed us to also remove all of the dependencies ApacheThrift requires -with the exception of `System.Threading.Tasks.Extensions` (needed for .NET -Standard 2.0 target only). - -This was done because the official NuGet has two issues: - -* The .NET Standard 2.0 target requires `Microsoft.AspNetCore.Http.Abstractions - (>= 2.2.0)` which forces Jaeger consumers to use at least .NET Core 2.2+. We - wanted to support at least .NET Core 2.1 which is the LTS version. -* The nupkg contains a net45 library with a different API than the .NET Standard - 2.0 library. This breaks .NET Framework consumers of OpenTelemetry using - Jaeger unless we force selection of the lib/netstandard2.0 reference instead. - -Ideally we would consume the official package but these issues made it -difficult. - -Changes: - -* Everything made internal. -* Added [PR#2093](https://github.com/apache/thrift/pull/2093). -* Added [PR#2057](https://github.com/apache/thrift/pull/2057). - -The included files were made synchronous and anything unused was removed. [See -PR #1374](https://github.com/open-telemetry/opentelemetry-dotnet/pull/1374). +protocol over UDP and binary Thrift over HTTP are included. Further +customizations have been made to improve the performance of our specific use +cases. diff --git a/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/TBaseClient.cs b/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/TBaseClient.cs deleted file mode 100644 index bcbd524b4b7..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/TBaseClient.cs +++ /dev/null @@ -1,69 +0,0 @@ -// (Turns off StyleCop analysis in this file.) - -// Licensed to the Apache Software Foundation(ASF) under one -// or more contributor license agreements.See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership.The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -using System; -using Thrift.Protocol; - -namespace Thrift -{ - // ReSharper disable once InconsistentNaming - /// - /// TBaseClient. - /// Base client for generated clients. - /// Do not change this class without checking generated code (namings, etc.) - /// - internal abstract class TBaseClient - { - private readonly TProtocol _outputProtocol; - private bool _isDisposed; - private int _seqId; - public readonly Guid ClientId = Guid.NewGuid(); - - protected TBaseClient(TProtocol outputProtocol) - { - _outputProtocol = outputProtocol ?? throw new ArgumentNullException(nameof(outputProtocol)); - } - - public TProtocol OutputProtocol => _outputProtocol; - - public int SeqId - { - get { return ++_seqId; } - } - - public void Dispose() - { - this.Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - if (!_isDisposed) - { - if (disposing) - { - _outputProtocol?.Dispose(); - } - } - - _isDisposed = true; - } - } -} diff --git a/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Transport/TTransport.cs b/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Transport/TTransport.cs deleted file mode 100644 index 8ad190849dc..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Transport/TTransport.cs +++ /dev/null @@ -1,48 +0,0 @@ -// (Turns off StyleCop analysis in this file.) - -// Licensed to the Apache Software Foundation(ASF) under one -// or more contributor license agreements.See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership.The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -using System; - -namespace Thrift.Transport -{ - // ReSharper disable once InconsistentNaming - internal abstract class TTransport : IDisposable - { - public abstract bool IsOpen { get; } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - public abstract void Close(); - - public void Write(byte[] buffer) - { - Write(buffer, 0, buffer?.Length ?? 0); - } - - public abstract void Write(byte[] buffer, int offset, int length); - - public abstract int Flush(); - - protected abstract void Dispose(bool disposing); - } -} diff --git a/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Transport/TTransportException.cs b/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Transport/TTransportException.cs deleted file mode 100644 index f4a1300fb85..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/ApacheThrift/Transport/TTransportException.cs +++ /dev/null @@ -1,62 +0,0 @@ -// (Turns off StyleCop analysis in this file.) - -// Licensed to the Apache Software Foundation(ASF) under one -// or more contributor license agreements.See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership.The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -using System; - -namespace Thrift.Transport -{ - // ReSharper disable once InconsistentNaming - internal class TTransportException : TException - { - public enum ExceptionType - { - Unknown, - NotOpen, - AlreadyOpen, - TimedOut, - EndOfFile, - Interrupted - } - - public ExceptionType ExType { get; private set; } - - public TTransportException() - { - } - - public TTransportException(ExceptionType exType, Exception inner = null) - : base(string.Empty, inner) - { - ExType = exType; - } - - public TTransportException(ExceptionType exType, string message, Exception inner = null) - : base(message, inner) - { - ExType = exType; - } - - public TTransportException(string message, Exception inner = null) - : base(message, inner) - { - } - - public ExceptionType Type => ExType; - } -} diff --git a/src/OpenTelemetry.Exporter.Jaeger/CHANGELOG.md b/src/OpenTelemetry.Exporter.Jaeger/CHANGELOG.md index 740395daa8c..4d4a038052e 100644 --- a/src/OpenTelemetry.Exporter.Jaeger/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Jaeger/CHANGELOG.md @@ -2,6 +2,48 @@ ## Unreleased +* Improved span duration's precision from millisecond to microsecond + ([#2814](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2814)) + +## 1.2.0-rc1 + +Released 2021-Nov-29 + +## 1.2.0-beta2 + +Released 2021-Nov-19 + +* Changed `JaegerExporterOptions` constructor to throw + `FormatException` if it fails to parse any of the supported environment + variables. + +* Added support for sending spans directly to a Jaeger Collector over HTTP + ([#2574](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2574)) + +## 1.2.0-beta1 + +Released 2021-Oct-08 + +## 1.2.0-alpha4 + +Released 2021-Sep-23 + +## 1.2.0-alpha3 + +Released 2021-Sep-13 + +* `JaegerExporterOptions.BatchExportProcessorOptions` is initialized with + `BatchExportActivityProcessorOptions` which supports field value overriding + using `OTEL_BSP_SCHEDULE_DELAY`, `OTEL_BSP_EXPORT_TIMEOUT`, + `OTEL_BSP_MAX_QUEUE_SIZE`, `OTEL_BSP_MAX_EXPORT_BATCH_SIZE` + envionmental variables as defined in the + [specification](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.5.0/specification/sdk-environment-variables.md#batch-span-processor). + ([#2219](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2219)) + +## 1.2.0-alpha2 + +Released 2021-Aug-24 + ## 1.2.0-alpha1 Released 2021-Jul-23 diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/Batch.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/Batch.cs index 36965771c09..d8b632f7ac3 100644 --- a/src/OpenTelemetry.Exporter.Jaeger/Implementation/Batch.cs +++ b/src/OpenTelemetry.Exporter.Jaeger/Implementation/Batch.cs @@ -14,96 +14,73 @@ // limitations under the License. // +#if NETSTANDARD2_0 || NET461 using System; -using System.Runtime.CompilerServices; -using System.Text; -using OpenTelemetry.Internal; +#endif using Thrift.Protocol; using Thrift.Protocol.Entities; namespace OpenTelemetry.Exporter.Jaeger.Implementation { - internal class Batch + internal sealed class Batch { - private PooledList spanMessages; - - public Batch(Process process) + public Batch(Process process, TProtocol protocol) { - this.Process = process ?? throw new ArgumentNullException(nameof(process)); - this.spanMessages = PooledList.Create(); + this.BatchBeginMessage = this.GenerateBeginMessage(process, protocol, out int spanCountPosition); + this.SpanCountPosition = spanCountPosition; + this.BatchEndMessage = this.GenerateEndMessage(protocol); } - public Process Process { get; } + public byte[] BatchBeginMessage { get; } - public int Count => this.spanMessages.Count; + public int SpanCountPosition { get; set; } - public override string ToString() - { - var sb = new StringBuilder("Batch("); - sb.Append(", Process: "); - sb.Append(this.Process?.ToString() ?? ""); - sb.Append(", Spans: "); - sb.Append(this.spanMessages); - sb.Append(')'); - return sb.ToString(); - } + public byte[] BatchEndMessage { get; } + + public int MinimumMessageSize => this.BatchBeginMessage.Length + + this.BatchEndMessage.Length; - internal void Write(TProtocol oprot) + private byte[] GenerateBeginMessage(Process process, TProtocol oprot, out int spanCountPosition) { - oprot.IncrementRecursionDepth(); - try - { - var struc = new TStruct("Batch"); - - oprot.WriteStructBegin(struc); - - var field = new TField - { - Name = "process", - Type = TType.Struct, - ID = 1, - }; - - oprot.WriteFieldBegin(field); - oprot.Transport.Write(this.Process.Message); - oprot.WriteFieldEnd(); - - field.Name = "spans"; - field.Type = TType.List; - field.ID = 2; - - oprot.WriteFieldBegin(field); - { - oprot.WriteListBegin(new TList(TType.Struct, this.spanMessages.Count)); - - foreach (var s in this.spanMessages) - { - oprot.Transport.Write(s.BufferWriter.Buffer, s.Offset, s.Count); - } - - oprot.WriteListEnd(); - } - - oprot.WriteFieldEnd(); - oprot.WriteFieldStop(); - oprot.WriteStructEnd(); - } - finally + var struc = new TStruct("Batch"); + + oprot.WriteStructBegin(struc); + + var field = new TField { - oprot.DecrementRecursionDepth(); - } - } + Name = "process", + Type = TType.Struct, + ID = 1, + }; - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal void Add(BufferWriterMemory spanMessage) - { - PooledList.Add(ref this.spanMessages, spanMessage); + oprot.WriteFieldBegin(field); + process.Write(oprot); + oprot.WriteFieldEnd(); + + field.Name = "spans"; + field.Type = TType.List; + field.ID = 2; + + oprot.WriteFieldBegin(field); + + oprot.WriteListBegin(new TList(TType.Struct, 0), out spanCountPosition); + + byte[] beginMessage = oprot.WrittenData.ToArray(); + oprot.Clear(); + return beginMessage; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal void Clear() + private byte[] GenerateEndMessage(TProtocol oprot) { - PooledList.Clear(ref this.spanMessages); + oprot.WriteListEnd(); + + oprot.WriteFieldEnd(); + oprot.WriteFieldStop(); + oprot.WriteStructEnd(); + + byte[] endMessage = oprot.WrittenData.ToArray(); + oprot.Clear(); + return endMessage; } } } diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/BufferWriter.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/BufferWriter.cs deleted file mode 100644 index cc32111b6fe..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/Implementation/BufferWriter.cs +++ /dev/null @@ -1,64 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -using System; - -namespace OpenTelemetry.Exporter.Jaeger.Implementation -{ - internal sealed class BufferWriter - { - private int index; - - public BufferWriter(int initialCapacity) - { - if (initialCapacity < 0) - { - throw new ArgumentOutOfRangeException(nameof(initialCapacity), initialCapacity, "initialCapacity should be non-negative."); - } - - this.Buffer = new byte[initialCapacity]; - } - - public byte[] Buffer { get; private set; } - - public BufferWriterMemory GetMemory(int length) - { - this.CheckAndResizeBuffer(length); - var memory = new BufferWriterMemory(this, this.index, length); - this.index += length; - return memory; - } - - public void Clear() => this.index = 0; - - private void CheckAndResizeBuffer(int length) - { - int availableSpace = this.Buffer.Length - this.index; - - if (length > availableSpace) - { - int growBy = Math.Max(length, this.Buffer.Length); - - int newSize = checked(this.Buffer.Length + growBy); - - var previousBuffer = this.Buffer.AsSpan(0, this.index); - - this.Buffer = new byte[newSize]; - - previousBuffer.CopyTo(this.Buffer); - } - } - } -} diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/BufferWriterMemory.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/BufferWriterMemory.cs deleted file mode 100644 index 5f176ad5a94..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/Implementation/BufferWriterMemory.cs +++ /dev/null @@ -1,47 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -using System; - -namespace OpenTelemetry.Exporter.Jaeger.Implementation -{ - internal readonly struct BufferWriterMemory - { - public BufferWriterMemory(BufferWriter bufferWriter, int offset, int count) - { - this.BufferWriter = bufferWriter; - this.Offset = offset; - this.Count = count; - } - - public BufferWriter BufferWriter { get; } - - public int Offset { get; } - - public int Count { get; } - - public BufferWriterMemory Expand(int length) - { - return new BufferWriterMemory(this.BufferWriter, this.Offset, this.Count + length); - } - - public byte[] ToArray() - { - var array = new byte[this.Count]; - Array.Copy(this.BufferWriter.Buffer, this.Offset, array, 0, this.Count); - return array; - } - } -} diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/EmitBatchArgs.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/EmitBatchArgs.cs index 117c7f117fe..038f5fd05ec 100644 --- a/src/OpenTelemetry.Exporter.Jaeger/Implementation/EmitBatchArgs.cs +++ b/src/OpenTelemetry.Exporter.Jaeger/Implementation/EmitBatchArgs.cs @@ -14,44 +14,64 @@ // limitations under the License. // +#if NETSTANDARD2_0 || NET461 +using System; +#endif using Thrift.Protocol; using Thrift.Protocol.Entities; namespace OpenTelemetry.Exporter.Jaeger.Implementation { - internal class EmitBatchArgs + internal sealed class EmitBatchArgs { - public static void Send(int seqId, Batch batch, TProtocol oprot) + public EmitBatchArgs(TProtocol protocol) { - oprot.WriteMessageBegin(new TMessage("emitBatch", TMessageType.Oneway, seqId)); + this.EmitBatchArgsBeginMessage = this.GenerateBeginMessage(protocol, out int seqIdPosition); + this.SeqIdPosition = seqIdPosition; + this.EmitBatchArgsEndMessage = this.GenerateEndMessage(protocol); + } - oprot.IncrementRecursionDepth(); - try - { - var struc = new TStruct("emitBatch_args"); - oprot.WriteStructBegin(struc); - - var field = new TField - { - Name = "batch", - Type = TType.Struct, - ID = 1, - }; - - oprot.WriteFieldBegin(field); - batch.Write(oprot); - oprot.WriteFieldEnd(); - - oprot.WriteFieldStop(); - oprot.WriteStructEnd(); - } - finally + public byte[] EmitBatchArgsBeginMessage { get; } + + public int SeqIdPosition { get; } + + public byte[] EmitBatchArgsEndMessage { get; } + + public int MinimumMessageSize => this.EmitBatchArgsBeginMessage.Length + + this.EmitBatchArgsEndMessage.Length; + + private byte[] GenerateBeginMessage(TProtocol oprot, out int seqIdPosition) + { + oprot.WriteMessageBegin(new TMessage("emitBatch", TMessageType.Oneway, 0), out seqIdPosition); + + var struc = new TStruct("emitBatch_args"); + oprot.WriteStructBegin(struc); + + var field = new TField { - oprot.DecrementRecursionDepth(); - } + Name = "batch", + Type = TType.Struct, + ID = 1, + }; + + oprot.WriteFieldBegin(field); + + byte[] beginMessage = oprot.WrittenData.ToArray(); + oprot.Clear(); + return beginMessage; + } + + private byte[] GenerateEndMessage(TProtocol oprot) + { + oprot.WriteFieldEnd(); + oprot.WriteFieldStop(); + oprot.WriteStructEnd(); oprot.WriteMessageEnd(); - oprot.Transport.Flush(); + + byte[] endMessage = oprot.WrittenData.ToArray(); + oprot.Clear(); + return endMessage; } } } diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/IJaegerClient.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/IJaegerClient.cs index 8a879259827..ada32a9e395 100644 --- a/src/OpenTelemetry.Exporter.Jaeger/Implementation/IJaegerClient.cs +++ b/src/OpenTelemetry.Exporter.Jaeger/Implementation/IJaegerClient.cs @@ -13,8 +13,8 @@ // See the License for the specific language governing permissions and // limitations under the License. // + using System; -using System.Net; namespace OpenTelemetry.Exporter.Jaeger.Implementation { @@ -22,14 +22,10 @@ internal interface IJaegerClient : IDisposable { bool Connected { get; } - EndPoint RemoteEndPoint { get; } - - void Connect(string host, int port); + void Connect(); void Close(); - int Send(byte[] buffer); - int Send(byte[] buffer, int offset, int count); } } diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/InMemoryTransport.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/InMemoryTransport.cs deleted file mode 100644 index d868fb713d1..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/Implementation/InMemoryTransport.cs +++ /dev/null @@ -1,96 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -using System; -using Thrift.Transport; - -namespace OpenTelemetry.Exporter.Jaeger.Implementation -{ - internal class InMemoryTransport : TTransport - { - private readonly BufferWriter bufferWriter; - private BufferWriterMemory? buffer; - - public InMemoryTransport(int initialCapacity = 512) - { - this.bufferWriter = new BufferWriter(initialCapacity); - } - - public override bool IsOpen => true; - - public override void Close() - { - // do nothing - } - - public override void Write(byte[] buffer, int offset, int length) - { - var memory = this.bufferWriter.GetMemory(length); - - var span = new ReadOnlySpan(buffer, offset, length); - span.CopyTo(new Span(memory.BufferWriter.Buffer, memory.Offset, memory.Count)); - - if (!this.buffer.HasValue) - { - this.buffer = memory; - } - else - { - // Resize if we already had a window into the current buffer. - this.buffer = this.buffer.Value.Expand(memory.Count); - } - } - - public override int Flush() - { - return 0; - } - - public byte[] ToArray() - { - if (!this.buffer.HasValue) - { - return Array.Empty(); - } - - var buffer = this.buffer.Value.ToArray(); - this.buffer = null; - return buffer; - } - - public BufferWriterMemory ToBuffer() - { - if (!this.buffer.HasValue) - { - return new BufferWriterMemory(this.bufferWriter, 0, 0); - } - - var buffer = this.buffer.Value; - this.buffer = null; - return buffer; - } - - public void Reset() - { - this.buffer = null; - this.bufferWriter.Clear(); - } - - protected override void Dispose(bool disposing) - { - } - } -} diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerActivityExtensions.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerActivityExtensions.cs index ccc89d17643..d2c2d0876cc 100644 --- a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerActivityExtensions.cs +++ b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerActivityExtensions.cs @@ -128,7 +128,7 @@ public static JaegerSpan ToJaegerSpan(this Activity activity) operationName: activity.DisplayName, flags: (activity.Context.TraceFlags & ActivityTraceFlags.Recorded) > 0 ? 0x1 : 0, startTime: ToEpochMicroseconds(activity.StartTimeUtc), - duration: (long)activity.Duration.TotalMilliseconds * 1000, + duration: activity.Duration.Ticks / TicksPerMicrosecond, references: activity.ToJaegerSpanRefs(), tags: jaegerTags.Tags, logs: activity.ToJaegerLogs()); diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerExporterEventSource.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerExporterEventSource.cs index 45ec68f326a..747982f555b 100644 --- a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerExporterEventSource.cs +++ b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerExporterEventSource.cs @@ -16,7 +16,6 @@ using System; using System.Diagnostics.Tracing; -using System.Security; using OpenTelemetry.Internal; namespace OpenTelemetry.Exporter.Jaeger.Implementation @@ -38,31 +37,10 @@ public void FailedExport(Exception ex) } } - [NonEvent] - public void MissingPermissionsToReadEnvironmentVariable(SecurityException ex) - { - if (this.IsEnabled(EventLevel.Warning, EventKeywords.All)) - { - this.MissingPermissionsToReadEnvironmentVariable(ex.ToInvariantString()); - } - } - [Event(1, Message = "Failed to send spans: '{0}'", Level = EventLevel.Error)] public void FailedExport(string exception) { this.WriteEvent(1, exception); } - - [Event(2, Message = "Failed to parse environment variable: '{0}', value: '{1}'.", Level = EventLevel.Warning)] - public void FailedToParseEnvironmentVariable(string name, string value) - { - this.WriteEvent(2, name, value); - } - - [Event(3, Message = "Missing permissions to read environment variable: '{0}'", Level = EventLevel.Warning)] - public void MissingPermissionsToReadEnvironmentVariable(string exception) - { - this.WriteEvent(3, exception); - } } } diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerExporterException.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerExporterException.cs index 959004c1d29..5deb4960d79 100644 --- a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerExporterException.cs +++ b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerExporterException.cs @@ -19,7 +19,7 @@ namespace OpenTelemetry.Exporter.Jaeger.Implementation { #pragma warning disable CA1032 // Implement standard exception constructors #pragma warning disable CA1064 // Exceptions should be public - internal class JaegerExporterException : Exception + internal sealed class JaegerExporterException : Exception #pragma warning restore CA1064 // Exceptions should be public #pragma warning restore CA1032 // Implement standard exception constructors { diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerHttpClient.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerHttpClient.cs new file mode 100644 index 00000000000..6b4596cd68f --- /dev/null +++ b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerHttpClient.cs @@ -0,0 +1,87 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Diagnostics; +using System.Net.Http; +using System.Net.Http.Headers; + +namespace OpenTelemetry.Exporter.Jaeger.Implementation +{ + internal sealed class JaegerHttpClient : IJaegerClient + { + private static readonly MediaTypeHeaderValue ContentTypeHeader = new MediaTypeHeaderValue("application/vnd.apache.thrift.binary"); + + private readonly Uri endpoint; + private readonly HttpClient httpClient; + private bool disposed; + + public JaegerHttpClient(Uri endpoint, HttpClient httpClient) + { + Debug.Assert(endpoint != null, "endpoint is null"); + Debug.Assert(httpClient != null, "httpClient is null"); + + this.endpoint = endpoint; + this.httpClient = httpClient; + + this.httpClient.BaseAddress = this.endpoint; + } + + public bool Connected => true; + + public void Close() + { + } + + public void Connect() + { + } + + public void Dispose() + { + if (this.disposed) + { + return; + } + + this.httpClient.Dispose(); + + this.disposed = true; + } + + public int Send(byte[] buffer, int offset, int count) + { + // Prevent Jaeger's HTTP operations from being instrumented. + using var scope = SuppressInstrumentationScope.Begin(); + + using HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, "/api/traces"); + + request.Content = new ByteArrayContent(buffer, offset, count) + { + Headers = { ContentType = ContentTypeHeader }, + }; + +#if NET5_0_OR_GREATER + using HttpResponseMessage response = this.httpClient.Send(request); +#else + using HttpResponseMessage response = this.httpClient.SendAsync(request).GetAwaiter().GetResult(); +#endif + response.EnsureSuccessStatusCode(); + + return count; + } + } +} diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerSpanRefType.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerSpanRefType.cs index 91fa06c0606..031a6d151f7 100644 --- a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerSpanRefType.cs +++ b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerSpanRefType.cs @@ -22,12 +22,12 @@ namespace OpenTelemetry.Exporter.Jaeger.Implementation internal enum JaegerSpanRefType { /// - /// A child span + /// A child span. /// CHILD_OF = 0, /// - /// A sibling span + /// A sibling span. /// FOLLOWS_FROM = 1, } diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerTag.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerTag.cs index 5e1592c97cb..5ea83000d04 100644 --- a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerTag.cs +++ b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerTag.cs @@ -91,8 +91,7 @@ public void Write(TProtocol oprot) oprot.WriteString(this.VStr); oprot.WriteFieldEnd(); } - - if (this.VDouble.HasValue) + else if (this.VDouble.HasValue) { field.Name = "vDouble"; field.Type = TType.Double; @@ -101,8 +100,7 @@ public void Write(TProtocol oprot) oprot.WriteDouble(this.VDouble.Value); oprot.WriteFieldEnd(); } - - if (this.VBool.HasValue) + else if (this.VBool.HasValue) { field.Name = "vBool"; field.Type = TType.Bool; @@ -111,8 +109,7 @@ public void Write(TProtocol oprot) oprot.WriteBool(this.VBool.Value); oprot.WriteFieldEnd(); } - - if (this.VLong.HasValue) + else if (this.VLong.HasValue) { field.Name = "vLong"; field.Type = TType.I64; @@ -121,8 +118,7 @@ public void Write(TProtocol oprot) oprot.WriteI64(this.VLong.Value); oprot.WriteFieldEnd(); } - - if (this.VBinary != null) + else if (this.VBinary != null) { field.Name = "vBinary"; field.Type = TType.String; diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerTagType.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerTagType.cs index 4d137dc49cf..3aeb586747c 100644 --- a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerTagType.cs +++ b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerTagType.cs @@ -22,27 +22,27 @@ namespace OpenTelemetry.Exporter.Jaeger.Implementation internal enum JaegerTagType { /// - /// Tag contains a string + /// Tag contains a string. /// STRING = 0, /// - /// Tag contains a double + /// Tag contains a double. /// DOUBLE = 1, /// - /// Tag contains a boolean + /// Tag contains a boolean. /// BOOL = 2, /// - /// Tag contains a long + /// Tag contains a long. /// LONG = 3, /// - /// Tag contains binary data + /// Tag contains binary data. /// BINARY = 4, } diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerThriftClientTransport.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerThriftClientTransport.cs deleted file mode 100644 index d4472c8f133..00000000000 --- a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerThriftClientTransport.cs +++ /dev/null @@ -1,100 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -using System; -using System.IO; -using System.Net.Sockets; -using Thrift.Transport; - -namespace OpenTelemetry.Exporter.Jaeger.Implementation -{ - internal class JaegerThriftClientTransport : TTransport - { - private readonly IJaegerClient client; - private readonly MemoryStream byteStream; - private bool isDisposed; - - public JaegerThriftClientTransport(string host, int port) - : this(host, port, new MemoryStream(), new JaegerUdpClient()) - { - } - - public JaegerThriftClientTransport(string host, int port, MemoryStream stream, IJaegerClient client) - { - this.byteStream = stream; - this.client = client; - this.client.Connect(host, port); - } - - public override bool IsOpen => this.client.Connected; - - public override void Close() - { - this.client.Close(); - } - - public override int Flush() - { - // GetBuffer returns the underlying storage, which saves an allocation over ToArray. - if (!this.byteStream.TryGetBuffer(out var buffer)) - { - buffer = new ArraySegment(this.byteStream.ToArray(), 0, (int)this.byteStream.Length); - } - - if (buffer.Count == 0) - { - return 0; - } - - try - { - return this.client.Send(buffer.Array, buffer.Offset, buffer.Count); - } - catch (SocketException se) - { - throw new TTransportException(TTransportException.ExceptionType.Unknown, $"Cannot flush because of socket exception. UDP Packet size was {buffer.Count}. Exception message: {se.Message}"); - } - catch (Exception e) - { - throw new TTransportException(TTransportException.ExceptionType.Unknown, $"Cannot flush closed transport. {e.Message}"); - } - finally - { - this.byteStream.SetLength(0); - } - } - - public override void Write(byte[] buffer, int offset, int length) - { - this.byteStream.Write(buffer, offset, length); - } - - public override string ToString() - { - return $"{nameof(JaegerThriftClientTransport)}(Client={this.client.RemoteEndPoint})"; - } - - protected override void Dispose(bool disposing) - { - if (!this.isDisposed && disposing) - { - this.byteStream?.Dispose(); - this.client?.Dispose(); - } - - this.isDisposed = true; - } - } -} diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerUdpClient.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerUdpClient.cs index ea16caa1544..71b473a3739 100644 --- a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerUdpClient.cs +++ b/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerUdpClient.cs @@ -14,34 +14,29 @@ // limitations under the License. // -using System; -using System.Net; using System.Net.Sockets; namespace OpenTelemetry.Exporter.Jaeger.Implementation { - internal class JaegerUdpClient : IJaegerClient + internal sealed class JaegerUdpClient : IJaegerClient { + private readonly string host; + private readonly int port; private readonly UdpClient client; private bool disposed; - public JaegerUdpClient() + public JaegerUdpClient(string host, int port) { + this.host = host; + this.port = port; this.client = new UdpClient(); } public bool Connected => this.client.Client.Connected; - public EndPoint RemoteEndPoint => this.client.Client.RemoteEndPoint; - public void Close() => this.client.Close(); - public void Connect(string host, int port) => this.client.Connect(host, port); - - public int Send(byte[] buffer) - { - return this.Send(buffer, 0, buffer?.Length ?? 0); - } + public void Connect() => this.client.Connect(this.host, this.port); public int Send(byte[] buffer, int offset, int count) { @@ -50,26 +45,13 @@ public int Send(byte[] buffer, int offset, int count) /// public void Dispose() - { - this.Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Releases the unmanaged resources used by this class and optionally releases the managed resources. - /// - /// to release both managed and unmanaged resources; to release only unmanaged resources. - protected virtual void Dispose(bool disposing) { if (this.disposed) { return; } - if (disposing) - { - this.client.Dispose(); - } + this.client.Dispose(); this.disposed = true; } diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/Process.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/Process.cs index b8e7cee3d69..95bb58587b1 100644 --- a/src/OpenTelemetry.Exporter.Jaeger/Implementation/Process.cs +++ b/src/OpenTelemetry.Exporter.Jaeger/Implementation/Process.cs @@ -22,7 +22,7 @@ namespace OpenTelemetry.Exporter.Jaeger.Implementation { - internal class Process + internal sealed class Process { public Process(string serviceName) { @@ -47,8 +47,6 @@ internal Process(string serviceName, Dictionary processTags) internal Dictionary Tags { get; set; } - internal byte[] Message { get; set; } - public override string ToString() { var sb = new StringBuilder("Process("); diff --git a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerThriftClient.cs b/src/OpenTelemetry.Exporter.Jaeger/Implementation/ShimExtensions.cs similarity index 54% rename from src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerThriftClient.cs rename to src/OpenTelemetry.Exporter.Jaeger/Implementation/ShimExtensions.cs index 273c3d9a273..e1d7f26a2b4 100644 --- a/src/OpenTelemetry.Exporter.Jaeger/Implementation/JaegerThriftClient.cs +++ b/src/OpenTelemetry.Exporter.Jaeger/Implementation/ShimExtensions.cs @@ -1,4 +1,4 @@ -// +// // Copyright The OpenTelemetry Authors // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,24 +14,23 @@ // limitations under the License. // -using Thrift; -using Thrift.Protocol; - -namespace OpenTelemetry.Exporter.Jaeger.Implementation +#if NETSTANDARD2_0 || NET461 +namespace System { - internal class JaegerThriftClient : TBaseClient + internal static class ShimExtensions { - public JaegerThriftClient(TProtocol outputProtocol) - : base(outputProtocol) + public static byte[] ToArray(this ArraySegment arraySegment) { - } + int count = arraySegment.Count; + if (count == 0) + { + return Array.Empty(); + } - internal void SendBatch(Batch batch) - { - EmitBatchArgs.Send( - this.SeqId, - batch, - this.OutputProtocol); + var array = new byte[count]; + Array.Copy(arraySegment.Array, arraySegment.Offset, array, 0, count); + return array; } } } +#endif diff --git a/src/OpenTelemetry.Exporter.InMemory/InMemoryExporterOptions.cs b/src/OpenTelemetry.Exporter.Jaeger/JaegerExportProtocol.cs similarity index 55% rename from src/OpenTelemetry.Exporter.InMemory/InMemoryExporterOptions.cs rename to src/OpenTelemetry.Exporter.Jaeger/JaegerExportProtocol.cs index 767741cb527..8898ac96130 100644 --- a/src/OpenTelemetry.Exporter.InMemory/InMemoryExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.Jaeger/JaegerExportProtocol.cs @@ -1,4 +1,4 @@ -// +// // Copyright The OpenTelemetry Authors // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,17 +16,25 @@ namespace OpenTelemetry.Exporter { - public class InMemoryExporterOptions + /// + /// Defines the exporter protocols supported by the . + /// + public enum JaegerExportProtocol : byte { /// - /// Gets or sets the metric export interval in milliseconds. The default value is 1000 milliseconds. + /// Compact thrift protocol over UDP. /// - public int MetricExportIntervalMilliseconds { get; set; } = 1000; + /// + /// Note: Supported by Jaeger Agents only. + /// + UdpCompactThrift = 0, /// - /// Gets or sets a value indicating whether to export Delta - /// values or not (Cumulative). + /// Binary thrift protocol over HTTP. /// - public bool IsDelta { get; set; } = true; + /// + /// Note: Supported by Jaeger Collectors only. + /// + HttpBinaryThrift = 1, } } diff --git a/src/OpenTelemetry.Exporter.Jaeger/JaegerExporter.cs b/src/OpenTelemetry.Exporter.Jaeger/JaegerExporter.cs index 5d2a888c8ac..3de88ba15b5 100644 --- a/src/OpenTelemetry.Exporter.Jaeger/JaegerExporter.cs +++ b/src/OpenTelemetry.Exporter.Jaeger/JaegerExporter.cs @@ -20,50 +20,75 @@ using System.Linq; using System.Runtime.CompilerServices; using OpenTelemetry.Exporter.Jaeger.Implementation; +using OpenTelemetry.Internal; using OpenTelemetry.Resources; using Thrift.Protocol; -using Thrift.Transport; using Process = OpenTelemetry.Exporter.Jaeger.Implementation.Process; namespace OpenTelemetry.Exporter { public class JaegerExporter : BaseExporter { + internal uint NumberOfSpansInCurrentBatch; + + private readonly byte[] uInt32Storage = new byte[8]; private readonly int maxPayloadSizeInBytes; - private readonly TProtocolFactory protocolFactory; - private readonly TTransport clientTransport; - private readonly JaegerThriftClient thriftClient; - private readonly InMemoryTransport memoryTransport; - private readonly TProtocol memoryProtocol; - private int batchByteSize; - private bool disposedValue; // To detect redundant dispose calls + private readonly IJaegerClient client; + private readonly TProtocol batchWriter; + private readonly TProtocol spanWriter; + private readonly bool sendUsingEmitBatchArgs; + private int minimumBatchSizeInBytes; + private int currentBatchSizeInBytes; + private int spanStartPosition; + private uint sequenceId; + private bool disposed; public JaegerExporter(JaegerExporterOptions options) : this(options, null) { } - internal JaegerExporter(JaegerExporterOptions options, TTransport clientTransport = null) + internal JaegerExporter(JaegerExporterOptions options, TProtocolFactory protocolFactory = null, IJaegerClient client = null) { - if (options is null) + Guard.ThrowIfNull(options, nameof(options)); + + this.maxPayloadSizeInBytes = (!options.MaxPayloadSizeInBytes.HasValue || options.MaxPayloadSizeInBytes <= 0) + ? JaegerExporterOptions.DefaultMaxPayloadSizeInBytes + : options.MaxPayloadSizeInBytes.Value; + + if (options.Protocol == JaegerExportProtocol.UdpCompactThrift) + { + protocolFactory ??= new TCompactProtocol.Factory(); + client ??= new JaegerUdpClient(options.AgentHost, options.AgentPort); + this.sendUsingEmitBatchArgs = true; + } + else if (options.Protocol == JaegerExportProtocol.HttpBinaryThrift) + { + protocolFactory ??= new TBinaryProtocol.Factory(); + client ??= new JaegerHttpClient( + options.Endpoint, + options.HttpClientFactory?.Invoke() ?? throw new InvalidOperationException("JaegerExporterOptions was missing HttpClientFactory or it returned null.")); + } + else { - throw new ArgumentNullException(nameof(options)); + throw new NotSupportedException(); } - this.maxPayloadSizeInBytes = (!options.MaxPayloadSizeInBytes.HasValue || options.MaxPayloadSizeInBytes <= 0) ? JaegerExporterOptions.DefaultMaxPayloadSizeInBytes : options.MaxPayloadSizeInBytes.Value; - this.protocolFactory = new TCompactProtocol.Factory(); - this.clientTransport = clientTransport ?? new JaegerThriftClientTransport(options.AgentHost, options.AgentPort); - this.thriftClient = new JaegerThriftClient(this.protocolFactory.GetProtocol(this.clientTransport)); - this.memoryTransport = new InMemoryTransport(16000); - this.memoryProtocol = this.protocolFactory.GetProtocol(this.memoryTransport); + this.client = client; + this.batchWriter = protocolFactory.GetProtocol(this.maxPayloadSizeInBytes * 2); + this.spanWriter = protocolFactory.GetProtocol(this.maxPayloadSizeInBytes); - string serviceName = (string)this.ParentProvider.GetDefaultResource().Attributes.Where( - pair => pair.Key == ResourceSemanticConventions.AttributeServiceName).FirstOrDefault().Value; + string serviceName = (string)this.ParentProvider.GetDefaultResource().Attributes.FirstOrDefault( + pair => pair.Key == ResourceSemanticConventions.AttributeServiceName).Value; this.Process = new Process(serviceName); + + client.Connect(); } internal Process Process { get; set; } + internal EmitBatchArgs EmitBatchArgs { get; private set; } + internal Batch Batch { get; private set; } /// @@ -78,7 +103,9 @@ public override ExportResult Export(in Batch activityBatch) foreach (var activity in activityBatch) { - this.AppendSpan(activity.ToJaegerSpan()); + var jaegerSpan = activity.ToJaegerSpan(); + this.AppendSpan(jaegerSpan); + jaegerSpan.Return(); } this.SendCurrentBatch(); @@ -95,10 +122,7 @@ public override ExportResult Export(in Batch activityBatch) internal void SetResourceAndInitializeBatch(Resource resource) { - if (resource is null) - { - throw new ArgumentNullException(nameof(resource)); - } + Guard.ThrowIfNull(resource, nameof(resource)); var process = this.Process; @@ -141,84 +165,114 @@ internal void SetResourceAndInitializeBatch(Resource resource) process.ServiceName = serviceName; } - this.Process.Message = this.BuildThriftMessage(this.Process).ToArray(); - this.Batch = new Batch(this.Process); - this.batchByteSize = this.Process.Message.Length; + this.Batch = new Batch(this.Process, this.batchWriter); + if (this.sendUsingEmitBatchArgs) + { + this.EmitBatchArgs = new EmitBatchArgs(this.batchWriter); + this.Batch.SpanCountPosition += this.EmitBatchArgs.EmitBatchArgsBeginMessage.Length; + this.batchWriter.WriteRaw(this.EmitBatchArgs.EmitBatchArgsBeginMessage); + } + + this.batchWriter.WriteRaw(this.Batch.BatchBeginMessage); + this.spanStartPosition = this.batchWriter.Position; + + this.minimumBatchSizeInBytes = this.EmitBatchArgs?.MinimumMessageSize ?? 0 + + this.Batch.MinimumMessageSize; + + this.ResetBatch(); } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void AppendSpan(JaegerSpan jaegerSpan) { - var spanMessage = this.BuildThriftMessage(jaegerSpan); + jaegerSpan.Write(this.spanWriter); + try + { + var spanTotalBytesNeeded = this.spanWriter.Length; - jaegerSpan.Return(); + if (this.NumberOfSpansInCurrentBatch > 0 + && this.currentBatchSizeInBytes + spanTotalBytesNeeded >= this.maxPayloadSizeInBytes) + { + this.SendCurrentBatch(); + } - var spanTotalBytesNeeded = spanMessage.Count; + var spanData = this.spanWriter.WrittenData; + this.batchWriter.WriteRaw(spanData); - if (this.batchByteSize + spanTotalBytesNeeded >= this.maxPayloadSizeInBytes) + this.NumberOfSpansInCurrentBatch++; + this.currentBatchSizeInBytes += spanTotalBytesNeeded; + } + finally { - this.SendCurrentBatch(); - - // SendCurrentBatch clears/invalidates the BufferWriter in InMemoryTransport. - // The new spanMessage is still located in it, though. It might get overwritten later - // when spans are written in the buffer for the next batch. - // Move spanMessage to the beginning of the BufferWriter to avoid data corruption. - this.memoryTransport.Write(spanMessage.BufferWriter.Buffer, spanMessage.Offset, spanMessage.Count); - spanMessage = this.memoryTransport.ToBuffer(); + this.spanWriter.Clear(); } - - this.Batch.Add(spanMessage); - this.batchByteSize += spanTotalBytesNeeded; } - /// - protected override void Dispose(bool disposing) + internal void SendCurrentBatch() { - if (!this.disposedValue) + try { - if (disposing) + this.batchWriter.WriteRaw(this.Batch.BatchEndMessage); + + if (this.sendUsingEmitBatchArgs) { - this.thriftClient.Dispose(); - this.clientTransport.Dispose(); - this.memoryTransport.Dispose(); - this.memoryProtocol.Dispose(); + this.batchWriter.WriteRaw(this.EmitBatchArgs.EmitBatchArgsEndMessage); + + this.WriteUInt32AtPosition(this.EmitBatchArgs.SeqIdPosition, ++this.sequenceId); } - this.disposedValue = true; - } + this.WriteUInt32AtPosition(this.Batch.SpanCountPosition, this.NumberOfSpansInCurrentBatch); - base.Dispose(disposing); - } + var writtenData = this.batchWriter.WrittenData; - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void SendCurrentBatch() - { - try - { - this.thriftClient.SendBatch(this.Batch); + this.client.Send(writtenData.Array, writtenData.Offset, writtenData.Count); } finally { - this.Batch.Clear(); - this.batchByteSize = this.Process.Message.Length; - this.memoryTransport.Reset(); + this.ResetBatch(); } } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private BufferWriterMemory BuildThriftMessage(Process process) + /// + protected override void Dispose(bool disposing) { - process.Write(this.memoryProtocol); + if (!this.disposed) + { + if (disposing) + { + try + { + this.client.Close(); + } + catch + { + } + + this.client.Dispose(); + this.batchWriter.Dispose(); + this.spanWriter.Dispose(); + } + + this.disposed = true; + } - return this.memoryTransport.ToBuffer(); + base.Dispose(disposing); } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private BufferWriterMemory BuildThriftMessage(in JaegerSpan jaegerSpan) + private void WriteUInt32AtPosition(int position, uint value) { - jaegerSpan.Write(this.memoryProtocol); + this.batchWriter.Position = position; + int numberOfBytes = this.batchWriter.WriteUI32(value, this.uInt32Storage); + this.batchWriter.WriteRaw(this.uInt32Storage, 0, numberOfBytes); + } - return this.memoryTransport.ToBuffer(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ResetBatch() + { + this.currentBatchSizeInBytes = this.minimumBatchSizeInBytes; + this.NumberOfSpansInCurrentBatch = 0; + this.batchWriter.Clear(this.spanStartPosition); } } } diff --git a/src/OpenTelemetry.Exporter.Jaeger/JaegerExporterHelperExtensions.cs b/src/OpenTelemetry.Exporter.Jaeger/JaegerExporterHelperExtensions.cs index 827b35047b7..7d3e641d5be 100644 --- a/src/OpenTelemetry.Exporter.Jaeger/JaegerExporterHelperExtensions.cs +++ b/src/OpenTelemetry.Exporter.Jaeger/JaegerExporterHelperExtensions.cs @@ -15,7 +15,10 @@ // using System; +using System.Net.Http; +using System.Reflection; using OpenTelemetry.Exporter; +using OpenTelemetry.Internal; namespace OpenTelemetry.Trace { @@ -30,29 +33,58 @@ public static class JaegerExporterHelperExtensions /// builder to use. /// Exporter configuration options. /// The instance of to chain the calls. - [System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "The objects should not be disposed.")] public static TracerProviderBuilder AddJaegerExporter(this TracerProviderBuilder builder, Action configure = null) { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } + Guard.ThrowIfNull(builder, nameof(builder)); if (builder is IDeferredTracerProviderBuilder deferredTracerProviderBuilder) { return deferredTracerProviderBuilder.Configure((sp, builder) => { - AddJaegerExporter(builder, sp.GetOptions(), configure); + AddJaegerExporter(builder, sp.GetOptions(), configure, sp); }); } - return AddJaegerExporter(builder, new JaegerExporterOptions(), configure); + return AddJaegerExporter(builder, new JaegerExporterOptions(), configure, serviceProvider: null); } - private static TracerProviderBuilder AddJaegerExporter(TracerProviderBuilder builder, JaegerExporterOptions options, Action configure = null) + private static TracerProviderBuilder AddJaegerExporter( + TracerProviderBuilder builder, + JaegerExporterOptions options, + Action configure, + IServiceProvider serviceProvider) { configure?.Invoke(options); + if (serviceProvider != null + && options.Protocol == JaegerExportProtocol.HttpBinaryThrift + && options.HttpClientFactory == JaegerExporterOptions.DefaultHttpClientFactory) + { + options.HttpClientFactory = () => + { + Type httpClientFactoryType = Type.GetType("System.Net.Http.IHttpClientFactory, Microsoft.Extensions.Http", throwOnError: false); + if (httpClientFactoryType != null) + { + object httpClientFactory = serviceProvider.GetService(httpClientFactoryType); + if (httpClientFactory != null) + { + MethodInfo createClientMethod = httpClientFactoryType.GetMethod( + "CreateClient", + BindingFlags.Public | BindingFlags.Instance, + binder: null, + new Type[] { typeof(string) }, + modifiers: null); + if (createClientMethod != null) + { + return (HttpClient)createClientMethod.Invoke(httpClientFactory, new object[] { "JaegerExporter" }); + } + } + } + + return new HttpClient(); + }; + } + var jaegerExporter = new JaegerExporter(options); if (options.ExportProcessorType == ExportProcessorType.Simple) diff --git a/src/OpenTelemetry.Exporter.Jaeger/JaegerExporterOptions.cs b/src/OpenTelemetry.Exporter.Jaeger/JaegerExporterOptions.cs index ed87beb5926..3f2397bb498 100644 --- a/src/OpenTelemetry.Exporter.Jaeger/JaegerExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.Jaeger/JaegerExporterOptions.cs @@ -16,59 +16,74 @@ using System; using System.Diagnostics; -using System.Security; -using OpenTelemetry.Exporter.Jaeger.Implementation; +using System.Net.Http; +using OpenTelemetry.Internal; +using OpenTelemetry.Trace; namespace OpenTelemetry.Exporter { + /// + /// Jaeger exporter options. + /// OTEL_EXPORTER_JAEGER_AGENT_HOST, OTEL_EXPORTER_JAEGER_AGENT_PORT + /// environment variables are parsed during object construction. + /// + /// + /// The constructor throws if it fails to parse + /// any of the supported environment variables. + /// public class JaegerExporterOptions { internal const int DefaultMaxPayloadSizeInBytes = 4096; + internal const string OtelProtocolEnvVarKey = "OTEL_EXPORTER_JAEGER_PROTOCOL"; internal const string OTelAgentHostEnvVarKey = "OTEL_EXPORTER_JAEGER_AGENT_HOST"; internal const string OTelAgentPortEnvVarKey = "OTEL_EXPORTER_JAEGER_AGENT_PORT"; + internal const string OTelEndpointEnvVarKey = "OTEL_EXPORTER_JAEGER_ENDPOINT"; + + internal static readonly Func DefaultHttpClientFactory = () => new HttpClient(); public JaegerExporterOptions() { - try + if (EnvironmentVariableHelper.LoadString(OtelProtocolEnvVarKey, out string protocolEnvVar) + && Enum.TryParse(protocolEnvVar, ignoreCase: true, out JaegerExportProtocol protocol)) + { + this.Protocol = protocol; + } + + if (EnvironmentVariableHelper.LoadString(OTelAgentHostEnvVarKey, out string agentHostEnvVar)) { - string agentHostEnvVar = Environment.GetEnvironmentVariable(OTelAgentHostEnvVarKey); - if (!string.IsNullOrEmpty(agentHostEnvVar)) - { - this.AgentHost = agentHostEnvVar; - } - - string agentPortEnvVar = Environment.GetEnvironmentVariable(OTelAgentPortEnvVarKey); - if (!string.IsNullOrEmpty(agentPortEnvVar)) - { - if (int.TryParse(agentPortEnvVar, out var agentPortValue)) - { - this.AgentPort = agentPortValue; - } - else - { - JaegerExporterEventSource.Log.FailedToParseEnvironmentVariable(OTelAgentPortEnvVarKey, agentPortEnvVar); - } - } + this.AgentHost = agentHostEnvVar; } - catch (SecurityException ex) + + if (EnvironmentVariableHelper.LoadNumeric(OTelAgentPortEnvVarKey, out int agentPortEnvVar)) + { + this.AgentPort = agentPortEnvVar; + } + + if (EnvironmentVariableHelper.LoadString(OTelEndpointEnvVarKey, out string endpointEnvVar) + && Uri.TryCreate(endpointEnvVar, UriKind.Absolute, out Uri endpoint)) { - // The caller does not have the required permission to - // retrieve the value of an environment variable from the current process. - JaegerExporterEventSource.Log.MissingPermissionsToReadEnvironmentVariable(ex); + this.Endpoint = endpoint; } } + public JaegerExportProtocol Protocol { get; set; } = JaegerExportProtocol.UdpCompactThrift; + /// /// Gets or sets the Jaeger agent host. Default value: localhost. /// public string AgentHost { get; set; } = "localhost"; /// - /// Gets or sets the Jaeger agent "compact thrift protocol" port. Default value: 6831. + /// Gets or sets the Jaeger agent port. Default value: 6831. /// public int AgentPort { get; set; } = 6831; + /// + /// Gets or sets the Jaeger HTTP endpoint. Default value: "http://localhost:14268". + /// + public Uri Endpoint { get; set; } = new Uri("http://localhost:14268"); + /// /// Gets or sets the maximum payload size in bytes. Default value: 4096. /// @@ -82,6 +97,29 @@ public JaegerExporterOptions() /// /// Gets or sets the BatchExportProcessor options. Ignored unless ExportProcessorType is BatchExporter. /// - public BatchExportProcessorOptions BatchExportProcessorOptions { get; set; } = new BatchExportProcessorOptions(); + public BatchExportProcessorOptions BatchExportProcessorOptions { get; set; } = new BatchExportActivityProcessorOptions(); + + /// + /// Gets or sets the factory function called to create the instance that will be used at runtime to + /// transmit spans over HTTP. The returned instance will be reused for + /// all export invocations. + /// + /// + /// Notes: + /// + /// This is only invoked for the protocol. + /// The default behavior when using the extension is if an IHttpClientFactory + /// instance can be resolved through the application then an will be + /// created through the factory with the name "JaegerExporter" otherwise + /// an will be instantiated directly. + /// + /// + public Func HttpClientFactory { get; set; } = DefaultHttpClientFactory; } } diff --git a/src/OpenTelemetry.Exporter.Jaeger/OpenTelemetry.Exporter.Jaeger.csproj b/src/OpenTelemetry.Exporter.Jaeger/OpenTelemetry.Exporter.Jaeger.csproj index 77427f0943a..81ce14572bd 100644 --- a/src/OpenTelemetry.Exporter.Jaeger/OpenTelemetry.Exporter.Jaeger.csproj +++ b/src/OpenTelemetry.Exporter.Jaeger/OpenTelemetry.Exporter.Jaeger.csproj @@ -1,15 +1,17 @@  - netstandard2.0;netstandard2.1;net461 + netstandard2.0;netstandard2.1;net461;net5.0 Jaeger exporter for OpenTelemetry .NET $(PackageTags);Jaeger;distributed-tracing core- - - + + false @@ -25,6 +27,9 @@ + + + diff --git a/src/OpenTelemetry.Exporter.Jaeger/README.md b/src/OpenTelemetry.Exporter.Jaeger/README.md index e69929ea840..522e8bb262f 100644 --- a/src/OpenTelemetry.Exporter.Jaeger/README.md +++ b/src/OpenTelemetry.Exporter.Jaeger/README.md @@ -14,7 +14,7 @@ the Compact Thrift API port, and as such only supports Thrift over UDP. This package supports all the officially supported versions of [.NET Core](https://dotnet.microsoft.com/download/dotnet-core). -For .NET Framework, versions 4.6 and above are supported. +For .NET Framework, versions 4.6.1 and above are supported. ## Prerequisite @@ -37,19 +37,39 @@ take precedence over the environment variables. The `JaegerExporter` can be configured using the `JaegerExporterOptions` properties: -* `AgentHost`: The Jaeger Agent host (default `localhost`). -* `AgentPort`: The compact thrift protocol UDP port of the Jaeger Agent - (default `6831`). -* `MaxPayloadSizeInBytes`: The maximum size of each UDP packet that gets - sent to the agent (default `4096`). -* `ExportProcessorType`: Whether the exporter should use - [Batch or Simple exporting processor](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#built-in-span-processors) - (default `ExportProcessorType.Batch`). +* `AgentHost`: The Jaeger Agent host (default `localhost`). Used for + `UdpCompactThrift` protocol. + +* `AgentPort`: The Jaeger Agent port (default `6831`). Used for + `UdpCompactThrift` protocol. + * `BatchExportProcessorOptions`: Configuration options for the batch exporter. - Only used if ExportProcessorType is set to Batch. + Only used if `ExportProcessorType` is set to `Batch`. + +* `Endpoint`: The Jaeger Collector HTTP endpoint (default + `http://localhost:14268`). Used for `HttpBinaryThrift` protocol. + +* `ExportProcessorType`: Whether the exporter should use [Batch or Simple + exporting + processor](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#built-in-span-processors) + (default `ExportProcessorType.Batch`). + +* `HttpClientFactory`: A factory function called to create the `HttpClient` + instance that will be used at runtime to transmit spans over HTTP when the + `HttpBinaryThrift` protocol is configured. See [Configure + HttpClient](#configure-httpclient) for more details. + +* `MaxPayloadSizeInBytes`: The maximum size of each batch that gets sent to the + agent or collector (default `4096`). -See the -[`TestJaegerExporter.cs`](../../examples/Console/TestJaegerExporter.cs) +* `Protocol`: The protocol to use. The default value is `UdpCompactThrift`. + + | Protocol | Description | + |----------------|-------------------------------------------------------| + |UdpCompactThrift| Apache Thrift compact over UDP to a Jaeger Agent. | + |HttpBinaryThrift| Apache Thrift binary over HTTP to a Jaeger Collector. | + +See the [`TestJaegerExporter.cs`](../../examples/Console/TestJaegerExporter.cs) for an example of how to use the exporter. ## Environment Variables @@ -57,10 +77,51 @@ for an example of how to use the exporter. The following environment variables can be used to override the default values of the `JaegerExporterOptions`. -| Environment variable | `JaegerExporterOptions` property | -| --------------------------------- | -------------------------------- | -| `OTEL_EXPORTER_JAEGER_AGENT_HOST` | `AgentHost` | -| `OTEL_EXPORTER_JAEGER_AGENT_PORT` | `AgentPort` | +| Environment variable | `JaegerExporterOptions` property | +| ---------------------------------- | -------------------------------- | +| `OTEL_EXPORTER_JAEGER_AGENT_HOST` | `AgentHost` | +| `OTEL_EXPORTER_JAEGER_AGENT_PORT` | `AgentPort` | +| `OTEL_EXPORTER_JAEGER_ENDPOINT` | `Endpoint` | +| `OTEL_EXPORTER_JAEGER_PROTOCOL` | `Protocol` | + +`FormatException` is thrown in case of an invalid value for any of the +supported environment variables. + +## Configure HttpClient + +The `HttpClientFactory` option is provided on `JaegerExporterOptions` for users +who want to configure the `HttpClient` used by the `JaegerExporter` when +`HttpBinaryThrift` protocol is used. Simply replace the function with your own +implementation if you want to customize the generated `HttpClient`: + +```csharp +services.AddOpenTelemetryTracing((builder) => builder + .AddJaegerExporter(o => + { + o.Protocol = JaegerExportProtocol.HttpBinaryThrift; + o.HttpClientFactory = () => + { + HttpClient client = new HttpClient(); + client.DefaultRequestHeaders.Add("X-MyCustomHeader", "value"); + return client; + }; + })); +``` + +For users using +[IHttpClientFactory](https://docs.microsoft.com/dotnet/architecture/microservices/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests) +you may also customize the named "JaegerExporter" `HttpClient` using the +built-in `AddHttpClient` extension: + +```csharp +services.AddHttpClient( + "JaegerExporter", + configureClient: (client) => + client.DefaultRequestHeaders.Add("X-MyCustomHeader", "value")); +``` + +Note: The single instance returned by `HttpClientFactory` is reused by all +export requests. ## References diff --git a/src/OpenTelemetry.Instrumentation.SqlClient/.publicApi/net452/PublicAPI.Shipped.txt b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs/.publicApi/net461/PublicAPI.Shipped.txt similarity index 100% rename from src/OpenTelemetry.Instrumentation.SqlClient/.publicApi/net452/PublicAPI.Shipped.txt rename to src/OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs/.publicApi/net461/PublicAPI.Shipped.txt diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs/.publicApi/net461/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs/.publicApi/net461/PublicAPI.Unshipped.txt new file mode 100644 index 00000000000..78927f7a9cc --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs/.publicApi/net461/PublicAPI.Unshipped.txt @@ -0,0 +1,2 @@ +OpenTelemetry.Logs.OtlpLogExporterHelperExtensions +static OpenTelemetry.Logs.OtlpLogExporterHelperExtensions.AddOtlpExporter(this OpenTelemetry.Logs.OpenTelemetryLoggerOptions loggerOptions, System.Action configure = null) -> OpenTelemetry.Logs.OpenTelemetryLoggerOptions diff --git a/src/OpenTelemetry.Shims.OpenTracing/.publicApi/net452/PublicAPI.Shipped.txt b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs/.publicApi/netstandard2.0/PublicAPI.Shipped.txt similarity index 100% rename from src/OpenTelemetry.Shims.OpenTracing/.publicApi/net452/PublicAPI.Shipped.txt rename to src/OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs/.publicApi/netstandard2.0/PublicAPI.Shipped.txt diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt new file mode 100644 index 00000000000..78927f7a9cc --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt @@ -0,0 +1,2 @@ +OpenTelemetry.Logs.OtlpLogExporterHelperExtensions +static OpenTelemetry.Logs.OtlpLogExporterHelperExtensions.AddOtlpExporter(this OpenTelemetry.Logs.OpenTelemetryLoggerOptions loggerOptions, System.Action configure = null) -> OpenTelemetry.Logs.OpenTelemetryLoggerOptions diff --git a/src/OpenTelemetry/.publicApi/net452/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs/.publicApi/netstandard2.1/PublicAPI.Shipped.txt similarity index 100% rename from src/OpenTelemetry/.publicApi/net452/PublicAPI.Unshipped.txt rename to src/OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs/.publicApi/netstandard2.1/PublicAPI.Shipped.txt diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt new file mode 100644 index 00000000000..78927f7a9cc --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt @@ -0,0 +1,2 @@ +OpenTelemetry.Logs.OtlpLogExporterHelperExtensions +static OpenTelemetry.Logs.OtlpLogExporterHelperExtensions.AddOtlpExporter(this OpenTelemetry.Logs.OpenTelemetryLoggerOptions loggerOptions, System.Action configure = null) -> OpenTelemetry.Logs.OpenTelemetryLoggerOptions diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs/AssemblyInfo.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs/AssemblyInfo.cs new file mode 100644 index 00000000000..8e90458d1fc --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs/AssemblyInfo.cs @@ -0,0 +1,31 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Runtime.CompilerServices; + +#if SIGNED +[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010051c1562a090fb0c9f391012a32198b5e5d9a60e9b80fa2d7b434c9e5ccb7259bd606e66f9660676afc6692b8cdc6793d190904551d2103b7b22fa636dcbb8208839785ba402ea08fc00c8f1500ccef28bbf599aa64ffb1e1d5dc1bf3420a3777badfe697856e9d52070a50c3ea5821c80bef17ca3acffa28f89dd413f096f898")] +[assembly: InternalsVisibleTo("Benchmarks, PublicKey=002400000480000094000000060200000024000052534131000400000100010051c1562a090fb0c9f391012a32198b5e5d9a60e9b80fa2d7b434c9e5ccb7259bd606e66f9660676afc6692b8cdc6793d190904551d2103b7b22fa636dcbb8208839785ba402ea08fc00c8f1500ccef28bbf599aa64ffb1e1d5dc1bf3420a3777badfe697856e9d52070a50c3ea5821c80bef17ca3acffa28f89dd413f096f898")] + +// Used by Moq. +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] +#else +[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests")] +[assembly: InternalsVisibleTo("Benchmarks")] + +// Used by Moq. +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] +#endif diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs/CHANGELOG.md b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs/CHANGELOG.md new file mode 100644 index 00000000000..8c033883b52 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs/CHANGELOG.md @@ -0,0 +1,7 @@ +# Changelog + +## Unreleased + +## 1.0.0-rc8 + +Released 2021-Oct-08 diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs/OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs.csproj b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs/OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs.csproj new file mode 100644 index 00000000000..2a56adc3edf --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs/OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs.csproj @@ -0,0 +1,18 @@ + + + netstandard2.0;netstandard2.1;net461 + OpenTelemetry protocol exporter for OpenTelemetry .NET + $(PackageTags);OTLP + + + + + false + + + + + + + diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs/OtlpLogExporterHelperExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs/OtlpLogExporterHelperExtensions.cs new file mode 100644 index 00000000000..b99be335bac --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs/OtlpLogExporterHelperExtensions.cs @@ -0,0 +1,61 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using OpenTelemetry.Exporter; +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Logs +{ + /// + /// Extension methods to simplify registering of the OpenTelemetry Protocol (OTLP) exporter. + /// + public static class OtlpLogExporterHelperExtensions + { + /// + /// Adds OTLP Exporter as a configuration to the OpenTelemetry ILoggingBuilder. + /// + /// options to use. + /// Exporter configuration options. + /// The instance of to chain the calls. + public static OpenTelemetryLoggerOptions AddOtlpExporter(this OpenTelemetryLoggerOptions loggerOptions, Action configure = null) + { + Guard.ThrowIfNull(loggerOptions); + + return AddOtlpExporter(loggerOptions, new OtlpExporterOptions(), configure); + } + + private static OpenTelemetryLoggerOptions AddOtlpExporter(OpenTelemetryLoggerOptions loggerOptions, OtlpExporterOptions exporterOptions, Action configure = null) + { + configure?.Invoke(exporterOptions); + var otlpExporter = new OtlpLogExporter(exporterOptions); + + if (exporterOptions.ExportProcessorType == ExportProcessorType.Simple) + { + return loggerOptions.AddProcessor(new SimpleLogRecordExportProcessor(otlpExporter)); + } + else + { + return loggerOptions.AddProcessor(new BatchLogRecordExportProcessor( + otlpExporter, + exporterOptions.BatchExportProcessorOptions.MaxQueueSize, + exporterOptions.BatchExportProcessorOptions.ScheduledDelayMilliseconds, + exporterOptions.BatchExportProcessorOptions.ExporterTimeoutMilliseconds, + exporterOptions.BatchExportProcessorOptions.MaxExportBatchSize)); + } + } + } +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs/README.md b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs/README.md new file mode 100644 index 00000000000..9c2c0a286c6 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs/README.md @@ -0,0 +1,11 @@ +# OTLP Logs Exporter for OpenTelemetry .NET + +[![NuGet](https://img.shields.io/nuget/v/OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs.svg)](https://www.nuget.org/packages/OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs) +[![NuGet](https://img.shields.io/nuget/dt/OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs.svg)](https://www.nuget.org/packages/OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs) + +[The OTLP (OpenTelemetry Protocol) exporter](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md) +implementation for logs. + +## Prerequisite + +* [Get OpenTelemetry Collector](https://opentelemetry.io/docs/collector/) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/net46/PublicAPI.Shipped.txt b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/net46/PublicAPI.Shipped.txt deleted file mode 100644 index b457c2d381d..00000000000 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/net46/PublicAPI.Shipped.txt +++ /dev/null @@ -1,18 +0,0 @@ -OpenTelemetry.Exporter.OtlpExporterOptions -OpenTelemetry.Exporter.OtlpExporterOptions.BatchExportProcessorOptions.get -> OpenTelemetry.BatchExportProcessorOptions -OpenTelemetry.Exporter.OtlpExporterOptions.BatchExportProcessorOptions.set -> void -OpenTelemetry.Exporter.OtlpExporterOptions.Endpoint.get -> System.Uri -OpenTelemetry.Exporter.OtlpExporterOptions.Endpoint.set -> void -OpenTelemetry.Exporter.OtlpExporterOptions.ExportProcessorType.get -> OpenTelemetry.ExportProcessorType -OpenTelemetry.Exporter.OtlpExporterOptions.ExportProcessorType.set -> void -OpenTelemetry.Exporter.OtlpExporterOptions.Headers.get -> string -OpenTelemetry.Exporter.OtlpExporterOptions.Headers.set -> void -OpenTelemetry.Exporter.OtlpExporterOptions.OtlpExporterOptions() -> void -OpenTelemetry.Exporter.OtlpExporterOptions.TimeoutMilliseconds.get -> int -OpenTelemetry.Exporter.OtlpExporterOptions.TimeoutMilliseconds.set -> void -OpenTelemetry.Exporter.OtlpTraceExporter -OpenTelemetry.Exporter.OtlpTraceExporter.OtlpTraceExporter(OpenTelemetry.Exporter.OtlpExporterOptions options) -> void -OpenTelemetry.Trace.OtlpTraceExporterHelperExtensions -override OpenTelemetry.Exporter.OtlpTraceExporter.Export(in OpenTelemetry.Batch activityBatch) -> OpenTelemetry.ExportResult -override OpenTelemetry.Exporter.OtlpTraceExporter.OnShutdown(int timeoutMilliseconds) -> bool -static OpenTelemetry.Trace.OtlpTraceExporterHelperExtensions.AddOtlpExporter(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action configure = null) -> OpenTelemetry.Trace.TracerProviderBuilder diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/net461/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/net461/PublicAPI.Unshipped.txt index b457c2d381d..ed28f69db50 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/net461/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/net461/PublicAPI.Unshipped.txt @@ -1,4 +1,6 @@ OpenTelemetry.Exporter.OtlpExporterOptions +OpenTelemetry.Exporter.OtlpExporterOptions.AggregationTemporality.get -> OpenTelemetry.Metrics.AggregationTemporality +OpenTelemetry.Exporter.OtlpExporterOptions.AggregationTemporality.set -> void OpenTelemetry.Exporter.OtlpExporterOptions.BatchExportProcessorOptions.get -> OpenTelemetry.BatchExportProcessorOptions OpenTelemetry.Exporter.OtlpExporterOptions.BatchExportProcessorOptions.set -> void OpenTelemetry.Exporter.OtlpExporterOptions.Endpoint.get -> System.Uri @@ -7,12 +9,29 @@ OpenTelemetry.Exporter.OtlpExporterOptions.ExportProcessorType.get -> OpenTeleme OpenTelemetry.Exporter.OtlpExporterOptions.ExportProcessorType.set -> void OpenTelemetry.Exporter.OtlpExporterOptions.Headers.get -> string OpenTelemetry.Exporter.OtlpExporterOptions.Headers.set -> void +OpenTelemetry.Exporter.OtlpExporterOptions.MetricReaderType.get -> OpenTelemetry.Metrics.MetricReaderType +OpenTelemetry.Exporter.OtlpExporterOptions.MetricReaderType.set -> void +OpenTelemetry.Exporter.OtlpExporterOptions.PeriodicExportingMetricReaderOptions.get -> OpenTelemetry.Metrics.PeriodicExportingMetricReaderOptions +OpenTelemetry.Exporter.OtlpExporterOptions.PeriodicExportingMetricReaderOptions.set -> void +OpenTelemetry.Exporter.OtlpExporterOptions.HttpClientFactory.get -> System.Func +OpenTelemetry.Exporter.OtlpExporterOptions.HttpClientFactory.set -> void OpenTelemetry.Exporter.OtlpExporterOptions.OtlpExporterOptions() -> void +OpenTelemetry.Exporter.OtlpExporterOptions.Protocol.get -> OpenTelemetry.Exporter.OtlpExportProtocol +OpenTelemetry.Exporter.OtlpExporterOptions.Protocol.set -> void OpenTelemetry.Exporter.OtlpExporterOptions.TimeoutMilliseconds.get -> int OpenTelemetry.Exporter.OtlpExporterOptions.TimeoutMilliseconds.set -> void +OpenTelemetry.Exporter.OtlpExportProtocol +OpenTelemetry.Exporter.OtlpExportProtocol.Grpc = 0 -> OpenTelemetry.Exporter.OtlpExportProtocol +OpenTelemetry.Exporter.OtlpExportProtocol.HttpProtobuf = 1 -> OpenTelemetry.Exporter.OtlpExportProtocol +OpenTelemetry.Exporter.OtlpMetricExporter +OpenTelemetry.Exporter.OtlpMetricExporter.OtlpMetricExporter(OpenTelemetry.Exporter.OtlpExporterOptions options) -> void OpenTelemetry.Exporter.OtlpTraceExporter OpenTelemetry.Exporter.OtlpTraceExporter.OtlpTraceExporter(OpenTelemetry.Exporter.OtlpExporterOptions options) -> void +OpenTelemetry.Metrics.OtlpMetricExporterExtensions OpenTelemetry.Trace.OtlpTraceExporterHelperExtensions +override OpenTelemetry.Exporter.OtlpMetricExporter.Export(in OpenTelemetry.Batch metrics) -> OpenTelemetry.ExportResult +override OpenTelemetry.Exporter.OtlpMetricExporter.OnShutdown(int timeoutMilliseconds) -> bool override OpenTelemetry.Exporter.OtlpTraceExporter.Export(in OpenTelemetry.Batch activityBatch) -> OpenTelemetry.ExportResult override OpenTelemetry.Exporter.OtlpTraceExporter.OnShutdown(int timeoutMilliseconds) -> bool +static OpenTelemetry.Metrics.OtlpMetricExporterExtensions.AddOtlpExporter(this OpenTelemetry.Metrics.MeterProviderBuilder builder, System.Action configure = null) -> OpenTelemetry.Metrics.MeterProviderBuilder static OpenTelemetry.Trace.OtlpTraceExporterHelperExtensions.AddOtlpExporter(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action configure = null) -> OpenTelemetry.Trace.TracerProviderBuilder diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/net452/PublicAPI.Shipped.txt b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/net5.0/PublicAPI.Shipped.txt similarity index 100% rename from src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/net452/PublicAPI.Shipped.txt rename to src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/net5.0/PublicAPI.Shipped.txt diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/net5.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/net5.0/PublicAPI.Unshipped.txt new file mode 100644 index 00000000000..f59367caac1 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/net5.0/PublicAPI.Unshipped.txt @@ -0,0 +1,19 @@ +OpenTelemetry.Exporter.OtlpExporterOptions.AggregationTemporality.get -> OpenTelemetry.Metrics.AggregationTemporality +OpenTelemetry.Exporter.OtlpExporterOptions.AggregationTemporality.set -> void +OpenTelemetry.Exporter.OtlpExporterOptions.MetricReaderType.get -> OpenTelemetry.Metrics.MetricReaderType +OpenTelemetry.Exporter.OtlpExporterOptions.MetricReaderType.set -> void +OpenTelemetry.Exporter.OtlpExporterOptions.PeriodicExportingMetricReaderOptions.get -> OpenTelemetry.Metrics.PeriodicExportingMetricReaderOptions +OpenTelemetry.Exporter.OtlpExporterOptions.PeriodicExportingMetricReaderOptions.set -> void +OpenTelemetry.Exporter.OtlpExporterOptions.HttpClientFactory.get -> System.Func +OpenTelemetry.Exporter.OtlpExporterOptions.HttpClientFactory.set -> void +OpenTelemetry.Exporter.OtlpExporterOptions.Protocol.get -> OpenTelemetry.Exporter.OtlpExportProtocol +OpenTelemetry.Exporter.OtlpExporterOptions.Protocol.set -> void +OpenTelemetry.Exporter.OtlpExportProtocol +OpenTelemetry.Exporter.OtlpExportProtocol.Grpc = 0 -> OpenTelemetry.Exporter.OtlpExportProtocol +OpenTelemetry.Exporter.OtlpExportProtocol.HttpProtobuf = 1 -> OpenTelemetry.Exporter.OtlpExportProtocol +OpenTelemetry.Exporter.OtlpMetricExporter +OpenTelemetry.Exporter.OtlpMetricExporter.OtlpMetricExporter(OpenTelemetry.Exporter.OtlpExporterOptions options) -> void +OpenTelemetry.Metrics.OtlpMetricExporterExtensions +override OpenTelemetry.Exporter.OtlpMetricExporter.Export(in OpenTelemetry.Batch metrics) -> OpenTelemetry.ExportResult +override OpenTelemetry.Exporter.OtlpMetricExporter.OnShutdown(int timeoutMilliseconds) -> bool +static OpenTelemetry.Metrics.OtlpMetricExporterExtensions.AddOtlpExporter(this OpenTelemetry.Metrics.MeterProviderBuilder builder, System.Action configure = null) -> OpenTelemetry.Metrics.MeterProviderBuilder diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt index e69de29bb2d..f59367caac1 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt @@ -0,0 +1,19 @@ +OpenTelemetry.Exporter.OtlpExporterOptions.AggregationTemporality.get -> OpenTelemetry.Metrics.AggregationTemporality +OpenTelemetry.Exporter.OtlpExporterOptions.AggregationTemporality.set -> void +OpenTelemetry.Exporter.OtlpExporterOptions.MetricReaderType.get -> OpenTelemetry.Metrics.MetricReaderType +OpenTelemetry.Exporter.OtlpExporterOptions.MetricReaderType.set -> void +OpenTelemetry.Exporter.OtlpExporterOptions.PeriodicExportingMetricReaderOptions.get -> OpenTelemetry.Metrics.PeriodicExportingMetricReaderOptions +OpenTelemetry.Exporter.OtlpExporterOptions.PeriodicExportingMetricReaderOptions.set -> void +OpenTelemetry.Exporter.OtlpExporterOptions.HttpClientFactory.get -> System.Func +OpenTelemetry.Exporter.OtlpExporterOptions.HttpClientFactory.set -> void +OpenTelemetry.Exporter.OtlpExporterOptions.Protocol.get -> OpenTelemetry.Exporter.OtlpExportProtocol +OpenTelemetry.Exporter.OtlpExporterOptions.Protocol.set -> void +OpenTelemetry.Exporter.OtlpExportProtocol +OpenTelemetry.Exporter.OtlpExportProtocol.Grpc = 0 -> OpenTelemetry.Exporter.OtlpExportProtocol +OpenTelemetry.Exporter.OtlpExportProtocol.HttpProtobuf = 1 -> OpenTelemetry.Exporter.OtlpExportProtocol +OpenTelemetry.Exporter.OtlpMetricExporter +OpenTelemetry.Exporter.OtlpMetricExporter.OtlpMetricExporter(OpenTelemetry.Exporter.OtlpExporterOptions options) -> void +OpenTelemetry.Metrics.OtlpMetricExporterExtensions +override OpenTelemetry.Exporter.OtlpMetricExporter.Export(in OpenTelemetry.Batch metrics) -> OpenTelemetry.ExportResult +override OpenTelemetry.Exporter.OtlpMetricExporter.OnShutdown(int timeoutMilliseconds) -> bool +static OpenTelemetry.Metrics.OtlpMetricExporterExtensions.AddOtlpExporter(this OpenTelemetry.Metrics.MeterProviderBuilder builder, System.Action configure = null) -> OpenTelemetry.Metrics.MeterProviderBuilder diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt index e69de29bb2d..f59367caac1 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt @@ -0,0 +1,19 @@ +OpenTelemetry.Exporter.OtlpExporterOptions.AggregationTemporality.get -> OpenTelemetry.Metrics.AggregationTemporality +OpenTelemetry.Exporter.OtlpExporterOptions.AggregationTemporality.set -> void +OpenTelemetry.Exporter.OtlpExporterOptions.MetricReaderType.get -> OpenTelemetry.Metrics.MetricReaderType +OpenTelemetry.Exporter.OtlpExporterOptions.MetricReaderType.set -> void +OpenTelemetry.Exporter.OtlpExporterOptions.PeriodicExportingMetricReaderOptions.get -> OpenTelemetry.Metrics.PeriodicExportingMetricReaderOptions +OpenTelemetry.Exporter.OtlpExporterOptions.PeriodicExportingMetricReaderOptions.set -> void +OpenTelemetry.Exporter.OtlpExporterOptions.HttpClientFactory.get -> System.Func +OpenTelemetry.Exporter.OtlpExporterOptions.HttpClientFactory.set -> void +OpenTelemetry.Exporter.OtlpExporterOptions.Protocol.get -> OpenTelemetry.Exporter.OtlpExportProtocol +OpenTelemetry.Exporter.OtlpExporterOptions.Protocol.set -> void +OpenTelemetry.Exporter.OtlpExportProtocol +OpenTelemetry.Exporter.OtlpExportProtocol.Grpc = 0 -> OpenTelemetry.Exporter.OtlpExportProtocol +OpenTelemetry.Exporter.OtlpExportProtocol.HttpProtobuf = 1 -> OpenTelemetry.Exporter.OtlpExportProtocol +OpenTelemetry.Exporter.OtlpMetricExporter +OpenTelemetry.Exporter.OtlpMetricExporter.OtlpMetricExporter(OpenTelemetry.Exporter.OtlpExporterOptions options) -> void +OpenTelemetry.Metrics.OtlpMetricExporterExtensions +override OpenTelemetry.Exporter.OtlpMetricExporter.Export(in OpenTelemetry.Batch metrics) -> OpenTelemetry.ExportResult +override OpenTelemetry.Exporter.OtlpMetricExporter.OnShutdown(int timeoutMilliseconds) -> bool +static OpenTelemetry.Metrics.OtlpMetricExporterExtensions.AddOtlpExporter(this OpenTelemetry.Metrics.MeterProviderBuilder builder, System.Action configure = null) -> OpenTelemetry.Metrics.MeterProviderBuilder diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/AssemblyInfo.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/AssemblyInfo.cs index 8e90458d1fc..46798ca4e77 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/AssemblyInfo.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/AssemblyInfo.cs @@ -19,6 +19,7 @@ #if SIGNED [assembly: InternalsVisibleTo("OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010051c1562a090fb0c9f391012a32198b5e5d9a60e9b80fa2d7b434c9e5ccb7259bd606e66f9660676afc6692b8cdc6793d190904551d2103b7b22fa636dcbb8208839785ba402ea08fc00c8f1500ccef28bbf599aa64ffb1e1d5dc1bf3420a3777badfe697856e9d52070a50c3ea5821c80bef17ca3acffa28f89dd413f096f898")] [assembly: InternalsVisibleTo("Benchmarks, PublicKey=002400000480000094000000060200000024000052534131000400000100010051c1562a090fb0c9f391012a32198b5e5d9a60e9b80fa2d7b434c9e5ccb7259bd606e66f9660676afc6692b8cdc6793d190904551d2103b7b22fa636dcbb8208839785ba402ea08fc00c8f1500ccef28bbf599aa64ffb1e1d5dc1bf3420a3777badfe697856e9d52070a50c3ea5821c80bef17ca3acffa28f89dd413f096f898")] +[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs, PublicKey=002400000480000094000000060200000024000052534131000400000100010051c1562a090fb0c9f391012a32198b5e5d9a60e9b80fa2d7b434c9e5ccb7259bd606e66f9660676afc6692b8cdc6793d190904551d2103b7b22fa636dcbb8208839785ba402ea08fc00c8f1500ccef28bbf599aa64ffb1e1d5dc1bf3420a3777badfe697856e9d52070a50c3ea5821c80bef17ca3acffa28f89dd413f096f898")] // Used by Moq. [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/BaseOtlpExporter.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/BaseOtlpExporter.cs deleted file mode 100644 index aad3503b2a2..00000000000 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/BaseOtlpExporter.cs +++ /dev/null @@ -1,83 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -using System; -using System.Threading.Tasks; -using Grpc.Core; -using OpenTelemetry.Exporter.OpenTelemetryProtocol; -#if NETSTANDARD2_1 -using Grpc.Net.Client; -#endif -using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; -using OtlpResource = Opentelemetry.Proto.Resource.V1; - -namespace OpenTelemetry.Exporter -{ - /// - /// Implements exporter that exports telemetry objects over OTLP/gRPC. - /// - /// The type of telemetry object to be exported. - public abstract class BaseOtlpExporter : BaseExporter - where T : class - { - private OtlpResource.Resource processResource; - - /// - /// Initializes a new instance of the class. - /// - /// The for configuring the exporter. - protected BaseOtlpExporter(OtlpExporterOptions options) - { - this.Options = options ?? throw new ArgumentNullException(nameof(options)); - this.Headers = options.GetMetadataFromHeaders(); - if (this.Options.TimeoutMilliseconds <= 0) - { - throw new ArgumentException("Timeout value provided is not a positive number.", nameof(this.Options.TimeoutMilliseconds)); - } - } - - internal OtlpResource.Resource ProcessResource => this.processResource ??= this.ParentProvider.GetResource().ToOtlpResource(); - -#if NETSTANDARD2_1 - internal GrpcChannel Channel { get; set; } -#else - internal Channel Channel { get; set; } -#endif - - internal OtlpExporterOptions Options { get; } - - internal Metadata Headers { get; } - - /// - protected override bool OnShutdown(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/CHANGELOG.md b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md index c464714f603..373e0bcda63 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/CHANGELOG.md @@ -1,13 +1,83 @@ # Changelog +* Changed `OtlpLogExporter` to convert `ILogger` structured log inputs to + `Attributes` in OpenTelemetry (only active when `ParseStateValues` is `true` + on `OpenTelemetryLoggerOptions`) + ## Unreleased +* Added validation that insecure channel is configured correctly when using + .NET Core 3.x for gRPC-based exporting. + ([#2691](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2691)) + +## 1.2.0-rc1 + +Released 2021-Nov-29 + +* Added configuration options for `MetricReaderType` to allow for configuring + the `OtlpMetricExporter` to export either manually or periodically. + ([#2674](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2674)) + +* The internal log message used when OTLP export client connection failure occurs, + will now include the endpoint uri as well. + ([#2686](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2686)) + +* Support `HttpProtobuf` protocol with metrics & added `HttpClientFactory` + option + ([#2696](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2696)) + +## 1.2.0-beta2 + +Released 2021-Nov-19 + +* Changed `OtlpExporterOptions` constructor to throw + `FormatException` if it fails to parse any of the supported environment + variables. + +* Changed `OtlpExporterOptions.MetricExportIntervalMilliseconds` to default + 60000 milliseconds. + ([#2641](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2641)) + +## 1.2.0-beta1 + +Released 2021-Oct-08 + +* `MeterProviderBuilder` extension methods now support `OtlpExporterOptions` + bound to `IConfiguration` when using OpenTelemetry.Extensions.Hosting + ([#2413](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2413)) +* Extended `OtlpExporterOptions` by `Protocol` property. The property can be + overridden by `OTEL_EXPORTER_OTLP_PROTOCOL` environmental variable (grpc or http/protobuf). + Implemented OTLP over HTTP binary protobuf trace exporter. + ([#2292](https://github.com/open-telemetry/opentelemetry-dotnet/issues/2292)) + +## 1.2.0-alpha4 + +Released 2021-Sep-23 + +## 1.2.0-alpha3 + +Released 2021-Sep-13 + +* `OtlpExporterOptions.BatchExportProcessorOptions` is initialized with + `BatchExportActivityProcessorOptions` which supports field value overriding + using `OTEL_BSP_SCHEDULE_DELAY`, `OTEL_BSP_EXPORT_TIMEOUT`, + `OTEL_BSP_MAX_QUEUE_SIZE`, `OTEL_BSP_MAX_EXPORT_BATCH_SIZE` + environmental variables as defined in the + [specification](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.5.0/specification/sdk-environment-variables.md#batch-span-processor). + ([#2219](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2219)) + +## 1.2.0-alpha2 + +Released 2021-Aug-24 + * The `OtlpExporterOptions` defaults can be overridden using `OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_EXPORTER_OTLP_HEADERS` and `OTEL_EXPORTER_OTLP_TIMEOUT` - envionmental variables as defined in the + environmental variables as defined in the [specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md). ([#2188](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2188)) +* Changed default temporality for Metrics to be cumulative. + ## 1.2.0-alpha1 Released 2021-Jul-23 diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/BaseOtlpGrpcExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/BaseOtlpGrpcExportClient.cs new file mode 100644 index 00000000000..69c0b03e749 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/BaseOtlpGrpcExportClient.cs @@ -0,0 +1,74 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Threading; +using System.Threading.Tasks; +using Grpc.Core; +using OpenTelemetry.Internal; +#if NETSTANDARD2_1 || NET5_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 BaseOtlpGrpcExportClient(OtlpExporterOptions options) + { + Guard.ThrowIfNull(options, nameof(options)); + Guard.ThrowIfInvalidTimeout(options.TimeoutMilliseconds, nameof(options.TimeoutMilliseconds)); + + ExporterClientValidation.EnsureUnencryptedSupportIsEnabled(options); + + this.Options = options; + this.Headers = options.GetMetadataFromHeaders(); + } + + internal OtlpExporterOptions Options { get; } + +#if NETSTANDARD2_1 || NET5_0_OR_GREATER + internal GrpcChannel Channel { get; set; } +#else + internal Channel Channel { get; set; } +#endif + + internal Metadata Headers { get; } + + /// + public abstract bool SendExportRequest(TRequest request, 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 new file mode 100644 index 00000000000..3b46f9728c4 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/BaseOtlpHttpExportClient.cs @@ -0,0 +1,84 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +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 + { + protected BaseOtlpHttpExportClient(OtlpExporterOptions options, HttpClient httpClient) + { + Guard.ThrowIfNull(options, nameof(options)); + Guard.ThrowIfNull(httpClient, nameof(httpClient)); + Guard.ThrowIfInvalidTimeout(options.TimeoutMilliseconds, $"{nameof(options)}.{nameof(options.TimeoutMilliseconds)}"); + + this.Options = options; + this.Headers = options.GetHeaders>((d, k, v) => d.Add(k, v)); + this.HttpClient = httpClient; + } + + internal OtlpExporterOptions Options { get; } + + internal HttpClient HttpClient { get; } + + internal IReadOnlyDictionary Headers { get; } + + /// + public bool SendExportRequest(TRequest request, CancellationToken cancellationToken = default) + { + try + { + using var httpRequest = this.CreateHttpRequest(request); + + using var httpResponse = this.SendHttpRequest(httpRequest, cancellationToken); + + httpResponse?.EnsureSuccessStatusCode(); + } + catch (HttpRequestException ex) + { + OpenTelemetryProtocolExporterEventSource.Log.FailedToReachCollector(this.Options.Endpoint, ex); + + return false; + } + + return true; + } + + /// + public bool Shutdown(int timeoutMilliseconds) + { + this.HttpClient.CancelPendingRequests(); + return true; + } + + protected abstract HttpRequestMessage CreateHttpRequest(TRequest request); + + protected HttpResponseMessage SendHttpRequest(HttpRequestMessage request, CancellationToken cancellationToken) + { +#if NET5_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/ExporterClientValidation.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/ExporterClientValidation.cs new file mode 100644 index 00000000000..468fd0402a3 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/ExporterClientValidation.cs @@ -0,0 +1,45 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient +{ + internal static class ExporterClientValidation + { + internal static void EnsureUnencryptedSupportIsEnabled(OtlpExporterOptions options) + { + var version = System.Environment.Version; + + // This verification is only required for .NET Core 3.x + if (version.Major != 3) + { + return; + } + + if (options.Endpoint.Scheme.Equals("http", StringComparison.InvariantCultureIgnoreCase)) + { + if (AppContext.TryGetSwitch( + "System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", out var unencryptedIsSupported) == false + || unencryptedIsSupported == false) + { + throw new InvalidOperationException( + "Calling insecure gRPC services on .NET Core 3.x requires enabling the 'System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport' switch. See: https://docs.microsoft.com/aspnet/core/grpc/troubleshoot#call-insecure-grpc-services-with-net-core-client"); + } + } + } + } +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/IExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/IExportClient.cs new file mode 100644 index 00000000000..61d85bacdad --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/IExportClient.cs @@ -0,0 +1,45 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Threading; + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient +{ + /// Export client interface. + /// Type of export request. + internal interface IExportClient + { + /// + /// Method for sending export request to the server. + /// + /// The request to send to the server. + /// An optional token for canceling the call. + /// True if the request has been sent successfully, otherwise false. + bool SendExportRequest(TRequest request, CancellationToken cancellationToken = default); + + /// + /// Method for shutting down the export client. + /// + /// + /// The number of milliseconds to wait, or Timeout.Infinite to + /// wait indefinitely. + /// + /// + /// Returns true if shutdown succeeded; otherwise, false. + /// + bool Shutdown(int timeoutMilliseconds); + } +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcLogExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcLogExportClient.cs new file mode 100644 index 00000000000..2e50d821e9c --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcLogExportClient.cs @@ -0,0 +1,62 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Threading; +using Grpc.Core; +using OtlpCollector = Opentelemetry.Proto.Collector.Logs.V1; + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient +{ + /// Class for sending OTLP metrics export request over gRPC. + internal sealed class OtlpGrpcLogExportClient : BaseOtlpGrpcExportClient + { + private readonly OtlpCollector.LogsService.ILogsServiceClient logsClient; + + public OtlpGrpcLogExportClient(OtlpExporterOptions options, OtlpCollector.LogsService.ILogsServiceClient 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 bool SendExportRequest(OtlpCollector.ExportLogsServiceRequest request, CancellationToken cancellationToken = default) + { + var deadline = DateTime.UtcNow.AddMilliseconds(this.Options.TimeoutMilliseconds); + + try + { + this.logsClient.Export(request, headers: this.Headers, deadline: deadline); + } + catch (RpcException ex) + { + OpenTelemetryProtocolExporterEventSource.Log.FailedToReachCollector(this.Options.Endpoint, ex); + + return false; + } + + return true; + } + } +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcMetricsExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcMetricsExportClient.cs new file mode 100644 index 00000000000..0086bcdd402 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcMetricsExportClient.cs @@ -0,0 +1,62 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Threading; +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.IMetricsServiceClient metricsClient; + + public OtlpGrpcMetricsExportClient(OtlpExporterOptions options, OtlpCollector.MetricsService.IMetricsServiceClient 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 bool SendExportRequest(OtlpCollector.ExportMetricsServiceRequest request, CancellationToken cancellationToken = default) + { + var deadline = DateTime.UtcNow.AddMilliseconds(this.Options.TimeoutMilliseconds); + + try + { + this.metricsClient.Export(request, headers: this.Headers, deadline: deadline); + } + catch (RpcException ex) + { + OpenTelemetryProtocolExporterEventSource.Log.FailedToReachCollector(this.Options.Endpoint, ex); + + return false; + } + + return true; + } + } +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcTraceExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcTraceExportClient.cs new file mode 100644 index 00000000000..e08abedf2c6 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpGrpcTraceExportClient.cs @@ -0,0 +1,62 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Threading; +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.ITraceServiceClient traceClient; + + public OtlpGrpcTraceExportClient(OtlpExporterOptions options, OtlpCollector.TraceService.ITraceServiceClient 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 bool SendExportRequest(OtlpCollector.ExportTraceServiceRequest request, CancellationToken cancellationToken = default) + { + var deadline = DateTime.UtcNow.AddMilliseconds(this.Options.TimeoutMilliseconds); + + try + { + this.traceClient.Export(request, headers: this.Headers, deadline: deadline); + } + catch (RpcException ex) + { + OpenTelemetryProtocolExporterEventSource.Log.FailedToReachCollector(this.Options.Endpoint, ex); + + return false; + } + + return true; + } + } +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpHttpMetricsExportClient.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpHttpMetricsExportClient.cs new file mode 100644 index 00000000000..e8652d87de2 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpHttpMetricsExportClient.cs @@ -0,0 +1,96 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Runtime.CompilerServices; +#if NET5_0_OR_GREATER +using System.Threading; +#endif +using System.Threading.Tasks; +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 readonly Uri exportMetricsUri; + + public OtlpHttpMetricsExportClient(OtlpExporterOptions options, HttpClient httpClient) + : base(options, httpClient) + { + this.exportMetricsUri = this.Options.Endpoint; + } + + protected override HttpRequestMessage CreateHttpRequest(OtlpCollector.ExportMetricsServiceRequest exportRequest) + { + var request = new HttpRequestMessage(HttpMethod.Post, this.exportMetricsUri); + foreach (var header in this.Headers) + { + request.Headers.Add(header.Key, header.Value); + } + + request.Content = new ExportRequestContent(exportRequest); + + return request; + } + + internal sealed class ExportRequestContent : HttpContent + { + private static readonly MediaTypeHeaderValue ProtobufMediaTypeHeader = new MediaTypeHeaderValue(MediaContentType); + + private readonly OtlpCollector.ExportMetricsServiceRequest exportRequest; + + public ExportRequestContent(OtlpCollector.ExportMetricsServiceRequest exportRequest) + { + this.exportRequest = exportRequest; + this.Headers.ContentType = ProtobufMediaTypeHeader; + } + +#if NET5_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 new file mode 100644 index 00000000000..44cb6a00f28 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/ExportClient/OtlpHttpTraceExportClient.cs @@ -0,0 +1,96 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Runtime.CompilerServices; +#if NET5_0_OR_GREATER +using System.Threading; +#endif +using System.Threading.Tasks; +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 readonly Uri exportTracesUri; + + public OtlpHttpTraceExportClient(OtlpExporterOptions options, HttpClient httpClient) + : base(options, httpClient) + { + this.exportTracesUri = this.Options.Endpoint; + } + + protected override HttpRequestMessage CreateHttpRequest(OtlpCollector.ExportTraceServiceRequest exportRequest) + { + var request = new HttpRequestMessage(HttpMethod.Post, this.exportTracesUri); + foreach (var header in this.Headers) + { + request.Headers.Add(header.Key, header.Value); + } + + request.Content = new ExportRequestContent(exportRequest); + + return request; + } + + internal sealed class ExportRequestContent : HttpContent + { + private static readonly MediaTypeHeaderValue ProtobufMediaTypeHeader = new MediaTypeHeaderValue(MediaContentType); + + private readonly OtlpCollector.ExportTraceServiceRequest exportRequest; + + public ExportRequestContent(OtlpCollector.ExportTraceServiceRequest exportRequest) + { + this.exportRequest = exportRequest; + this.Headers.ContentType = ProtobufMediaTypeHeader; + } + +#if NET5_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/LogRecordExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/LogRecordExtensions.cs new file mode 100644 index 00000000000..0b60eeabce4 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/LogRecordExtensions.cs @@ -0,0 +1,138 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Runtime.CompilerServices; +using Google.Protobuf; +using Google.Protobuf.Collections; +using OpenTelemetry.Internal; +using OpenTelemetry.Logs; +using OpenTelemetry.Trace; +using OtlpCollector = Opentelemetry.Proto.Collector.Logs.V1; +using OtlpCommon = Opentelemetry.Proto.Common.V1; +using OtlpLogs = Opentelemetry.Proto.Logs.V1; +using OtlpResource = Opentelemetry.Proto.Resource.V1; + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation +{ + internal static class LogRecordExtensions + { + internal static void AddBatch( + this OtlpCollector.ExportLogsServiceRequest request, + OtlpResource.Resource processResource, + in Batch logRecordBatch) + { + OtlpLogs.ResourceLogs resourceLogs = new OtlpLogs.ResourceLogs + { + Resource = processResource, + }; + request.ResourceLogs.Add(resourceLogs); + + var instrumentationLibraryLogs = new OtlpLogs.InstrumentationLibraryLogs(); + resourceLogs.InstrumentationLibraryLogs.Add(instrumentationLibraryLogs); + + foreach (var item in logRecordBatch) + { + var logRecord = item.ToOtlpLog(); + if (logRecord == null) + { + OpenTelemetryProtocolExporterEventSource.Log.CouldNotTranslateLogRecord( + nameof(LogRecordExtensions), + nameof(AddBatch)); + continue; + } + + instrumentationLibraryLogs.Logs.Add(logRecord); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static OtlpLogs.LogRecord ToOtlpLog(this LogRecord logRecord) + { + var otlpLogRecord = new OtlpLogs.LogRecord + { + TimeUnixNano = (ulong)logRecord.Timestamp.ToUnixTimeNanoseconds(), + Name = logRecord.CategoryName, + + // TODO: Devise mapping of LogLevel to SeverityNumber + // See: https://github.com/open-telemetry/opentelemetry-proto/blob/bacfe08d84e21fb2a779e302d12e8dfeb67e7b86/opentelemetry/proto/logs/v1/logs.proto#L100-L102 + SeverityText = logRecord.LogLevel.ToString(), + }; + + if (logRecord.FormattedMessage != null) + { + otlpLogRecord.Body = new OtlpCommon.AnyValue { StringValue = logRecord.FormattedMessage }; + } + + if (logRecord.StateValues != null) + { + foreach (var stateValue in logRecord.StateValues) + { + var otlpAttribute = stateValue.ToOtlpAttribute(); + otlpLogRecord.Attributes.Add(otlpAttribute); + } + } + + if (logRecord.EventId != default) + { + otlpLogRecord.Attributes.AddIntAttribute(nameof(logRecord.EventId.Id), logRecord.EventId.Id); + otlpLogRecord.Attributes.AddStringAttribute(nameof(logRecord.EventId.Name), logRecord.EventId.Name); + } + + if (logRecord.Exception != null) + { + otlpLogRecord.Attributes.AddStringAttribute(SemanticConventions.AttributeExceptionType, logRecord.Exception.GetType().Name); + otlpLogRecord.Attributes.AddStringAttribute(SemanticConventions.AttributeExceptionMessage, logRecord.Exception.Message); + otlpLogRecord.Attributes.AddStringAttribute(SemanticConventions.AttributeExceptionStacktrace, logRecord.Exception.ToInvariantString()); + } + + 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; + } + + // TODO: Add additional attributes from scope and state + // Might make sense to take an approach similar to https://github.com/open-telemetry/opentelemetry-dotnet-contrib/blob/897b734aa5ea9992538f04f6ea6871fe211fa903/src/OpenTelemetry.Contrib.Preview/Internal/DefaultLogStateConverter.cs + + return otlpLogRecord; + } + + private static void AddStringAttribute(this RepeatedField repeatedField, string key, string value) + { + repeatedField.Add(new OtlpCommon.KeyValue + { + Key = key, + Value = new OtlpCommon.AnyValue { StringValue = value }, + }); + } + + private static void AddIntAttribute(this RepeatedField repeatedField, string key, int value) + { + repeatedField.Add(new OtlpCommon.KeyValue + { + Key = key, + Value = new OtlpCommon.AnyValue { IntValue = value }, + }); + } + } +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/LogsService.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/LogsService.cs new file mode 100644 index 00000000000..7355c0153a4 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/LogsService.cs @@ -0,0 +1,50 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Threading; +using Grpc.Core; + +namespace Opentelemetry.Proto.Collector.Logs.V1 +{ + /// + /// LogService extensions. + /// + internal static partial class LogsService + { + /// Interface for LogService. + public interface ILogsServiceClient + { + /// + /// For performance reasons, it is recommended to keep this RPC + /// alive for the entire life of the application. + /// + /// The request to send to the server. + /// The initial metadata to send with the call. This parameter is optional. + /// An optional deadline for the call. The call will be cancelled if deadline is hit. + /// An optional token for canceling the call. + /// The response received from the server. + ExportLogsServiceResponse Export(ExportLogsServiceRequest request, Metadata headers = null, DateTime? deadline = null, CancellationToken cancellationToken = default); + } + + /// + /// LogsServiceClient extensions. + /// + public partial class LogsServiceClient : ILogsServiceClient + { + } + } +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs index f2bd7010b16..eafacb2f979 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/MetricItemExtensions.cs @@ -21,7 +21,6 @@ using System.Reflection; using System.Reflection.Emit; using System.Runtime.CompilerServices; -using Google.Protobuf; using Google.Protobuf.Collections; using OpenTelemetry.Metrics; using OtlpCollector = Opentelemetry.Proto.Collector.Metrics.V1; @@ -36,10 +35,10 @@ internal static class MetricItemExtensions private static readonly ConcurrentBag MetricListPool = new ConcurrentBag(); private static readonly Action, int> RepeatedFieldOfMetricSetCountAction = CreateRepeatedFieldOfMetricSetCountAction(); - internal static void AddBatch( + internal static void AddMetrics( this OtlpCollector.ExportMetricsServiceRequest request, OtlpResource.Resource processResource, - in Batch batch) + in Batch metrics) { var metricsByLibrary = new Dictionary(); var resourceMetrics = new OtlpMetrics.ResourceMetrics @@ -48,30 +47,29 @@ internal static void AddBatch( }; request.ResourceMetrics.Add(resourceMetrics); - foreach (var metricItem in batch) + foreach (var metric in metrics) { - foreach (var metric in metricItem.Metrics) - { - var otlpMetric = metric.ToOtlpMetric(); - if (otlpMetric == null) - { - OpenTelemetryProtocolExporterEventSource.Log.CouldNotTranslateMetric( - nameof(MetricItemExtensions), - nameof(AddBatch)); - continue; - } + var otlpMetric = metric.ToOtlpMetric(); - var meterName = metric.Meter.Name; - if (!metricsByLibrary.TryGetValue(meterName, out var metrics)) - { - metrics = GetMetricListFromPool(meterName, metric.Meter.Version); + // TODO: Replace null check with exception handling. + if (otlpMetric == null) + { + OpenTelemetryProtocolExporterEventSource.Log.CouldNotTranslateMetric( + nameof(MetricItemExtensions), + nameof(AddMetrics)); + continue; + } - metricsByLibrary.Add(meterName, metrics); - resourceMetrics.InstrumentationLibraryMetrics.Add(metrics); - } + var meterName = metric.Meter.Name; + if (!metricsByLibrary.TryGetValue(meterName, out var instrumentationLibraryMetrics)) + { + instrumentationLibraryMetrics = GetMetricListFromPool(meterName, metric.Meter.Version); - metrics.Metrics.Add(otlpMetric); + metricsByLibrary.Add(meterName, instrumentationLibraryMetrics); + resourceMetrics.InstrumentationLibraryMetrics.Add(instrumentationLibraryMetrics); } + + instrumentationLibraryMetrics.Metrics.Add(otlpMetric); } } @@ -115,7 +113,7 @@ internal static OtlpMetrics.InstrumentationLibraryMetrics GetMetricListFromPool( } [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static OtlpMetrics.Metric ToOtlpMetric(this IMetric metric) + internal static OtlpMetrics.Metric ToOtlpMetric(this Metric metric) { var otlpMetric = new OtlpMetrics.Metric { @@ -132,133 +130,138 @@ internal static OtlpMetrics.Metric ToOtlpMetric(this IMetric metric) 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: { - var sumMetric = metric as ISumMetricLong; - var sum = new OtlpMetrics.Sum + var sum = new OtlpMetrics.Sum(); + sum.IsMonotonic = true; + sum.AggregationTemporality = temporality; + + foreach (ref readonly var metricPoint in metric.GetMetricPoints()) { - IsMonotonic = sumMetric.IsMonotonic, - AggregationTemporality = sumMetric.IsDeltaTemporality - ? OtlpMetrics.AggregationTemporality.Delta - : OtlpMetrics.AggregationTemporality.Cumulative, - }; - var dataPoint = metric.ToNumberDataPoint(sumMetric.LongSum, sumMetric.Exemplars); - sum.DataPoints.Add(dataPoint); + var dataPoint = new OtlpMetrics.NumberDataPoint + { + StartTimeUnixNano = (ulong)metricPoint.StartTime.ToUnixTimeNanoseconds(), + TimeUnixNano = (ulong)metricPoint.EndTime.ToUnixTimeNanoseconds(), + }; + + AddAttributes(metricPoint.Tags, dataPoint.Attributes); + + dataPoint.AsInt = metricPoint.GetSumLong(); + sum.DataPoints.Add(dataPoint); + } + otlpMetric.Sum = sum; break; } case MetricType.DoubleSum: { - var sumMetric = metric as ISumMetricDouble; - var sum = new OtlpMetrics.Sum + var sum = new OtlpMetrics.Sum(); + sum.IsMonotonic = true; + sum.AggregationTemporality = temporality; + + foreach (ref readonly var metricPoint in metric.GetMetricPoints()) { - IsMonotonic = sumMetric.IsMonotonic, - AggregationTemporality = sumMetric.IsDeltaTemporality - ? OtlpMetrics.AggregationTemporality.Delta - : OtlpMetrics.AggregationTemporality.Cumulative, - }; - var dataPoint = metric.ToNumberDataPoint(sumMetric.DoubleSum, sumMetric.Exemplars); - sum.DataPoints.Add(dataPoint); + var dataPoint = new OtlpMetrics.NumberDataPoint + { + StartTimeUnixNano = (ulong)metricPoint.StartTime.ToUnixTimeNanoseconds(), + TimeUnixNano = (ulong)metricPoint.EndTime.ToUnixTimeNanoseconds(), + }; + + AddAttributes(metricPoint.Tags, dataPoint.Attributes); + + dataPoint.AsDouble = metricPoint.GetSumDouble(); + sum.DataPoints.Add(dataPoint); + } + otlpMetric.Sum = sum; break; } case MetricType.LongGauge: { - var gaugeMetric = metric as IGaugeMetric; var gauge = new OtlpMetrics.Gauge(); - var dataPoint = metric.ToNumberDataPoint(gaugeMetric.LastValue.Value, gaugeMetric.Exemplars); - gauge.DataPoints.Add(dataPoint); + foreach (ref readonly var metricPoint in metric.GetMetricPoints()) + { + var dataPoint = new OtlpMetrics.NumberDataPoint + { + StartTimeUnixNano = (ulong)metricPoint.StartTime.ToUnixTimeNanoseconds(), + TimeUnixNano = (ulong)metricPoint.EndTime.ToUnixTimeNanoseconds(), + }; + + AddAttributes(metricPoint.Tags, dataPoint.Attributes); + + dataPoint.AsInt = metricPoint.GetGaugeLastValueLong(); + gauge.DataPoints.Add(dataPoint); + } + otlpMetric.Gauge = gauge; break; } case MetricType.DoubleGauge: { - var gaugeMetric = metric as IGaugeMetric; var gauge = new OtlpMetrics.Gauge(); - var dataPoint = metric.ToNumberDataPoint(gaugeMetric.LastValue.Value, gaugeMetric.Exemplars); - gauge.DataPoints.Add(dataPoint); - otlpMetric.Gauge = gauge; - break; - } - - case MetricType.Histogram: - { - var histogramMetric = metric as IHistogramMetric; - var histogram = new OtlpMetrics.Histogram - { - AggregationTemporality = histogramMetric.IsDeltaTemporality - ? OtlpMetrics.AggregationTemporality.Delta - : OtlpMetrics.AggregationTemporality.Cumulative, - }; - - var dataPoint = new OtlpMetrics.HistogramDataPoint - { - StartTimeUnixNano = (ulong)metric.StartTimeExclusive.ToUnixTimeNanoseconds(), - TimeUnixNano = (ulong)metric.EndTimeInclusive.ToUnixTimeNanoseconds(), - Count = (ulong)histogramMetric.PopulationCount, - Sum = histogramMetric.PopulationSum, - }; - - foreach (var bucket in histogramMetric.Buckets) + foreach (ref readonly var metricPoint in metric.GetMetricPoints()) { - dataPoint.BucketCounts.Add((ulong)bucket.Count); - - // TODO: Verify how to handle the bounds. We've modeled things with - // a LowBoundary and HighBoundary. OTLP data model has modeled this - // differently: https://github.com/open-telemetry/opentelemetry-proto/blob/bacfe08d84e21fb2a779e302d12e8dfeb67e7b86/opentelemetry/proto/metrics/v1/metrics.proto#L554-L568 - dataPoint.ExplicitBounds.Add(bucket.HighBoundary); - } + var dataPoint = new OtlpMetrics.NumberDataPoint + { + StartTimeUnixNano = (ulong)metricPoint.StartTime.ToUnixTimeNanoseconds(), + TimeUnixNano = (ulong)metricPoint.EndTime.ToUnixTimeNanoseconds(), + }; - // TODO: Do TagEnumerationState thing. - foreach (var attribute in metric.Attributes) - { - dataPoint.Attributes.Add(attribute.ToOtlpAttribute()); - } + AddAttributes(metricPoint.Tags, dataPoint.Attributes); - foreach (var exemplar in histogramMetric.Exemplars) - { - dataPoint.Exemplars.Add(exemplar.ToOtlpExemplar()); + dataPoint.AsDouble = metricPoint.GetGaugeLastValueDouble(); + gauge.DataPoints.Add(dataPoint); } - otlpMetric.Histogram = histogram; + otlpMetric.Gauge = gauge; break; } - case MetricType.Summary: + case MetricType.Histogram: { - var summaryMetric = metric as ISummaryMetric; - var summary = new OtlpMetrics.Summary(); - - var dataPoint = new OtlpMetrics.SummaryDataPoint - { - StartTimeUnixNano = (ulong)metric.StartTimeExclusive.ToUnixTimeNanoseconds(), - TimeUnixNano = (ulong)metric.EndTimeInclusive.ToUnixTimeNanoseconds(), - Count = (ulong)summaryMetric.PopulationCount, - Sum = summaryMetric.PopulationSum, - }; - - // TODO: Do TagEnumerationState thing. - foreach (var attribute in metric.Attributes) - { - dataPoint.Attributes.Add(attribute.ToOtlpAttribute()); - } + var histogram = new OtlpMetrics.Histogram(); + histogram.AggregationTemporality = temporality; - foreach (var quantile in summaryMetric.Quantiles) + foreach (ref readonly var metricPoint in metric.GetMetricPoints()) { - var quantileValue = new OtlpMetrics.SummaryDataPoint.Types.ValueAtQuantile + var dataPoint = new OtlpMetrics.HistogramDataPoint { - Quantile = quantile.Quantile, - Value = quantile.Value, + StartTimeUnixNano = (ulong)metricPoint.StartTime.ToUnixTimeNanoseconds(), + TimeUnixNano = (ulong)metricPoint.EndTime.ToUnixTimeNanoseconds(), }; - dataPoint.QuantileValues.Add(quantileValue); + + AddAttributes(metricPoint.Tags, dataPoint.Attributes); + dataPoint.Count = (ulong)metricPoint.GetHistogramCount(); + dataPoint.Sum = metricPoint.GetHistogramSum(); + + foreach (var histogramMeasurement in metricPoint.GetHistogramBuckets()) + { + dataPoint.BucketCounts.Add((ulong)histogramMeasurement.BucketCount); + if (histogramMeasurement.ExplicitBound != double.PositiveInfinity) + { + dataPoint.ExplicitBounds.Add(histogramMeasurement.ExplicitBound); + } + } + + histogram.DataPoints.Add(dataPoint); } - otlpMetric.Summary = summary; + otlpMetric.Histogram = histogram; break; } } @@ -266,43 +269,15 @@ internal static OtlpMetrics.Metric ToOtlpMetric(this IMetric metric) return otlpMetric; } - private static OtlpMetrics.NumberDataPoint ToNumberDataPoint(this IMetric metric, object value, IEnumerable exemplars) + private static void AddAttributes(ReadOnlyTagCollection tags, RepeatedField attributes) { - var dataPoint = new OtlpMetrics.NumberDataPoint - { - StartTimeUnixNano = (ulong)metric.StartTimeExclusive.ToUnixTimeNanoseconds(), - TimeUnixNano = (ulong)metric.EndTimeInclusive.ToUnixTimeNanoseconds(), - }; - - if (value is double doubleValue) - { - dataPoint.AsDouble = doubleValue; - } - else if (value is long longValue) - { - dataPoint.AsInt = longValue; - } - else - { - // TODO: Determine how we want to handle exceptions here. - // Do we want to just skip this metric and move on? - throw new ArgumentException($"Value must be a long or a double.", nameof(value)); - } - - // TODO: Do TagEnumerationState thing. - foreach (var attribute in metric.Attributes) + foreach (var tag in tags) { - dataPoint.Attributes.Add(attribute.ToOtlpAttribute()); + attributes.Add(tag.ToOtlpAttribute()); } - - foreach (var exemplar in exemplars) - { - dataPoint.Exemplars.Add(exemplar.ToOtlpExemplar()); - } - - return dataPoint; } + /* [MethodImpl(MethodImplOptions.AggressiveInlining)] private static OtlpMetrics.Exemplar ToOtlpExemplar(this IExemplar exemplar) { @@ -348,6 +323,7 @@ private static OtlpMetrics.Exemplar ToOtlpExemplar(this IExemplar exemplar) return otlpExemplar; } + */ private static Action, int> CreateRepeatedFieldOfMetricSetCountAction() { diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OpenTelemetryProtocolExporterEventSource.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OpenTelemetryProtocolExporterEventSource.cs index 1824c82fb69..2e46de5c651 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OpenTelemetryProtocolExporterEventSource.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/OpenTelemetryProtocolExporterEventSource.cs @@ -16,7 +16,6 @@ using System; using System.Diagnostics.Tracing; -using System.Security; using OpenTelemetry.Internal; namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation @@ -27,29 +26,12 @@ internal class OpenTelemetryProtocolExporterEventSource : EventSource public static readonly OpenTelemetryProtocolExporterEventSource Log = new OpenTelemetryProtocolExporterEventSource(); [NonEvent] - public void MissingPermissionsToReadEnvironmentVariable(SecurityException ex) - { - if (this.IsEnabled(EventLevel.Warning, EventKeywords.All)) - { - this.MissingPermissionsToReadEnvironmentVariable(ex.ToInvariantString()); - } - } - - [NonEvent] - public void FailedToConvertToProtoDefinitionError(Exception ex) - { - if (Log.IsEnabled(EventLevel.Error, EventKeywords.All)) - { - this.FailedToConvertToProtoDefinitionError(ex.ToInvariantString()); - } - } - - [NonEvent] - public void FailedToReachCollector(Exception ex) + public void FailedToReachCollector(Uri collectorUri, Exception ex) { if (Log.IsEnabled(EventLevel.Error, EventKeywords.All)) { - this.FailedToReachCollector(ex.ToInvariantString()); + var rawCollectorUri = collectorUri.ToString(); + this.FailedToReachCollector(rawCollectorUri, ex.ToInvariantString()); } } @@ -62,16 +44,10 @@ public void ExportMethodException(Exception ex) } } - [Event(1, Message = "Exporter failed to convert SpanData content into gRPC proto definition. Data will not be sent. Exception: {0}", Level = EventLevel.Error)] - public void FailedToConvertToProtoDefinitionError(string ex) - { - this.WriteEvent(1, ex); - } - - [Event(2, Message = "Exporter failed send data to collector. Data will not be sent. Exception: {0}", Level = EventLevel.Error)] - public void FailedToReachCollector(string ex) + [Event(2, Message = "Exporter failed send data to collector to {0} endpoint. Data will not be sent. Exception: {1}", Level = EventLevel.Error)] + public void FailedToReachCollector(string rawCollectorUri, string ex) { - this.WriteEvent(2, ex); + this.WriteEvent(2, rawCollectorUri, ex); } [Event(3, Message = "Could not translate activity from class '{0}' and method '{1}', span will not be recorded.", Level = EventLevel.Informational)] @@ -92,16 +68,16 @@ public void CouldNotTranslateMetric(string className, string methodName) this.WriteEvent(5, className, methodName); } - [Event(6, Message = "Failed to parse environment variable: '{0}', value: '{1}'.", Level = EventLevel.Warning)] - public void FailedToParseEnvironmentVariable(string name, string value) + [Event(8, Message = "Unsupported value for protocol '{0}' is configured, default protocol 'grpc' will be used.", Level = EventLevel.Warning)] + public void UnsupportedProtocol(string protocol) { - this.WriteEvent(6, name, value); + this.WriteEvent(8, protocol); } - [Event(7, Message = "Missing permissions to read environment variable: '{0}'", Level = EventLevel.Warning)] - public void MissingPermissionsToReadEnvironmentVariable(string exception) + [Event(9, Message = "Could not translate LogRecord from class '{0}' and method '{1}', log will not be exported.", Level = EventLevel.Informational)] + public void CouldNotTranslateLogRecord(string className, string methodName) { - this.WriteEvent(7, exception); + this.WriteEvent(9, className, methodName); } } } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/common/v1/common.proto b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/common/v1/common.proto index 8dc24567f5c..ebf73ad4f97 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/common/v1/common.proto +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/common/v1/common.proto @@ -26,7 +26,7 @@ option go_package = "github.com/open-telemetry/opentelemetry-proto/gen/go/common // object containing arrays, key-value lists and primitives. message AnyValue { // The value is one of the listed fields. It is valid for all values to be unspecified - // in which case this AnyValue is considered to be "null". + // in which case this AnyValue is considered to be "empty". oneof value { string string_value = 1; bool bool_value = 2; diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/logs/v1/logs.proto b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/logs/v1/logs.proto index 84fafef221b..ff7f13cbfde 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/logs/v1/logs.proto +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/logs/v1/logs.proto @@ -24,6 +24,25 @@ option java_package = "io.opentelemetry.proto.logs.v1"; option java_outer_classname = "LogsProto"; option go_package = "github.com/open-telemetry/opentelemetry-proto/gen/go/logs/v1"; +// LogsData represents the logs data that can be stored in a persistent storage, +// OR can be embedded by other protocols that transfer OTLP logs 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 LogsData { + // An array of ResourceLogs. + // 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 ResourceLogs resource_logs = 1; +} + // A collection of InstrumentationLibraryLogs from a Resource. message ResourceLogs { // The resource for the logs in this message. diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/metrics/v1/metrics.proto b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/metrics/v1/metrics.proto index 244f4229bda..60f5810c06b 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/metrics/v1/metrics.proto +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/metrics/v1/metrics.proto @@ -24,6 +24,25 @@ option java_package = "io.opentelemetry.proto.metrics.v1"; option java_outer_classname = "MetricsProto"; option go_package = "github.com/open-telemetry/opentelemetry-proto/gen/go/metrics/v1"; +// MetricsData represents the metrics data that can be stored in a persistent +// storage, OR can be embedded by other protocols that transfer OTLP metrics +// 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 MetricsData { + // An array of ResourceMetrics. + // 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 ResourceMetrics resource_metrics = 1; +} + // A collection of InstrumentationLibraryMetrics from a Resource. message ResourceMetrics { // The resource for the metrics in this message. @@ -176,28 +195,12 @@ message Metric { // This field will be removed in ~3 months, on July 1, 2021. IntHistogram int_histogram = 8 [deprecated = true]; Histogram histogram = 9; + ExponentialHistogram exponential_histogram = 10; Summary summary = 11; } } -// IntGauge is deprecated. Use Gauge with an integer value in NumberDataPoint. -// -// IntGauge represents the type of a int scalar metric that always exports the -// "current value" for every data point. It should be used for an "unknown" -// aggregation. -// -// A Gauge does not support different aggregation temporalities. Given the -// aggregation is unknown, points cannot be combined using the same -// aggregation, regardless of aggregation temporalities. Therefore, -// AggregationTemporality is not included. Consequently, this also means -// "StartTimeUnixNano" is ignored for all data points. -message IntGauge { - option deprecated = true; - - repeated IntDataPoint data_points = 1; -} - -// Gauge represents the type of a double scalar metric that always exports the +// Gauge represents the type of a scalar metric that always exports the // "current value" for every data point. It should be used for an "unknown" // aggregation. // @@ -210,25 +213,8 @@ message Gauge { repeated NumberDataPoint data_points = 1; } -// IntSum is deprecated. Use Sum with an integer value in NumberDataPoint. -// -// IntSum represents the type of a numeric int scalar metric that is calculated as -// a sum of all reported measurements over a time interval. -message IntSum { - option deprecated = true; - - repeated IntDataPoint data_points = 1; - - // aggregation_temporality describes if the aggregator reports delta changes - // since last report time, or cumulative changes since a fixed start time. - AggregationTemporality aggregation_temporality = 2; - - // If "true" means that the sum is monotonic. - bool is_monotonic = 3; -} - -// Sum represents the type of a numeric double scalar metric that is calculated -// as a sum of all reported measurements over a time interval. +// Sum represents the type of a scalar metric that is calculated as a sum of all +// reported measurements over a time interval. message Sum { repeated NumberDataPoint data_points = 1; @@ -240,25 +226,20 @@ message Sum { bool is_monotonic = 3; } -// IntHistogram is deprecated, replaced by Histogram points using double- -// valued exemplars. -// -// This represents the type of a metric that is calculated by aggregating as a -// Histogram of all reported int measurements over a time interval. -message IntHistogram { - option deprecated = true; - - repeated IntHistogramDataPoint data_points = 1; +// Histogram represents the type of a metric that is calculated by aggregating +// as a Histogram of all reported measurements over a time interval. +message Histogram { + repeated HistogramDataPoint data_points = 1; // aggregation_temporality describes if the aggregator reports delta changes // since last report time, or cumulative changes since a fixed start time. AggregationTemporality aggregation_temporality = 2; } -// Histogram represents the type of a metric that is calculated by aggregating -// as a Histogram of all reported double measurements over a time interval. -message Histogram { - repeated HistogramDataPoint data_points = 1; +// ExponentialHistogram represents the type of a metric that is calculated by aggregating +// as a ExponentialHistogram of all reported double measurements over a time interval. +message ExponentialHistogram { + repeated ExponentialHistogramDataPoint data_points = 1; // aggregation_temporality describes if the aggregator reports delta changes // since last report time, or cumulative changes since a fixed start time. @@ -346,37 +327,26 @@ enum AggregationTemporality { AGGREGATION_TEMPORALITY_CUMULATIVE = 2; } -// IntDataPoint is a single data point in a timeseries that describes the -// time-varying values of a int64 metric. -message IntDataPoint { - option deprecated = true; - - // The set of labels that uniquely identify this timeseries. - repeated opentelemetry.proto.common.v1.StringKeyValue labels = 1; - - // StartTimeUnixNano is optional but strongly encouraged, see the - // the detiled comments above Metric. - // - // Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January - // 1970. - fixed64 start_time_unix_nano = 2; - - // TimeUnixNano is required, see the detailed comments above Metric. - // - // Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January - // 1970. - fixed64 time_unix_nano = 3; +// DataPointFlags is defined as a protobuf 'uint32' type and is to be used as a +// bit-field representing 32 distinct boolean flags. Each flag defined in this +// enum is a bit-mask. To test the presence of a single flag in the flags of +// a data point, for example, use an expression like: +// +// (point.flags & FLAG_NO_RECORDED_VALUE) == FLAG_NO_RECORDED_VALUE +// +enum DataPointFlags { + FLAG_NONE = 0; - // value itself. - sfixed64 value = 4; + // This DataPoint is valid but has no recorded value. This value + // SHOULD be used to reflect explicitly missing data in a series, as + // for an equivalent to the Prometheus "staleness marker". + FLAG_NO_RECORDED_VALUE = 1; - // (Optional) List of exemplars collected from - // measurements that were used to form the data point - repeated IntExemplar exemplars = 5; + // Bits 2-31 are reserved for future use. } // NumberDataPoint is a single data point in a timeseries that describes the -// time-varying value of a double metric. +// time-varying scalar value of a metric. message NumberDataPoint { // The set of key/value pairs that uniquely identify the timeseries from // where this point belongs. The list may be empty (may contain 0 elements). @@ -393,7 +363,7 @@ message NumberDataPoint { repeated opentelemetry.proto.common.v1.StringKeyValue labels = 1 [deprecated = true]; // StartTimeUnixNano is optional but strongly encouraged, see the - // the detiled comments above Metric. + // the detailed comments above Metric. // // Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January // 1970. @@ -415,28 +385,39 @@ message NumberDataPoint { // (Optional) List of exemplars collected from // measurements that were used to form the data point repeated Exemplar exemplars = 5; + + // Flags that apply to this specific data point. See DataPointFlags + // for the available flags and their meaning. + uint32 flags = 8; } -// IntHistogramDataPoint is deprecated; use HistogramDataPoint. -// -// This is a single data point in a timeseries that describes -// the time-varying values of a Histogram of int values. A Histogram contains -// summary statistics for a population of values, it may optionally contain -// the distribution of those values across a set of buckets. +// HistogramDataPoint is a single data point in a timeseries that describes the +// time-varying values of a Histogram. A Histogram contains summary statistics +// for a population of values, it may optionally contain the distribution of +// those values across a set of buckets. // // If the histogram contains the distribution of values, then both // "explicit_bounds" and "bucket counts" fields must be defined. // If the histogram does not contain the distribution of values, then both // "explicit_bounds" and "bucket_counts" must be omitted and only "count" and // "sum" are known. -message IntHistogramDataPoint { - option deprecated = true; +message HistogramDataPoint { + // The set of key/value pairs that uniquely identify the timeseries from + // where this point belongs. The list may be empty (may contain 0 elements). + repeated opentelemetry.proto.common.v1.KeyValue attributes = 9; - // The set of labels that uniquely identify this timeseries. - repeated opentelemetry.proto.common.v1.StringKeyValue labels = 1; + // Labels is deprecated and will be removed soon. + // 1. Old senders and receivers that are not aware of this change will + // continue using the `labels` field. + // 2. New senders, which are aware of this change MUST send only `attributes`. + // 3. New receivers, which are aware of this change MUST convert this into + // `labels` by simply converting all int64 values into float. + // + // This field will be removed in ~3 months, on July 1, 2021. + repeated opentelemetry.proto.common.v1.StringKeyValue labels = 1 [deprecated = true]; // StartTimeUnixNano is optional but strongly encouraged, see the - // the detiled comments above Metric. + // the detailed comments above Metric. // // Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January // 1970. @@ -454,9 +435,14 @@ message IntHistogramDataPoint { fixed64 count = 4; // sum of the values in the population. If count is zero then this field - // must be zero. This value must be equal to the sum of the "sum" fields in - // buckets if a histogram is provided. - sfixed64 sum = 5; + // must be zero. + // + // Note: Sum should only be filled out when measuring non-negative discrete + // 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 + double sum = 5; // bucket_counts is an optional field contains the count values of histogram // for each bucket. @@ -469,12 +455,11 @@ message IntHistogramDataPoint { // explicit_bounds specifies buckets with explicitly defined bounds for values. // - // This defines size(explicit_bounds) + 1 (= N) buckets. The boundaries for - // bucket at index i are: + // The boundaries for bucket at index i are: // // (-infinity, explicit_bounds[i]] for i == 0 - // (explicit_bounds[i-1], explicit_bounds[i]] for 0 < i < N-1 - // (explicit_bounds[i], +infinity) for i == N-1 + // (explicit_bounds[i-1], explicit_bounds[i]] for 0 < i < size(explicit_bounds) + // (explicit_bounds[i-1], +infinity) for i == size(explicit_bounds) // // The values in the explicit_bounds array must be strictly increasing. // @@ -485,36 +470,25 @@ message IntHistogramDataPoint { // (Optional) List of exemplars collected from // measurements that were used to form the data point - repeated IntExemplar exemplars = 8; + repeated Exemplar exemplars = 8; + + // Flags that apply to this specific data point. See DataPointFlags + // for the available flags and their meaning. + uint32 flags = 10; } -// HistogramDataPoint is a single data point in a timeseries that describes the -// time-varying values of a Histogram of double values. A Histogram contains +// ExponentialHistogramDataPoint is a single data point in a timeseries that describes the +// time-varying values of a ExponentialHistogram of double values. A ExponentialHistogram contains // summary statistics for a population of values, it may optionally contain the // distribution of those values across a set of buckets. // -// If the histogram contains the distribution of values, then both -// "explicit_bounds" and "bucket counts" fields must be defined. -// If the histogram does not contain the distribution of values, then both -// "explicit_bounds" and "bucket_counts" must be omitted and only "count" and -// "sum" are known. -message HistogramDataPoint { +message ExponentialHistogramDataPoint { // The set of key/value pairs that uniquely identify the timeseries from // where this point belongs. The list may be empty (may contain 0 elements). - repeated opentelemetry.proto.common.v1.KeyValue attributes = 9; - - // Labels is deprecated and will be removed soon. - // 1. Old senders and receivers that are not aware of this change will - // continue using the `labels` field. - // 2. New senders, which are aware of this change MUST send only `attributes`. - // 3. New receivers, which are aware of this change MUST convert this into - // `labels` by simply converting all int64 values into float. - // - // This field will be removed in ~3 months, on July 1, 2021. - repeated opentelemetry.proto.common.v1.StringKeyValue labels = 1 [deprecated = true]; + repeated opentelemetry.proto.common.v1.KeyValue attributes = 1; // StartTimeUnixNano is optional but strongly encouraged, see the - // the detiled comments above Metric. + // the detailed comments above Metric. // // Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January // 1970. @@ -526,14 +500,13 @@ message HistogramDataPoint { // 1970. fixed64 time_unix_nano = 3; - // count is the number of values in the population. Must be non-negative. This - // value must be equal to the sum of the "count" fields in buckets if a - // histogram is provided. + // count is the number of values in the population. Must be + // non-negative. This value must be equal to the sum of the "bucket_counts" + // values in the positive and negative Buckets plus the "zero_count" field. fixed64 count = 4; // sum of the values in the population. If count is zero then this field - // must be zero. This value must be equal to the sum of the "sum" fields in - // buckets if a histogram is provided. + // must be zero. // // Note: Sum should only be filled out when measuring non-negative discrete // events, and is assumed to be monotonic over the values of these events. @@ -541,35 +514,67 @@ message HistogramDataPoint { // doing so. This is specifically to enforce compatibility w/ OpenMetrics, // see: https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#histogram double sum = 5; + + // scale describes the resolution of the histogram. Boundaries are + // located at powers of the base, where: + // + // base = (2^(2^-scale)) + // + // The histogram bucket identified by `index`, a signed integer, + // contains values that are greater than or equal to (base^index) and + // less than (base^(index+1)). + // + // The positive and negative ranges of the histogram are expressed + // separately. Negative values are mapped by their absolute value + // into the negative range using the same scale as the positive range. + // + // scale is not restricted by the protocol, as the permissible + // values depend on the range of the data. + sint32 scale = 6; + + // zero_count is the count of values that are either exactly zero or + // within the region considered zero by the instrumentation at the + // tolerated degree of precision. This bucket stores values that + // cannot be expressed using the standard exponential formula as + // well as values that have been rounded to zero. + // + // Implementations MAY consider the zero bucket to have probability + // mass equal to (zero_count / count). + fixed64 zero_count = 7; + + // positive carries the positive range of exponential bucket counts. + Buckets positive = 8; + + // negative carries the negative range of exponential bucket counts. + Buckets negative = 9; + + // Buckets are a set of bucket counts, encoded in a contiguous array + // of counts. + message Buckets { + // Offset is the bucket index of the first entry in the bucket_counts array. + // + // Note: This uses a varint encoding as a simple form of compression. + sint32 offset = 1; + + // Count is an array of counts, where count[i] carries the count + // of the bucket at index (offset+i). count[i] is the count of + // values greater than or equal to base^(offset+i) and less than + // base^(offset+i+1). + // + // Note: By contrast, the explicit HistogramDataPoint uses + // fixed64. This field is expected to have many buckets, + // especially zeros, so uint64 has been selected to ensure + // varint encoding. + repeated uint64 bucket_counts = 2; + } - // bucket_counts is an optional field contains the count values of histogram - // for each bucket. - // - // The sum of the bucket_counts must equal the value in the count field. - // - // The number of elements in bucket_counts array must be by one greater than - // the number of elements in explicit_bounds array. - repeated fixed64 bucket_counts = 6; - - // explicit_bounds specifies buckets with explicitly defined bounds for values. - // - // This defines size(explicit_bounds) + 1 (= N) buckets. The boundaries for - // bucket at index i are: - // - // (-infinity, explicit_bounds[i]] for i == 0 - // (explicit_bounds[i-1], explicit_bounds[i]] for 0 < i < N-1 - // (explicit_bounds[i], +infinity) for i == N-1 - // - // The values in the explicit_bounds array must be strictly increasing. - // - // Histogram buckets are inclusive of their upper boundary, except the last - // bucket where the boundary is at infinity. This format is intentionally - // compatible with the OpenMetrics histogram definition. - repeated double explicit_bounds = 7; + // Flags that apply to this specific data point. See DataPointFlags + // for the available flags and their meaning. + uint32 flags = 10; // (Optional) List of exemplars collected from // measurements that were used to form the data point - repeated Exemplar exemplars = 8; + repeated Exemplar exemplars = 11; } // SummaryDataPoint is a single data point in a timeseries that describes the @@ -590,7 +595,7 @@ message SummaryDataPoint { repeated opentelemetry.proto.common.v1.StringKeyValue labels = 1 [deprecated = true]; // StartTimeUnixNano is optional but strongly encouraged, see the - // the detiled comments above Metric. + // the detailed comments above Metric. // // Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January // 1970. @@ -637,38 +642,10 @@ message SummaryDataPoint { // (Optional) list of values at different quantiles of the distribution calculated // from the current snapshot. The quantiles must be strictly increasing. repeated ValueAtQuantile quantile_values = 6; -} - -// A representation of an exemplar, which is a sample input int measurement. -// Exemplars also hold information about the environment when the measurement -// was recorded, for example the span and trace ID of the active span when the -// exemplar was recorded. -message IntExemplar { - option deprecated = true; - - // The set of labels that were filtered out by the aggregator, but recorded - // alongside the original measurement. Only labels that were filtered out - // by the aggregator should be included - repeated opentelemetry.proto.common.v1.StringKeyValue filtered_labels = 1; - // time_unix_nano is the exact time when this exemplar was recorded - // - // Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January - // 1970. - fixed64 time_unix_nano = 2; - - // Numerical int value of the measurement that was recorded. - sfixed64 value = 3; - - // (Optional) Span ID of the exemplar trace. - // span_id may be missing if the measurement is not recorded inside a trace - // or if the trace is not sampled. - bytes span_id = 4; - - // (Optional) Trace ID of the exemplar trace. - // trace_id may be missing if the measurement is not recorded inside a trace - // or if the trace is not sampled. - bytes trace_id = 5; + // Flags that apply to this specific data point. See DataPointFlags + // for the available flags and their meaning. + uint32 flags = 8; } // A representation of an exemplar, which is a sample input measurement. @@ -698,7 +675,7 @@ message Exemplar { // 1970. fixed64 time_unix_nano = 2; - // Numerical value of the measurement that was recorded. An exemplar is + // The value of the measurement that was recorded. An exemplar is // considered invalid when one of the recognized value fields is not present // inside this oneof. oneof value { @@ -716,3 +693,156 @@ message Exemplar { // or if the trace is not sampled. bytes trace_id = 5; } + +// +// Move deprecated messages below this line +// + +// IntDataPoint is deprecated. Use integer value in NumberDataPoint. +message IntDataPoint { + option deprecated = true; + + // The set of labels that uniquely identify this timeseries. + repeated opentelemetry.proto.common.v1.StringKeyValue labels = 1; + + // StartTimeUnixNano is optional but strongly encouraged, see the + // the detailed comments above Metric. + // + // Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January + // 1970. + fixed64 start_time_unix_nano = 2; + + // TimeUnixNano is required, see the detailed comments above Metric. + // + // Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January + // 1970. + fixed64 time_unix_nano = 3; + + // value itself. + sfixed64 value = 4; + + // (Optional) List of exemplars collected from + // measurements that were used to form the data point + repeated IntExemplar exemplars = 5; +} + +// IntGauge is deprecated. Use Gauge with an integer value in NumberDataPoint. +message IntGauge { + option deprecated = true; + + repeated IntDataPoint data_points = 1; +} + +// IntSum is deprecated. Use Sum with an integer value in NumberDataPoint. +message IntSum { + option deprecated = true; + + repeated IntDataPoint data_points = 1; + + // aggregation_temporality describes if the aggregator reports delta changes + // since last report time, or cumulative changes since a fixed start time. + AggregationTemporality aggregation_temporality = 2; + + // If "true" means that the sum is monotonic. + bool is_monotonic = 3; +} + +// IntHistogramDataPoint is deprecated; use HistogramDataPoint. +message IntHistogramDataPoint { + option deprecated = true; + + // The set of labels that uniquely identify this timeseries. + repeated opentelemetry.proto.common.v1.StringKeyValue labels = 1; + + // StartTimeUnixNano is optional but strongly encouraged, see the + // the detailed comments above Metric. + // + // Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January + // 1970. + fixed64 start_time_unix_nano = 2; + + // TimeUnixNano is required, see the detailed comments above Metric. + // + // Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January + // 1970. + fixed64 time_unix_nano = 3; + + // count is the number of values in the population. Must be non-negative. This + // value must be equal to the sum of the "count" fields in buckets if a + // histogram is provided. + fixed64 count = 4; + + // sum of the values in the population. If count is zero then this field + // must be zero. This value must be equal to the sum of the "sum" fields in + // buckets if a histogram is provided. + sfixed64 sum = 5; + + // bucket_counts is an optional field contains the count values of histogram + // for each bucket. + // + // The sum of the bucket_counts must equal the value in the count field. + // + // The number of elements in bucket_counts array must be by one greater than + // the number of elements in explicit_bounds array. + repeated fixed64 bucket_counts = 6; + + // explicit_bounds specifies buckets with explicitly defined bounds for values. + // + // The boundaries for bucket at index i are: + // + // (-infinity, explicit_bounds[i]] for i == 0 + // (explicit_bounds[i-1], explicit_bounds[i]] for 0 < i < size(explicit_bounds) + // (explicit_bounds[i-1], +infinity) for i == size(explicit_bounds) + // + // The values in the explicit_bounds array must be strictly increasing. + // + // Histogram buckets are inclusive of their upper boundary, except the last + // bucket where the boundary is at infinity. This format is intentionally + // compatible with the OpenMetrics histogram definition. + repeated double explicit_bounds = 7; + + // (Optional) List of exemplars collected from + // measurements that were used to form the data point + repeated IntExemplar exemplars = 8; +} + +// IntHistogram is deprecated, replaced by Histogram points using double- +// valued exemplars. +message IntHistogram { + option deprecated = true; + + repeated IntHistogramDataPoint data_points = 1; + + // aggregation_temporality describes if the aggregator reports delta changes + // since last report time, or cumulative changes since a fixed start time. + AggregationTemporality aggregation_temporality = 2; +} + +// IntExemplar is deprecated. Use Exemplar with as_int for value +message IntExemplar { + option deprecated = true; + + // The set of labels that were filtered out by the aggregator, but recorded + // alongside the original measurement. Only labels that were filtered out + // by the aggregator should be included + repeated opentelemetry.proto.common.v1.StringKeyValue filtered_labels = 1; + + // time_unix_nano is the exact time when this exemplar was recorded + // + // Value is UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January + // 1970. + fixed64 time_unix_nano = 2; + + // Numerical int value of the measurement that was recorded. + sfixed64 value = 3; + + // (Optional) Span ID of the exemplar trace. + // span_id may be missing if the measurement is not recorded inside a trace + // or if the trace is not sampled. + bytes span_id = 4; + + // (Optional) Trace ID of the exemplar trace. + // trace_id may be missing if the measurement is not recorded inside a trace + // or if the trace is not sampled. + bytes trace_id = 5; +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/trace/v1/trace.proto b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/trace/v1/trace.proto index 1513e52ee3e..52ee150a7bb 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/trace/v1/trace.proto +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/Implementation/opentelemetry/proto/trace/v1/trace.proto @@ -24,6 +24,25 @@ option java_package = "io.opentelemetry.proto.trace.v1"; option java_outer_classname = "TraceProto"; option go_package = "github.com/open-telemetry/opentelemetry-proto/gen/go/trace/v1"; +// TracesData represents the traces data that can be stored in a persistent storage, +// OR can be embedded by other protocols that transfer OTLP traces 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 TracesData { + // An array of ResourceSpans. + // 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 ResourceSpans resource_spans = 1; +} + // A collection of InstrumentationLibrarySpans from a Resource. message ResourceSpans { // The resource for the spans in this message. @@ -100,9 +119,7 @@ message Span { // This makes it easier to correlate spans in different traces. // // This field is semantically required to be set to non-empty string. - // When null or empty string received - receiver may use string "name" - // as a replacement. There might be smarted algorithms implemented by - // receiver to fix the empty span name. + // Empty value is equivalent to an unknown span name. // // This field is required. string name = 5; @@ -158,14 +175,16 @@ message Span { // This field is semantically required and it is expected that end_time >= start_time. fixed64 end_time_unix_nano = 8; - // attributes is a collection of key/value pairs. The value can be a string, - // an integer, a double or the Boolean values `true` or `false`. Note, global attributes + // 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/common.md#attributes repeated opentelemetry.proto.common.v1.KeyValue attributes = 9; // dropped_attributes_count is the number of attributes that were discarded. Attributes diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OpenTelemetry.Exporter.OpenTelemetryProtocol.csproj b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OpenTelemetry.Exporter.OpenTelemetryProtocol.csproj index 205b1929d2a..8a8549653f0 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OpenTelemetry.Exporter.OpenTelemetryProtocol.csproj +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OpenTelemetry.Exporter.OpenTelemetryProtocol.csproj @@ -1,12 +1,14 @@  - netstandard2.0;netstandard2.1;net461 + netstandard2.0;netstandard2.1;net461;net5.0 OpenTelemetry protocol exporter for OpenTelemetry .NET $(PackageTags);OTLP core- + + false - false @@ -16,7 +18,15 @@ - + + + + + + + + + @@ -32,14 +42,13 @@ - - - + diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExportProtocol.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExportProtocol.cs new file mode 100644 index 00000000000..5a830082a5b --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExportProtocol.cs @@ -0,0 +1,34 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace OpenTelemetry.Exporter +{ + /// + /// Supported by OTLP exporter protocol types according to the specification https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md. + /// + public enum OtlpExportProtocol : byte + { + /// + /// OTLP over gRPC (corresponds to 'grpc' Protocol configuration option). Used as default. + /// + Grpc = 0, + + /// + /// OTLP over HTTP with protobuf payloads (corresponds to 'http/protobuf' Protocol configuration option). + /// + HttpProtobuf = 1, + } +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs index f112e76a773..a5725b9ba6c 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptions.cs @@ -16,72 +16,80 @@ using System; using System.Diagnostics; -using System.Security; -using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; +using System.Net.Http; +using OpenTelemetry.Internal; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; namespace OpenTelemetry.Exporter { /// - /// Configuration options for the OpenTelemetry Protocol (OTLP) exporter. + /// OpenTelemetry Protocol (OTLP) exporter options. + /// OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_HEADERS, OTEL_EXPORTER_OTLP_TIMEOUT, OTEL_EXPORTER_OTLP_PROTOCOL + /// environment variables are parsed during object construction. /// + /// + /// The constructor throws if it fails to parse + /// any of the supported environment variables. + /// public class OtlpExporterOptions { internal const string EndpointEnvVarName = "OTEL_EXPORTER_OTLP_ENDPOINT"; internal const string HeadersEnvVarName = "OTEL_EXPORTER_OTLP_HEADERS"; internal const string TimeoutEnvVarName = "OTEL_EXPORTER_OTLP_TIMEOUT"; + internal const string ProtocolEnvVarName = "OTEL_EXPORTER_OTLP_PROTOCOL"; + + internal const string TracesExportPath = "v1/traces"; + internal const string MetricsExportPath = "v1/metrics"; + + internal readonly Func DefaultHttpClientFactory; /// /// Initializes a new instance of the class. /// public OtlpExporterOptions() { - try + if (EnvironmentVariableHelper.LoadUri(EndpointEnvVarName, out Uri endpoint)) { - string endpointEnvVar = Environment.GetEnvironmentVariable(EndpointEnvVarName); - if (!string.IsNullOrEmpty(endpointEnvVar)) - { - if (Uri.TryCreate(endpointEnvVar, UriKind.Absolute, out var endpoint)) - { - this.Endpoint = endpoint; - } - else - { - OpenTelemetryProtocolExporterEventSource.Log.FailedToParseEnvironmentVariable(EndpointEnvVarName, endpointEnvVar); - } - } + this.Endpoint = endpoint; + } - string headersEnvVar = Environment.GetEnvironmentVariable(HeadersEnvVarName); - if (!string.IsNullOrEmpty(headersEnvVar)) + if (EnvironmentVariableHelper.LoadString(HeadersEnvVarName, out string headersEnvVar)) + { + this.Headers = headersEnvVar; + } + + if (EnvironmentVariableHelper.LoadNumeric(TimeoutEnvVarName, out int timeout)) + { + this.TimeoutMilliseconds = timeout; + } + + if (EnvironmentVariableHelper.LoadString(ProtocolEnvVarName, out string protocolEnvVar)) + { + var protocol = protocolEnvVar.ToOtlpExportProtocol(); + if (protocol.HasValue) { - this.Headers = headersEnvVar; + this.Protocol = protocol.Value; } - - string timeoutEnvVar = Environment.GetEnvironmentVariable(TimeoutEnvVarName); - if (!string.IsNullOrEmpty(timeoutEnvVar)) + else { - if (int.TryParse(timeoutEnvVar, out var timeout)) - { - this.TimeoutMilliseconds = timeout; - } - else - { - OpenTelemetryProtocolExporterEventSource.Log.FailedToParseEnvironmentVariable(TimeoutEnvVarName, timeoutEnvVar); - } + throw new FormatException($"{ProtocolEnvVarName} environment variable has an invalid value: '${protocolEnvVar}'"); } } - catch (SecurityException ex) + + this.HttpClientFactory = this.DefaultHttpClientFactory = () => { - // The caller does not have the required permission to - // retrieve the value of an environment variable from the current process. - OpenTelemetryProtocolExporterEventSource.Log.MissingPermissionsToReadEnvironmentVariable(ex); - } + return new HttpClient() + { + Timeout = TimeSpan.FromMilliseconds(this.TimeoutMilliseconds), + }; + }; } /// - /// Gets or sets the target to which the exporter is going to send traces. - /// Must be a valid Uri with scheme (http) and host, and - /// may contain a port and path. Secure connection(https) is not - /// supported. + /// Gets or sets the target to which the exporter is going to send telemetry. + /// Must be a valid Uri with scheme (http or https) and host, and + /// may contain a port and path. The default value is http://localhost:4317. /// public Uri Endpoint { get; set; } = new Uri("http://localhost:4317"); @@ -92,10 +100,15 @@ public OtlpExporterOptions() public string Headers { get; set; } /// - /// Gets or sets the max waiting time (in milliseconds) for the backend to process each span batch. The default value is 10000. + /// Gets or sets the max waiting time (in milliseconds) for the backend to process each batch. The default value is 10000. /// public int TimeoutMilliseconds { get; set; } = 10000; + /// + /// Gets or sets the the OTLP transport protocol. Supported values: Grpc and HttpProtobuf. + /// + public OtlpExportProtocol Protocol { get; set; } = OtlpExportProtocol.Grpc; + /// /// Gets or sets the export processor type to be used with the OpenTelemetry Protocol Exporter. The default value is . /// @@ -104,17 +117,55 @@ public OtlpExporterOptions() /// /// Gets or sets the BatchExportProcessor options. Ignored unless ExportProcessorType is Batch. /// - public BatchExportProcessorOptions BatchExportProcessorOptions { get; set; } = new BatchExportProcessorOptions(); + public BatchExportProcessorOptions BatchExportProcessorOptions { get; set; } = new BatchExportActivityProcessorOptions(); + + /// + /// Gets or sets the to use. Defaults to MetricReaderType.Periodic. + /// + public MetricReaderType MetricReaderType { get; set; } = MetricReaderType.Periodic; + + /// + /// Gets or sets the options. Ignored unless MetricReaderType is Periodic. + /// + public PeriodicExportingMetricReaderOptions PeriodicExportingMetricReaderOptions { get; set; } = new PeriodicExportingMetricReaderOptions(); /// - /// Gets or sets the metric export interval in milliseconds. The default value is 1000 milliseconds. + /// Gets or sets the AggregationTemporality used for Histogram + /// and Sum metrics. /// - public int MetricExportIntervalMilliseconds { get; set; } = 1000; + public AggregationTemporality AggregationTemporality { get; set; } = AggregationTemporality.Cumulative; /// - /// Gets or sets a value indicating whether to export Delta - /// values or not (Cumulative). + /// Gets or sets the factory function called to create the instance that will be used at runtime to + /// transmit telemetry over HTTP. The returned instance will be reused + /// for all export invocations. /// - public bool IsDelta { get; set; } = true; + /// + /// Notes: + /// + /// This is only invoked for the protocol. + /// The default behavior when using the extension is if an IHttpClientFactory + /// instance can be resolved through the application then an will be + /// created through the factory with the name "OtlpTraceExporter" + /// otherwise an will be instantiated + /// directly. + /// The default behavior when using the extension is if an IHttpClientFactory + /// instance can be resolved through the application then an will be + /// created through the factory with the name "OtlpMetricExporter" + /// otherwise an will be instantiated + /// directly. + /// + /// + public Func HttpClientFactory { get; set; } } } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptionsExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptionsExtensions.cs new file mode 100644 index 00000000000..3760a1830de --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptionsExtensions.cs @@ -0,0 +1,204 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Net.Http; +using System.Reflection; +using Grpc.Core; +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; +using OpenTelemetry.Internal; +#if NETSTANDARD2_1 || NET5_0_OR_GREATER +using Grpc.Net.Client; +#endif +using MetricsOtlpCollector = Opentelemetry.Proto.Collector.Metrics.V1; +using TraceOtlpCollector = Opentelemetry.Proto.Collector.Trace.V1; + +namespace OpenTelemetry.Exporter +{ + internal static class OtlpExporterOptionsExtensions + { +#if NETSTANDARD2_1 || NET5_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."); + } + +#if NETSTANDARD2_1 || NET5_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)); + } + + public static THeaders GetHeaders(this OtlpExporterOptions options, Action addHeader) + where THeaders : new() + { + var optionHeaders = options.Headers; + var headers = new THeaders(); + if (!string.IsNullOrEmpty(optionHeaders)) + { + Array.ForEach( + optionHeaders.Split(','), + (pair) => + { + // Specify the maximum number of substrings to return to 2 + // This treats everything that follows the first `=` in the string as the value to be added for the metadata key + var keyValueData = pair.Split(new char[] { '=' }, 2); + if (keyValueData.Length != 2) + { + throw new ArgumentException("Headers provided in an invalid format."); + } + + var key = keyValueData[0].Trim(); + var value = keyValueData[1].Trim(); + addHeader(headers, key, value); + }); + } + + return headers; + } + + public static IExportClient GetTraceExportClient(this OtlpExporterOptions options) => + options.Protocol switch + { + OtlpExportProtocol.Grpc => new OtlpGrpcTraceExportClient(options), + OtlpExportProtocol.HttpProtobuf => new OtlpHttpTraceExportClient( + options, + options.HttpClientFactory?.Invoke() ?? throw new InvalidOperationException("OtlpExporterOptions was missing HttpClientFactory or it returned null.")), + _ => throw new NotSupportedException($"Protocol {options.Protocol} is not supported."), + }; + + public static IExportClient GetMetricsExportClient(this OtlpExporterOptions options) => + options.Protocol switch + { + OtlpExportProtocol.Grpc => new OtlpGrpcMetricsExportClient(options), + OtlpExportProtocol.HttpProtobuf => new OtlpHttpMetricsExportClient( + options, + options.HttpClientFactory?.Invoke() ?? throw new InvalidOperationException("OtlpExporterOptions was missing HttpClientFactory or it returned null.")), + _ => throw new NotSupportedException($"Protocol {options.Protocol} is not supported."), + }; + + public static OtlpExportProtocol? ToOtlpExportProtocol(this string protocol) => + protocol.Trim() switch + { + "grpc" => OtlpExportProtocol.Grpc, + "http/protobuf" => OtlpExportProtocol.HttpProtobuf, + _ => null, + }; + + public static void TryEnableIHttpClientFactoryIntegration(this OtlpExporterOptions options, IServiceProvider serviceProvider, string httpClientName) + { + if (serviceProvider != null + && options.Protocol == OtlpExportProtocol.HttpProtobuf + && options.HttpClientFactory == options.DefaultHttpClientFactory) + { + options.HttpClientFactory = () => + { + Type httpClientFactoryType = Type.GetType("System.Net.Http.IHttpClientFactory, Microsoft.Extensions.Http", throwOnError: false); + if (httpClientFactoryType != null) + { + object httpClientFactory = serviceProvider.GetService(httpClientFactoryType); + if (httpClientFactory != null) + { + MethodInfo createClientMethod = httpClientFactoryType.GetMethod( + "CreateClient", + BindingFlags.Public | BindingFlags.Instance, + binder: null, + new Type[] { typeof(string) }, + modifiers: null); + if (createClientMethod != null) + { + HttpClient client = (HttpClient)createClientMethod.Invoke(httpClientFactory, new object[] { httpClientName }); + + client.Timeout = TimeSpan.FromMilliseconds(options.TimeoutMilliseconds); + + return client; + } + } + } + + return options.DefaultHttpClientFactory(); + }; + } + } + + internal static void AppendExportPath(this OtlpExporterOptions options, Uri initialEndpoint, string exportRelativePath) + { + // The exportRelativePath is only appended when the options.Endpoint property wasn't set by the user, + // the protocol is HttpProtobuf and the OTEL_EXPORTER_OTLP_ENDPOINT environment variable + // is present. If the user provides a custom value for options.Endpoint that value is taken as is. + if (ReferenceEquals(initialEndpoint, options.Endpoint)) + { + if (options.Protocol == OtlpExportProtocol.HttpProtobuf) + { + if (EnvironmentVariableHelper.LoadUri(OtlpExporterOptions.EndpointEnvVarName, out Uri endpoint)) + { + // At this point we can conclude that endpoint was initialized from OTEL_EXPORTER_OTLP_ENDPOINT + // and has to be appended by export relative path (traces/metrics). + options.Endpoint = endpoint.AppendPathIfNotPresent(exportRelativePath); + } + } + } + } + + internal static Uri AppendPathIfNotPresent(this Uri uri, string path) + { + var absoluteUri = uri.AbsoluteUri; + var separator = string.Empty; + + if (absoluteUri.EndsWith("/")) + { + // Endpoint already ends with 'path/' + if (absoluteUri.EndsWith(string.Concat(path, "/"), StringComparison.OrdinalIgnoreCase)) + { + return uri; + } + } + else + { + // Endpoint already ends with 'path' + if (absoluteUri.EndsWith(path, StringComparison.OrdinalIgnoreCase)) + { + return uri; + } + + separator = "/"; + } + + return new Uri(string.Concat(uri.AbsoluteUri, separator, path)); + } + } +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptionsGrpcExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptionsGrpcExtensions.cs deleted file mode 100644 index f3750e654db..00000000000 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpExporterOptionsGrpcExtensions.cs +++ /dev/null @@ -1,82 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -using System; -using Grpc.Core; -#if NETSTANDARD2_1 -using Grpc.Net.Client; -#endif - -namespace OpenTelemetry.Exporter.OpenTelemetryProtocol -{ - internal static class OtlpExporterOptionsGrpcExtensions - { -#if NETSTANDARD2_1 - 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."); - } - -#if NETSTANDARD2_1 - 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) - { - var headers = options.Headers; - var metadata = new Metadata(); - if (!string.IsNullOrEmpty(headers)) - { - Array.ForEach( - headers.Split(','), - (pair) => - { - // Specify the maximum number of substrings to return to 2 - // This treats everything that follows the first `=` in the string as the value to be added for the metadata key - var keyValueData = pair.Split(new char[] { '=' }, 2); - if (keyValueData.Length != 2) - { - throw new ArgumentException("Headers provided in an invalid format."); - } - - var key = keyValueData[0].Trim(); - var value = keyValueData[1].Trim(); - metadata.Add(key, value); - }); - } - - return metadata; - } - } -} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpLogExporter.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpLogExporter.cs new file mode 100644 index 00000000000..c033436b392 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpLogExporter.cs @@ -0,0 +1,97 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; +using OpenTelemetry.Logs; +using OtlpCollector = Opentelemetry.Proto.Collector.Logs.V1; +using OtlpResource = Opentelemetry.Proto.Resource.V1; + +namespace OpenTelemetry.Exporter +{ + /// + /// Exporter consuming and exporting the data using + /// the OpenTelemetry protocol (OTLP). + /// + internal class OtlpLogExporter : BaseExporter + { + private readonly IExportClient exportClient; + + private OtlpResource.Resource processResource; + + /// + /// Initializes a new instance of the class. + /// + /// Configuration options for the exporter. + public OtlpLogExporter(OtlpExporterOptions options) + : this(options, null) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Configuration options for the exporter. + /// Client used for sending export request. + internal OtlpLogExporter(OtlpExporterOptions options, IExportClient exportClient = null) + { + if (exportClient != null) + { + this.exportClient = exportClient; + } + else + { + // TODO: this instantiation should be aligned with the protocol option (grpc or http/protobuf) when OtlpHttpMetricsExportClient will be implemented. + this.exportClient = new OtlpGrpcLogExportClient(options); + } + } + + internal OtlpResource.Resource ProcessResource => this.processResource ??= this.ParentProvider.GetResource().ToOtlpResource(); + + /// + public override ExportResult Export(in Batch logRecordBatch) + { + // Prevents the exporter's gRPC and HTTP operations from being instrumented. + using var scope = SuppressInstrumentationScope.Begin(); + + var request = new OtlpCollector.ExportLogsServiceRequest(); + + request.AddBatch(this.ProcessResource, logRecordBatch); + + try + { + if (!this.exportClient.SendExportRequest(request)) + { + return ExportResult.Failure; + } + } + catch (Exception ex) + { + OpenTelemetryProtocolExporterEventSource.Log.ExportMethodException(ex); + return ExportResult.Failure; + } + + return ExportResult.Success; + } + + /// + protected override bool OnShutdown(int timeoutMilliseconds) + { + return this.exportClient?.Shutdown(timeoutMilliseconds) ?? true; + } + } +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricsExporter.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporter.cs similarity index 56% rename from src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricsExporter.cs rename to src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporter.cs index 46d87418d40..2cb2aae876d 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricsExporter.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporter.cs @@ -1,4 +1,4 @@ -// +// // Copyright The OpenTelemetry Authors // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,69 +15,68 @@ // using System; -using Grpc.Core; -using OpenTelemetry.Exporter.OpenTelemetryProtocol; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; using OpenTelemetry.Metrics; using OtlpCollector = Opentelemetry.Proto.Collector.Metrics.V1; +using OtlpResource = Opentelemetry.Proto.Resource.V1; namespace OpenTelemetry.Exporter { /// - /// Exporter consuming and exporting the data using + /// Exporter consuming and exporting the data using /// the OpenTelemetry protocol (OTLP). /// - public class OtlpMetricsExporter : BaseOtlpExporter + public class OtlpMetricExporter : BaseExporter { - private readonly OtlpCollector.MetricsService.IMetricsServiceClient metricsClient; + private readonly IExportClient exportClient; + + private OtlpResource.Resource processResource; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// Configuration options for the exporter. - public OtlpMetricsExporter(OtlpExporterOptions options) + public OtlpMetricExporter(OtlpExporterOptions options) : this(options, null) { } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - /// Configuration options for the exporter. - /// . - internal OtlpMetricsExporter(OtlpExporterOptions options, OtlpCollector.MetricsService.IMetricsServiceClient metricsServiceClient = null) - : base(options) + /// Configuration options for the export. + /// Client used for sending export request. + internal OtlpMetricExporter(OtlpExporterOptions options, IExportClient exportClient = null) { - if (metricsServiceClient != null) + if (exportClient != null) { - this.metricsClient = metricsServiceClient; + this.exportClient = exportClient; } else { - this.Channel = options.CreateChannel(); - this.metricsClient = new OtlpCollector.MetricsService.MetricsServiceClient(this.Channel); + this.exportClient = options.GetMetricsExportClient(); } } + internal OtlpResource.Resource ProcessResource => this.processResource ??= this.ParentProvider.GetResource().ToOtlpResource(); + /// - public override ExportResult Export(in Batch batch) + public override ExportResult Export(in Batch metrics) { // Prevents the exporter's gRPC and HTTP operations from being instrumented. using var scope = SuppressInstrumentationScope.Begin(); var request = new OtlpCollector.ExportMetricsServiceRequest(); - request.AddBatch(this.ProcessResource, batch); - var deadline = DateTime.UtcNow.AddMilliseconds(this.Options.TimeoutMilliseconds); + request.AddMetrics(this.ProcessResource, metrics); try { - this.metricsClient.Export(request, headers: this.Headers, deadline: deadline); - } - catch (RpcException ex) - { - OpenTelemetryProtocolExporterEventSource.Log.FailedToReachCollector(ex); - return ExportResult.Failure; + if (!this.exportClient.SendExportRequest(request)) + { + return ExportResult.Failure; + } } catch (Exception ex) { @@ -91,5 +90,11 @@ public override ExportResult Export(in Batch batch) return ExportResult.Success; } + + /// + protected override bool OnShutdown(int timeoutMilliseconds) + { + return this.exportClient?.Shutdown(timeoutMilliseconds) ?? true; + } } } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporterExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporterExtensions.cs new file mode 100644 index 00000000000..fc0956d0ad5 --- /dev/null +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporterExtensions.cs @@ -0,0 +1,73 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using OpenTelemetry.Exporter; +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Metrics +{ + /// + /// Extension methods to simplify registering of the OpenTelemetry Protocol (OTLP) exporter. + /// + public static class OtlpMetricExporterExtensions + { + /// + /// Adds to the . + /// + /// builder to use. + /// Exporter configuration options. + /// The instance of to chain the calls. + public static MeterProviderBuilder AddOtlpExporter(this MeterProviderBuilder builder, Action configure = null) + { + Guard.ThrowIfNull(builder, nameof(builder)); + + if (builder is IDeferredMeterProviderBuilder deferredMeterProviderBuilder) + { + return deferredMeterProviderBuilder.Configure((sp, builder) => + { + AddOtlpExporter(builder, sp.GetOptions(), configure, sp); + }); + } + + return AddOtlpExporter(builder, new OtlpExporterOptions(), configure, serviceProvider: null); + } + + private static MeterProviderBuilder AddOtlpExporter( + MeterProviderBuilder builder, + OtlpExporterOptions options, + Action configure, + IServiceProvider serviceProvider) + { + var initialEndpoint = options.Endpoint; + + configure?.Invoke(options); + + options.TryEnableIHttpClientFactoryIntegration(serviceProvider, "OtlpMetricExporter"); + + options.AppendExportPath(initialEndpoint, OtlpExporterOptions.MetricsExportPath); + + var metricExporter = new OtlpMetricExporter(options); + + var metricReader = options.MetricReaderType == MetricReaderType.Manual + ? new BaseExportingMetricReader(metricExporter) + : new PeriodicExportingMetricReader(metricExporter, options.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds); + + metricReader.Temporality = options.AggregationTemporality; + return builder.AddReader(metricReader); + } + } +} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporterHelperExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporterHelperExtensions.cs deleted file mode 100644 index de2bac2424c..00000000000 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpMetricExporterHelperExtensions.cs +++ /dev/null @@ -1,45 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -using System; -using OpenTelemetry.Exporter; - -namespace OpenTelemetry.Metrics -{ - /// - /// Extension methods to simplify registering of the OpenTelemetry Protocol (OTLP) exporter. - /// - public static class OtlpMetricExporterHelperExtensions - { - /// - /// Adds OpenTelemetry Protocol (OTLP) exporter to the MeterProvider. - /// - /// builder to use. - /// Exporter configuration options. - /// The instance of to chain the calls. - public static MeterProviderBuilder AddOtlpExporter(this MeterProviderBuilder builder, Action configure = null) - { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } - - var options = new OtlpExporterOptions(); - configure?.Invoke(options); - return builder.AddMetricProcessor(new PushMetricProcessor(new OtlpMetricsExporter(options), options.MetricExportIntervalMilliseconds, options.IsDelta)); - } - } -} diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporter.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporter.cs index 086216008ce..1a846dd6575 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporter.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporter.cs @@ -16,10 +16,10 @@ using System; using System.Diagnostics; -using Grpc.Core; -using OpenTelemetry.Exporter.OpenTelemetryProtocol; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; using OtlpCollector = Opentelemetry.Proto.Collector.Trace.V1; +using OtlpResource = Opentelemetry.Proto.Resource.V1; namespace OpenTelemetry.Exporter { @@ -27,14 +27,16 @@ namespace OpenTelemetry.Exporter /// Exporter consuming and exporting the data using /// the OpenTelemetry protocol (OTLP). /// - public class OtlpTraceExporter : BaseOtlpExporter + public class OtlpTraceExporter : BaseExporter { - private readonly OtlpCollector.TraceService.ITraceServiceClient traceClient; + private readonly IExportClient exportClient; + + private OtlpResource.Resource processResource; /// /// Initializes a new instance of the class. /// - /// Configuration options for the exporter. + /// Configuration options for the export. public OtlpTraceExporter(OtlpExporterOptions options) : this(options, null) { @@ -43,42 +45,38 @@ public OtlpTraceExporter(OtlpExporterOptions options) /// /// Initializes a new instance of the class. /// - /// Configuration options for the exporter. - /// . - internal OtlpTraceExporter(OtlpExporterOptions options, OtlpCollector.TraceService.ITraceServiceClient traceServiceClient = null) - : base(options) + /// Configuration options for the export. + /// Client used for sending export request. + internal OtlpTraceExporter(OtlpExporterOptions options, IExportClient exportClient = null) { - if (traceServiceClient != null) + if (exportClient != null) { - this.traceClient = traceServiceClient; + this.exportClient = exportClient; } else { - this.Channel = options.CreateChannel(); - this.traceClient = new OtlpCollector.TraceService.TraceServiceClient(this.Channel); + this.exportClient = options.GetTraceExportClient(); } } + internal OtlpResource.Resource ProcessResource => this.processResource ??= this.ParentProvider.GetResource().ToOtlpResource(); + /// public override ExportResult Export(in Batch activityBatch) { // Prevents the exporter's gRPC and HTTP operations from being instrumented. using var scope = SuppressInstrumentationScope.Begin(); - OtlpCollector.ExportTraceServiceRequest request = new OtlpCollector.ExportTraceServiceRequest(); + var request = new OtlpCollector.ExportTraceServiceRequest(); request.AddBatch(this.ProcessResource, activityBatch); - var deadline = DateTime.UtcNow.AddMilliseconds(this.Options.TimeoutMilliseconds); try { - this.traceClient.Export(request, headers: this.Headers, deadline: deadline); - } - catch (RpcException ex) - { - OpenTelemetryProtocolExporterEventSource.Log.FailedToReachCollector(ex); - - return ExportResult.Failure; + if (!this.exportClient.SendExportRequest(request)) + { + return ExportResult.Failure; + } } catch (Exception ex) { @@ -93,5 +91,11 @@ public override ExportResult Export(in Batch activityBatch) return ExportResult.Success; } + + /// + protected override bool OnShutdown(int timeoutMilliseconds) + { + return this.exportClient?.Shutdown(timeoutMilliseconds) ?? true; + } } } diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporterHelperExtensions.cs b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporterHelperExtensions.cs index ebb9e8f8d8f..d9e66316919 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporterHelperExtensions.cs +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/OtlpTraceExporterHelperExtensions.cs @@ -16,6 +16,7 @@ using System; using OpenTelemetry.Exporter; +using OpenTelemetry.Internal; namespace OpenTelemetry.Trace { @@ -32,25 +33,33 @@ public static class OtlpTraceExporterHelperExtensions /// The instance of to chain the calls. public static TracerProviderBuilder AddOtlpExporter(this TracerProviderBuilder builder, Action configure = null) { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } + Guard.ThrowIfNull(builder, nameof(builder)); if (builder is IDeferredTracerProviderBuilder deferredTracerProviderBuilder) { return deferredTracerProviderBuilder.Configure((sp, builder) => { - AddOtlpExporter(builder, sp.GetOptions(), configure); + AddOtlpExporter(builder, sp.GetOptions(), configure, sp); }); } - return AddOtlpExporter(builder, new OtlpExporterOptions(), configure); + return AddOtlpExporter(builder, new OtlpExporterOptions(), configure, serviceProvider: null); } - private static TracerProviderBuilder AddOtlpExporter(TracerProviderBuilder builder, OtlpExporterOptions exporterOptions, Action configure = null) + private static TracerProviderBuilder AddOtlpExporter( + TracerProviderBuilder builder, + OtlpExporterOptions exporterOptions, + Action configure, + IServiceProvider serviceProvider) { + var originalEndpoint = exporterOptions.Endpoint; + configure?.Invoke(exporterOptions); + + exporterOptions.TryEnableIHttpClientFactoryIntegration(serviceProvider, "OtlpTraceExporter"); + + exporterOptions.AppendExportPath(originalEndpoint, OtlpExporterOptions.TracesExportPath); + var otlpExporter = new OtlpTraceExporter(exporterOptions); if (exporterOptions.ExportProcessorType == ExportProcessorType.Simple) diff --git a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md index 5deba232f19..403227c49e7 100644 --- a/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md +++ b/src/OpenTelemetry.Exporter.OpenTelemetryProtocol/README.md @@ -3,8 +3,8 @@ [![NuGet](https://img.shields.io/nuget/v/OpenTelemetry.Exporter.OpenTelemetryProtocol.svg)](https://www.nuget.org/packages/OpenTelemetry.Exporter.OpenTelemetryProtocol) [![NuGet](https://img.shields.io/nuget/dt/OpenTelemetry.Exporter.OpenTelemetryProtocol.svg)](https://www.nuget.org/packages/OpenTelemetry.Exporter.OpenTelemetryProtocol) -The OTLP (OpenTelemetry Protocol) exporter communicates to an OpenTelemetry -Collector through a gRPC protocol. +[The OTLP (OpenTelemetry Protocol) exporter](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md) +implementation. ## Prerequisite @@ -24,17 +24,28 @@ setters take precedence over the environment variables. ## Options Properties +* `BatchExportProcessorOptions`: Configuration options for the batch exporter. + Only used if ExportProcessorType is set to Batch. + * `Endpoint`: Target to which the exporter is going to send traces or metrics. The endpoint must be a valid Uri with scheme (http or https) and host, and MAY contain a port and path. -* `Headers`: Optional headers for the connection. -* `TimeoutMilliseconds` : Max waiting time for the backend to process a batch. + * `ExportProcessorType`: Whether the exporter should use [Batch or Simple exporting - processor](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#built-in-span-processors) - . -* `BatchExportProcessorOptions`: Configuration options for the batch exporter. - Only used if ExportProcessorType is set to Batch. + processor](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#built-in-span-processors). + +* `Headers`: Optional headers for the connection. + +* `HttpClientFactory`: A factory function called to create the `HttpClient` + instance that will be used at runtime to transmit telemetry over HTTP when the + `HttpProtobuf` protocol is configured. See [Configure + HttpClient](#configure-httpclient) for more details. + +* `TimeoutMilliseconds` : Max waiting time for the backend to process a batch. + +* `Protocol`: OTLP transport protocol. Supported values: + `OtlpExportProtocol.Grpc` and `OtlpExportProtocol.HttpProtobuf`. See the [`TestOtlpExporter.cs`](../../examples/Console/TestOtlpExporter.cs) for an example of how to use the exporter. @@ -45,11 +56,25 @@ The following environment variables can be used to override the default values of the `OtlpExporterOptions` (following the [OpenTelemetry specification](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md)). -| Environment variable | `OtlpExporterOptions` property | -| ------------------------------| -------------------------------| -| `OTEL_EXPORTER_OTLP_ENDPOINT` | `Endpoint` | -| `OTEL_EXPORTER_OTLP_HEADERS` | `Headers` | -| `OTEL_EXPORTER_OTLP_TIMEOUT` | `TimeoutMilliseconds` | +| Environment variable | `OtlpExporterOptions` property | +| ------------------------------| ----------------------------------| +| `OTEL_EXPORTER_OTLP_ENDPOINT` | `Endpoint` | +| `OTEL_EXPORTER_OTLP_HEADERS` | `Headers` | +| `OTEL_EXPORTER_OTLP_TIMEOUT` | `TimeoutMilliseconds` | +| `OTEL_EXPORTER_OTLP_PROTOCOL` | `Protocol` (grpc or http/protobuf)| + +`FormatException` is thrown in case of an invalid value for any of the +supported environment variables. + +## OTLP Logs + +This package currently only supports exporting traces and metrics. Once the +[OTLP log data model](https://github.com/open-telemetry/opentelemetry-proto#maturity-level) +is deemed stable, the OTLP log exporter will be folded into this package. + +In the meantime, support for exporting logs is provided by installing the +[`OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs`](../OpenTelemetry.Exporter.OpenTelemetryProtocol.Logs/README.md) +package. ## Special case when using insecure channel @@ -65,6 +90,43 @@ See [this](https://docs.microsoft.com/aspnet/core/grpc/troubleshoot#call-insecure-grpc-services-with-net-core-client) for more information. +## Configure HttpClient + +The `HttpClientFactory` option is provided on `OtlpExporterOptions` for users +who want to configure the `HttpClient` used by the `OtlpTraceExporter` and/or +`OtlpMetricExporter` when `HttpProtobuf` protocol is used. Simply replace the +function with your own implementation if you want to customize the generated +`HttpClient`: + +```csharp +services.AddOpenTelemetryTracing((builder) => builder + .AddOtlpExporter(o => + { + o.Protocol = OtlpExportProtocol.HttpProtobuf; + o.HttpClientFactory = () => + { + HttpClient client = new HttpClient(); + client.DefaultRequestHeaders.Add("X-MyCustomHeader", "value"); + return client; + }; + })); +``` + +For users using +[IHttpClientFactory](https://docs.microsoft.com/dotnet/architecture/microservices/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests) +you may also customize the named "OtlpTraceExporter" or "OtlpMetricExporter" +`HttpClient` using the built-in `AddHttpClient` extension: + +```csharp +services.AddHttpClient( + "OtlpTraceExporter", + configureClient: (client) => + client.DefaultRequestHeaders.Add("X-MyCustomHeader", "value")); +``` + +Note: The single instance returned by `HttpClientFactory` is reused by all +export requests. + ## References * [OpenTelemetry diff --git a/src/OpenTelemetry/.publicApi/net46/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.Prometheus/.publicApi/net461/PublicAPI.Shipped.txt similarity index 100% rename from src/OpenTelemetry/.publicApi/net46/PublicAPI.Unshipped.txt rename to src/OpenTelemetry.Exporter.Prometheus/.publicApi/net461/PublicAPI.Shipped.txt diff --git a/src/OpenTelemetry.Exporter.Prometheus/.publicApi/net461/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.Prometheus/.publicApi/net461/PublicAPI.Unshipped.txt new file mode 100644 index 00000000000..70bd44b3df7 --- /dev/null +++ b/src/OpenTelemetry.Exporter.Prometheus/.publicApi/net461/PublicAPI.Unshipped.txt @@ -0,0 +1,18 @@ +OpenTelemetry.Exporter.PrometheusExporter +OpenTelemetry.Exporter.PrometheusExporter.Collect.get -> System.Func +OpenTelemetry.Exporter.PrometheusExporter.Collect.set -> void +OpenTelemetry.Exporter.PrometheusExporter.PrometheusExporter(OpenTelemetry.Exporter.PrometheusExporterOptions options) -> void +OpenTelemetry.Exporter.PrometheusExporterOptions +OpenTelemetry.Exporter.PrometheusExporterOptions.HttpListenerPrefixes.get -> System.Collections.Generic.IReadOnlyCollection +OpenTelemetry.Exporter.PrometheusExporterOptions.HttpListenerPrefixes.set -> void +OpenTelemetry.Exporter.PrometheusExporterOptions.PrometheusExporterOptions() -> void +OpenTelemetry.Exporter.PrometheusExporterOptions.ScrapeEndpointPath.get -> string +OpenTelemetry.Exporter.PrometheusExporterOptions.ScrapeEndpointPath.set -> void +OpenTelemetry.Exporter.PrometheusExporterOptions.ScrapeResponseCacheDurationMilliseconds.get -> int +OpenTelemetry.Exporter.PrometheusExporterOptions.ScrapeResponseCacheDurationMilliseconds.set -> void +OpenTelemetry.Exporter.PrometheusExporterOptions.StartHttpListener.get -> bool +OpenTelemetry.Exporter.PrometheusExporterOptions.StartHttpListener.set -> void +OpenTelemetry.Metrics.PrometheusExporterMeterProviderBuilderExtensions +override OpenTelemetry.Exporter.PrometheusExporter.Dispose(bool disposing) -> void +override OpenTelemetry.Exporter.PrometheusExporter.Export(in OpenTelemetry.Batch metrics) -> OpenTelemetry.ExportResult +static OpenTelemetry.Metrics.PrometheusExporterMeterProviderBuilderExtensions.AddPrometheusExporter(this OpenTelemetry.Metrics.MeterProviderBuilder builder, System.Action configure = null) -> OpenTelemetry.Metrics.MeterProviderBuilder \ No newline at end of file diff --git a/src/OpenTelemetry.Exporter.Prometheus/.publicApi/netcoreapp3.1/PublicAPI.Shipped.txt b/src/OpenTelemetry.Exporter.Prometheus/.publicApi/netcoreapp3.1/PublicAPI.Shipped.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/OpenTelemetry.Exporter.Prometheus/.publicApi/netcoreapp3.1/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.Prometheus/.publicApi/netcoreapp3.1/PublicAPI.Unshipped.txt new file mode 100644 index 00000000000..fa2cedde662 --- /dev/null +++ b/src/OpenTelemetry.Exporter.Prometheus/.publicApi/netcoreapp3.1/PublicAPI.Unshipped.txt @@ -0,0 +1,20 @@ +Microsoft.AspNetCore.Builder.PrometheusExporterApplicationBuilderExtensions +OpenTelemetry.Exporter.PrometheusExporter +OpenTelemetry.Exporter.PrometheusExporter.Collect.get -> System.Func +OpenTelemetry.Exporter.PrometheusExporter.Collect.set -> void +OpenTelemetry.Exporter.PrometheusExporter.PrometheusExporter(OpenTelemetry.Exporter.PrometheusExporterOptions options) -> void +OpenTelemetry.Exporter.PrometheusExporterOptions +OpenTelemetry.Exporter.PrometheusExporterOptions.HttpListenerPrefixes.get -> System.Collections.Generic.IReadOnlyCollection +OpenTelemetry.Exporter.PrometheusExporterOptions.HttpListenerPrefixes.set -> void +OpenTelemetry.Exporter.PrometheusExporterOptions.PrometheusExporterOptions() -> void +OpenTelemetry.Exporter.PrometheusExporterOptions.ScrapeEndpointPath.get -> string +OpenTelemetry.Exporter.PrometheusExporterOptions.ScrapeEndpointPath.set -> void +OpenTelemetry.Exporter.PrometheusExporterOptions.ScrapeResponseCacheDurationMilliseconds.get -> int +OpenTelemetry.Exporter.PrometheusExporterOptions.ScrapeResponseCacheDurationMilliseconds.set -> void +OpenTelemetry.Exporter.PrometheusExporterOptions.StartHttpListener.get -> bool +OpenTelemetry.Exporter.PrometheusExporterOptions.StartHttpListener.set -> void +OpenTelemetry.Metrics.PrometheusExporterMeterProviderBuilderExtensions +override OpenTelemetry.Exporter.PrometheusExporter.Dispose(bool disposing) -> void +override OpenTelemetry.Exporter.PrometheusExporter.Export(in OpenTelemetry.Batch metrics) -> OpenTelemetry.ExportResult +static Microsoft.AspNetCore.Builder.PrometheusExporterApplicationBuilderExtensions.UseOpenTelemetryPrometheusScrapingEndpoint(this Microsoft.AspNetCore.Builder.IApplicationBuilder app, OpenTelemetry.Metrics.MeterProvider meterProvider = null) -> Microsoft.AspNetCore.Builder.IApplicationBuilder +static OpenTelemetry.Metrics.PrometheusExporterMeterProviderBuilderExtensions.AddPrometheusExporter(this OpenTelemetry.Metrics.MeterProviderBuilder builder, System.Action configure = null) -> OpenTelemetry.Metrics.MeterProviderBuilder \ No newline at end of file diff --git a/src/OpenTelemetry.Exporter.Prometheus/AssemblyInfo.cs b/src/OpenTelemetry.Exporter.Prometheus/AssemblyInfo.cs new file mode 100644 index 00000000000..619668af3b4 --- /dev/null +++ b/src/OpenTelemetry.Exporter.Prometheus/AssemblyInfo.cs @@ -0,0 +1,25 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Runtime.CompilerServices; + +#if SIGNED +[assembly: InternalsVisibleTo("Benchmarks, PublicKey=002400000480000094000000060200000024000052534131000400000100010051c1562a090fb0c9f391012a32198b5e5d9a60e9b80fa2d7b434c9e5ccb7259bd606e66f9660676afc6692b8cdc6793d190904551d2103b7b22fa636dcbb8208839785ba402ea08fc00c8f1500ccef28bbf599aa64ffb1e1d5dc1bf3420a3777badfe697856e9d52070a50c3ea5821c80bef17ca3acffa28f89dd413f096f898")] +[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.Prometheus.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010051c1562a090fb0c9f391012a32198b5e5d9a60e9b80fa2d7b434c9e5ccb7259bd606e66f9660676afc6692b8cdc6793d190904551d2103b7b22fa636dcbb8208839785ba402ea08fc00c8f1500ccef28bbf599aa64ffb1e1d5dc1bf3420a3777badfe697856e9d52070a50c3ea5821c80bef17ca3acffa28f89dd413f096f898")] +#else +[assembly: InternalsVisibleTo("Benchmarks")] +[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.Prometheus.Tests")] +#endif diff --git a/src/OpenTelemetry.Exporter.Prometheus/CHANGELOG.md b/src/OpenTelemetry.Exporter.Prometheus/CHANGELOG.md index 62f86e96ddd..22d45e2c8ff 100644 --- a/src/OpenTelemetry.Exporter.Prometheus/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Prometheus/CHANGELOG.md @@ -2,6 +2,47 @@ ## Unreleased +* Update default `httpListenerPrefixes` for PrometheusExporter to be `http://localhost:9464/`. +([2783](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2783)) + +## 1.2.0-rc1 + +Released 2021-Nov-29 + +* Bug fix for handling Histogram with empty buckets. + ([#2651](https://github.com/open-telemetry/opentelemetry-dotnet/issues/2651)) + +## 1.2.0-beta2 + +Released 2021-Nov-19 + +* Added scrape endpoint response caching feature & + `ScrapeResponseCacheDurationMilliseconds` option + ([#2610](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2610)) + +## 1.2.0-beta1 + +Released 2021-Oct-08 + +## 1.2.0-alpha4 + +Released 2021-Sep-23 + +## 1.2.0-alpha3 + +Released 2021-Sep-13 + +* Bug fixes + ([#2289](https://github.com/open-telemetry/opentelemetry-dotnet/issues/2289)) + ([#2309](https://github.com/open-telemetry/opentelemetry-dotnet/issues/2309)) + +## 1.2.0-alpha2 + +Released 2021-Aug-24 + +* Revamped to support the new Metrics API/SDK. + Supports Counter, Gauge and Histogram. + ## 1.0.0-rc1.1 Released 2020-Nov-17 diff --git a/src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusCollectionManager.cs b/src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusCollectionManager.cs new file mode 100644 index 00000000000..8f672645ba0 --- /dev/null +++ b/src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusCollectionManager.cs @@ -0,0 +1,240 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using OpenTelemetry.Metrics; + +namespace OpenTelemetry.Exporter.Prometheus +{ + internal sealed class PrometheusCollectionManager + { + private readonly PrometheusExporter exporter; + private readonly int scrapeResponseCacheDurationInMilliseconds; + private readonly Func, ExportResult> onCollectRef; + private byte[] buffer = new byte[85000]; // encourage the object to live in LOH (large object heap) + private int globalLockState; + private ArraySegment previousDataView; + private DateTime? previousDataViewGeneratedAtUtc; + private int readerCount; + private bool collectionRunning; + private TaskCompletionSource collectionTcs; + + public PrometheusCollectionManager(PrometheusExporter exporter) + { + this.exporter = exporter; + this.scrapeResponseCacheDurationInMilliseconds = this.exporter.Options.ScrapeResponseCacheDurationMilliseconds; + this.onCollectRef = this.OnCollect; + } + +#if NETCOREAPP3_1_OR_GREATER + public ValueTask EnterCollect() +#else + public Task EnterCollect() +#endif + { + this.EnterGlobalLock(); + + // If we are within {ScrapeResponseCacheDurationMilliseconds} of the + // last successful collect, return the previous view. + if (this.previousDataViewGeneratedAtUtc.HasValue + && this.scrapeResponseCacheDurationInMilliseconds > 0 + && this.previousDataViewGeneratedAtUtc.Value.AddMilliseconds(this.scrapeResponseCacheDurationInMilliseconds) >= DateTime.UtcNow) + { + Interlocked.Increment(ref this.readerCount); + this.ExitGlobalLock(); +#if NETCOREAPP3_1_OR_GREATER + return new ValueTask(new CollectionResponse(this.previousDataView, this.previousDataViewGeneratedAtUtc.Value, fromCache: true)); +#else + return Task.FromResult(new CollectionResponse(this.previousDataView, this.previousDataViewGeneratedAtUtc.Value, fromCache: true)); +#endif + } + + // 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); + } + + Interlocked.Increment(ref this.readerCount); + this.ExitGlobalLock(); +#if NETCOREAPP3_1_OR_GREATER + return new ValueTask(this.collectionTcs.Task); +#else + return this.collectionTcs.Task; +#endif + } + + this.WaitForReadersToComplete(); + + // Start a collection on the current thread. + this.collectionRunning = true; + this.previousDataViewGeneratedAtUtc = null; + Interlocked.Increment(ref this.readerCount); + this.ExitGlobalLock(); + + CollectionResponse response; + bool result = this.ExecuteCollect(); + if (result) + { + this.previousDataViewGeneratedAtUtc = DateTime.UtcNow; + response = new CollectionResponse(this.previousDataView, this.previousDataViewGeneratedAtUtc.Value, fromCache: false); + } + else + { + response = default; + } + + this.EnterGlobalLock(); + + this.collectionRunning = false; + + if (this.collectionTcs != null) + { + this.collectionTcs.SetResult(response); + this.collectionTcs = null; + } + + this.ExitGlobalLock(); + +#if NETCOREAPP3_1_OR_GREATER + return new ValueTask(response); +#else + return Task.FromResult(response); +#endif + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void ExitCollect() + { + Interlocked.Decrement(ref this.readerCount); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void EnterGlobalLock() + { + SpinWait lockWait = default; + while (true) + { + if (Interlocked.CompareExchange(ref this.globalLockState, 1, this.globalLockState) != 0) + { + lockWait.SpinOnce(); + continue; + } + + break; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ExitGlobalLock() + { + this.globalLockState = 0; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void WaitForReadersToComplete() + { + SpinWait readWait = default; + while (true) + { + if (Interlocked.CompareExchange(ref this.readerCount, 0, this.readerCount) != 0) + { + readWait.SpinOnce(); + continue; + } + + break; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private bool ExecuteCollect() + { + this.exporter.OnExport = this.onCollectRef; + bool result = this.exporter.Collect(Timeout.Infinite); + this.exporter.OnExport = null; + return result; + } + + private ExportResult OnCollect(Batch metrics) + { + int cursor = 0; + + try + { + foreach (var metric in metrics) + { + while (true) + { + try + { + cursor = PrometheusSerializer.WriteMetric(this.buffer, cursor, metric); + break; + } + catch (IndexOutOfRangeException) + { + int bufferSize = this.buffer.Length * 2; + + // there are two cases we might run into the following condition: + // 1. we have many metrics to be exported - in this case we probably want + // to put some upper limit and allow the user to configure it. + // 2. we got an IndexOutOfRangeException which was triggered by some other + // code instead of the buffer[cursor++] - in this case we should give up + // at certain point rather than allocating like crazy. + if (bufferSize > 100 * 1024 * 1024) + { + throw; + } + + var newBuffer = new byte[bufferSize]; + this.buffer.CopyTo(newBuffer, 0); + this.buffer = newBuffer; + } + } + } + + this.previousDataView = new ArraySegment(this.buffer, 0, Math.Max(cursor - 1, 0)); + return ExportResult.Success; + } + catch (Exception) + { + this.previousDataView = new ArraySegment(Array.Empty(), 0, 0); + return ExportResult.Failure; + } + } + + public readonly struct CollectionResponse + { + public CollectionResponse(ArraySegment view, DateTime generatedAtUtc, bool fromCache) + { + this.View = view; + this.GeneratedAtUtc = generatedAtUtc; + this.FromCache = fromCache; + } + + public ArraySegment View { get; } + + public DateTime GeneratedAtUtc { get; } + + public bool FromCache { get; } + } + } +} diff --git a/src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusExporterEventSource.cs b/src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusExporterEventSource.cs index cd577f4f5aa..0523d120e73 100644 --- a/src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusExporterEventSource.cs +++ b/src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusExporterEventSource.cs @@ -18,13 +18,13 @@ using System.Diagnostics.Tracing; using OpenTelemetry.Internal; -namespace OpenTelemetry.Exporter.Prometheus.Implementation +namespace OpenTelemetry.Exporter.Prometheus { /// /// EventSource events emitted from the project. /// [EventSource(Name = "OpenTelemetry-Exporter-Prometheus")] - internal class PrometheusExporterEventSource : EventSource + internal sealed class PrometheusExporterEventSource : EventSource { public static PrometheusExporterEventSource Log = new PrometheusExporterEventSource(); diff --git a/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporterMetricsHttpServer.cs b/src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusExporterHttpServer.cs similarity index 56% rename from src/OpenTelemetry.Exporter.Prometheus/PrometheusExporterMetricsHttpServer.cs rename to src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusExporterHttpServer.cs index 73266aba29a..eada5dc0164 100644 --- a/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporterMetricsHttpServer.cs +++ b/src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusExporterHttpServer.cs @@ -1,4 +1,4 @@ -// +// // Copyright The OpenTelemetry Authors // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,18 +15,17 @@ // using System; -using System.IO; using System.Net; using System.Threading; using System.Threading.Tasks; -using OpenTelemetry.Exporter.Prometheus.Implementation; +using OpenTelemetry.Internal; -namespace OpenTelemetry.Exporter +namespace OpenTelemetry.Exporter.Prometheus { /// - /// A HTTP listener used to expose Prometheus metrics. + /// An HTTP listener used to expose Prometheus metrics. /// - public class PrometheusExporterMetricsHttpServer : IDisposable + internal sealed class PrometheusExporterHttpServer : IDisposable { private readonly PrometheusExporter exporter; private readonly HttpListener httpListener = new HttpListener(); @@ -36,19 +35,40 @@ public class PrometheusExporterMetricsHttpServer : IDisposable private Task workerThread; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The instance. - public PrometheusExporterMetricsHttpServer(PrometheusExporter exporter) + public PrometheusExporterHttpServer(PrometheusExporter exporter) { - this.exporter = exporter ?? throw new ArgumentNullException(nameof(exporter)); - this.httpListener.Prefixes.Add(exporter.Options.Url); + Guard.ThrowIfNull(exporter, nameof(exporter)); + + this.exporter = exporter; + if ((exporter.Options.HttpListenerPrefixes?.Count ?? 0) <= 0) + { + throw new ArgumentException("No HttpListenerPrefixes were specified on PrometheusExporterOptions."); + } + + string path = exporter.Options.ScrapeEndpointPath ?? PrometheusExporterOptions.DefaultScrapeEndpointPath; + if (!path.StartsWith("/")) + { + path = $"/{path}"; + } + + if (!path.EndsWith("/")) + { + path = $"{path}/"; + } + + foreach (string prefix in exporter.Options.HttpListenerPrefixes) + { + this.httpListener.Prefixes.Add($"{prefix.TrimEnd('/')}{path}"); + } } /// /// Start exporter. /// - /// An optional that can be used to stop the htto server. + /// An optional that can be used to stop the HTTP server. public void Start(CancellationToken token = default) { lock (this.syncObject) @@ -63,7 +83,7 @@ public void Start(CancellationToken token = default) new CancellationTokenSource() : CancellationTokenSource.CreateLinkedTokenSource(token); - this.workerThread = Task.Factory.StartNew(this.WorkerThread, default, TaskCreationOptions.LongRunning, TaskScheduler.Default); + this.workerThread = Task.Factory.StartNew(this.WorkerProc, default, TaskCreationOptions.LongRunning, TaskScheduler.Default); } } @@ -87,16 +107,6 @@ public void Stop() /// public void Dispose() - { - this.Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Releases the unmanaged resources used by this class and optionally releases the managed resources. - /// - /// to release both managed and unmanaged resources; to release only unmanaged resources. - protected virtual void Dispose(bool disposing) { if (this.httpListener != null && this.httpListener.IsListening) { @@ -105,7 +115,7 @@ protected virtual void Dispose(bool disposing) } } - private void WorkerThread() + private void WorkerProc() { this.httpListener.Start(); @@ -116,26 +126,15 @@ private void WorkerThread() { var ctxTask = this.httpListener.GetContextAsync(); ctxTask.Wait(this.tokenSource.Token); - var ctx = ctxTask.Result; - ctx.Response.StatusCode = 200; - ctx.Response.ContentType = PrometheusMetricBuilder.ContentType; - - using var output = ctx.Response.OutputStream; - using var writer = new StreamWriter(output); - this.exporter.MakePullRequest(); - this.exporter.WriteMetricsCollection(writer); + Task.Run(() => this.ProcessRequestAsync(ctx)); } } catch (OperationCanceledException ex) { PrometheusExporterEventSource.Log.CanceledExport(ex); } - catch (Exception ex) - { - PrometheusExporterEventSource.Log.FailedExport(ex); - } finally { try @@ -149,5 +148,47 @@ private void WorkerThread() } } } + + private async Task ProcessRequestAsync(HttpListenerContext context) + { + try + { + var collectionResponse = await this.exporter.CollectionManager.EnterCollect().ConfigureAwait(false); + try + { + if (collectionResponse.View.Count > 0) + { + context.Response.StatusCode = 200; + context.Response.Headers.Add("Server", string.Empty); + context.Response.Headers.Add("Last-Modified", collectionResponse.GeneratedAtUtc.ToString("R")); + context.Response.ContentType = "text/plain; charset=utf-8; version=0.0.4"; + + await context.Response.OutputStream.WriteAsync(collectionResponse.View.Array, 0, collectionResponse.View.Count).ConfigureAwait(false); + } + else + { + throw new InvalidOperationException("Collection failure."); + } + } + finally + { + this.exporter.CollectionManager.ExitCollect(); + } + } + catch (Exception ex) + { + PrometheusExporterEventSource.Log.FailedExport(ex); + + context.Response.StatusCode = 500; + } + + try + { + context.Response.Close(); + } + catch + { + } + } } } diff --git a/src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusExporterMiddleware.cs b/src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusExporterMiddleware.cs new file mode 100644 index 00000000000..b76342ca501 --- /dev/null +++ b/src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusExporterMiddleware.cs @@ -0,0 +1,103 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#if NETCOREAPP3_1_OR_GREATER +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using OpenTelemetry.Internal; +using OpenTelemetry.Metrics; + +namespace OpenTelemetry.Exporter.Prometheus +{ + /// + /// ASP.NET Core middleware for exposing a Prometheus metrics scraping endpoint. + /// + internal sealed class PrometheusExporterMiddleware + { + private readonly PrometheusExporter exporter; + + /// + /// Initializes a new instance of the class. + /// + /// . + /// . + public PrometheusExporterMiddleware(MeterProvider meterProvider, RequestDelegate next) + { + Guard.ThrowIfNull(meterProvider, nameof(meterProvider)); + + if (!meterProvider.TryFindExporter(out PrometheusExporter exporter)) + { + throw new ArgumentException("A PrometheusExporter could not be found configured on the provided MeterProvider."); + } + + this.exporter = exporter; + } + + internal PrometheusExporterMiddleware(PrometheusExporter exporter) + { + this.exporter = exporter; + } + + /// + /// Invoke. + /// + /// context. + /// Task. + public async Task InvokeAsync(HttpContext httpContext) + { + Debug.Assert(httpContext != null, "httpContext should not be null"); + + var response = httpContext.Response; + + try + { + var collectionResponse = await this.exporter.CollectionManager.EnterCollect().ConfigureAwait(false); + try + { + if (collectionResponse.View.Count > 0) + { + response.StatusCode = 200; + response.Headers.Add("Last-Modified", collectionResponse.GeneratedAtUtc.ToString("R")); + response.ContentType = "text/plain; charset=utf-8; version=0.0.4"; + + await response.Body.WriteAsync(collectionResponse.View.Array, 0, collectionResponse.View.Count).ConfigureAwait(false); + } + else + { + throw new InvalidOperationException("Collection failure."); + } + } + finally + { + this.exporter.CollectionManager.ExitCollect(); + } + } + catch (Exception ex) + { + PrometheusExporterEventSource.Log.FailedExport(ex); + if (!response.HasStarted) + { + response.StatusCode = 500; + } + } + + this.exporter.OnExport = null; + } + } +} +#endif diff --git a/src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusMetricBuilder.cs b/src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusMetricBuilder.cs deleted file mode 100644 index a11c8e5131d..00000000000 --- a/src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusMetricBuilder.cs +++ /dev/null @@ -1,276 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text; -#if NET452 -using OpenTelemetry.Internal; -#endif - -namespace OpenTelemetry.Exporter.Prometheus.Implementation -{ - internal class PrometheusMetricBuilder - { - public const string ContentType = "text/plain; version = 0.0.4"; - - private static readonly char[] FirstCharacterNameCharset = - { - 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', - 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', - '_', ':', - }; - - private static readonly char[] NameCharset = - { - 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', - 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', - '_', ':', - }; - - private static readonly char[] FirstCharacterLabelCharset = - { - 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', - 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', - '_', - }; - - private static readonly char[] LabelCharset = - { - 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', - 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', - '_', - }; - - private readonly ICollection values = new List(); - - private string name; - private string description; - private string type; - - public PrometheusMetricBuilder WithName(string name) - { - this.name = name; - return this; - } - - public PrometheusMetricBuilder WithDescription(string description) - { - this.description = description; - return this; - } - - public PrometheusMetricBuilder WithType(string type) - { - this.type = type; - return this; - } - - public PrometheusMetricValueBuilder AddValue() - { - var val = new PrometheusMetricValueBuilder(); - - this.values.Add(val); - - return val; - } - - public void Write(StreamWriter writer) - { - // https://prometheus.io/docs/instrumenting/exposition_formats/ - - if (string.IsNullOrEmpty(this.name)) - { - throw new InvalidOperationException("Metric name should not be empty"); - } - - this.name = GetSafeMetricName(this.name); - - if (!string.IsNullOrEmpty(this.description)) - { - // Lines with a # as the first non-whitespace character are comments. - // They are ignored unless the first token after # is either HELP or TYPE. - // Those lines are treated as follows: If the token is HELP, at least one - // more token is expected, which is the metric name. All remaining tokens - // are considered the docstring for that metric name. HELP lines may contain - // any sequence of UTF-8 characters (after the metric name), but the backslash - // and the line feed characters have to be escaped as \\ and \n, respectively. - // Only one HELP line may exist for any given metric name. - - writer.Write("# HELP "); - writer.Write(this.name); - writer.Write(GetSafeMetricDescription(this.description)); - writer.Write("\n"); - } - - if (!string.IsNullOrEmpty(this.type)) - { - // If the token is TYPE, exactly two more tokens are expected. The first is the - // metric name, and the second is either counter, gauge, histogram, summary, or - // untyped, defining the type for the metric of that name. Only one TYPE line - // may exist for a given metric name. The TYPE line for a metric name must appear - // before the first sample is reported for that metric name. If there is no TYPE - // line for a metric name, the type is set to untyped. - - writer.Write("# TYPE "); - writer.Write(this.name); - writer.Write(" "); - writer.Write(this.type); - writer.Write("\n"); - } - - // The remaining lines describe samples (one per line) using the following syntax (EBNF): - // metric_name [ - // "{" label_name "=" `"` label_value `"` { "," label_name "=" `"` label_value `"` } [ "," ] "}" - // ] value [ timestamp ] - // In the sample syntax: - - var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds().ToString(CultureInfo.InvariantCulture); - - foreach (var m in this.values) - { - // metric_name and label_name carry the usual Prometheus expression language restrictions. - writer.Write(m.Name != null ? GetSafeMetricName(m.Name) : this.name); - - // label_value can be any sequence of UTF-8 characters, but the backslash - // (\, double-quote ("}, and line feed (\n) characters have to be escaped - // as \\, \", and \n, respectively. - - if (m.Labels.Count > 0) - { - writer.Write(@"{"); - writer.Write(string.Join(",", m.Labels.Select(x => GetLabelAndValue(x.Item1, x.Item2)))); - writer.Write(@"}"); - } - - // value is a float represented as required by Go's ParseFloat() function. In addition to - // standard numerical values, Nan, +Inf, and -Inf are valid values representing not a number, - // positive infinity, and negative infinity, respectively. - writer.Write(" "); - writer.Write(m.Value.ToString(CultureInfo.InvariantCulture)); - writer.Write(" "); - - // The timestamp is an int64 (milliseconds since epoch, i.e. 1970-01-01 00:00:00 UTC, excluding - // leap seconds), represented as required by Go's ParseInt() function. - writer.Write(now); - - // Prometheus' text-based format is line oriented. Lines are separated - // by a line feed character (\n). The last line must end with a line - // feed character. Empty lines are ignored. - writer.Write("\n"); - } - - static string GetLabelAndValue(string label, string value) - { - var safeKey = GetSafeLabelName(label); - var safeValue = GetSafeLabelValue(value); - return $"{safeKey}=\"{safeValue}\""; - } - } - - private static string GetSafeName(string name, char[] firstCharNameCharset, char[] charNameCharset) - { - // https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels - // - // Metric names and labels - // Every time series is uniquely identified by its metric name and a set of key-value pairs, also known as labels. - // The metric name specifies the general feature of a system that is measured (e.g. http_requests_total - the total number of HTTP requests received). It may contain ASCII letters and digits, as well as underscores and colons. It must match the regex [a-zA-Z_:][a-zA-Z0-9_:]*. - // Note: The colons are reserved for user defined recording rules. They should not be used by exporters or direct instrumentation. - // Labels enable Prometheus's dimensional data model: any given combination of labels for the same metric name identifies a particular dimensional instantiation of that metric (for example: all HTTP requests that used the method POST to the /api/tracks handler). The query language allows filtering and aggregation based on these dimensions. Changing any label value, including adding or removing a label, will create a new time series. - // Label names may contain ASCII letters, numbers, as well as underscores. They must match the regex [a-zA-Z_][a-zA-Z0-9_]*. Label names beginning with __ are reserved for internal use. - // Label values may contain any Unicode characters. - - var sb = new StringBuilder(); - var firstChar = name[0]; - - sb.Append(firstCharNameCharset.Contains(firstChar) - ? firstChar - : GetSafeChar(char.ToLowerInvariant(firstChar), firstCharNameCharset)); - - for (var i = 1; i < name.Length; ++i) - { - sb.Append(GetSafeChar(name[i], charNameCharset)); - } - - return sb.ToString(); - - static char GetSafeChar(char c, char[] charset) => charset.Contains(c) ? c : '_'; - } - - private static string GetSafeMetricName(string name) => GetSafeName(name, FirstCharacterNameCharset, NameCharset); - - private static string GetSafeLabelName(string name) => GetSafeName(name, FirstCharacterLabelCharset, LabelCharset); - - private static string GetSafeLabelValue(string value) - { - // label_value can be any sequence of UTF-8 characters, but the backslash - // (\), double-quote ("), and line feed (\n) characters have to be escaped - // as \\, \", and \n, respectively. - - var result = value.Replace("\\", "\\\\"); - result = result.Replace("\n", "\\n"); - result = result.Replace("\"", "\\\""); - - return result; - } - - private static string GetSafeMetricDescription(string description) - { - // HELP lines may contain any sequence of UTF-8 characters(after the metric name), but the backslash - // and the line feed characters have to be escaped as \\ and \n, respectively.Only one HELP line may - // exist for any given metric name. - var result = description.Replace(@"\", @"\\"); - result = result.Replace("\n", @"\n"); - - return result; - } - - internal class PrometheusMetricValueBuilder - { - public readonly ICollection> Labels = new List>(); - public double Value; - public string Name; - - public PrometheusMetricValueBuilder WithLabel(string name, string value) - { - this.Labels.Add(new Tuple(name, value)); - return this; - } - - public PrometheusMetricValueBuilder WithValue(long metricValue) - { - this.Value = metricValue; - return this; - } - - public PrometheusMetricValueBuilder WithValue(double metricValue) - { - this.Value = metricValue; - return this; - } - - public PrometheusMetricValueBuilder WithName(string name) - { - this.Name = name; - return this; - } - } - } -} diff --git a/src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusSerializer.cs b/src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusSerializer.cs new file mode 100644 index 00000000000..68252e1166f --- /dev/null +++ b/src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusSerializer.cs @@ -0,0 +1,313 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT 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 NETCOREAPP3_1_OR_GREATER +using System; +#endif +using System.Diagnostics; +using System.Globalization; +using System.Runtime.CompilerServices; + +namespace OpenTelemetry.Exporter.Prometheus +{ + /// + /// Basic PrometheusSerializer which has no OpenTelemetry dependency. + /// + internal static partial class PrometheusSerializer + { +#pragma warning disable SA1310 // Field name should not contain an underscore + private const byte ASCII_QUOTATION_MARK = 0x22; // '"' + private const byte ASCII_FULL_STOP = 0x2E; // '.' + private const byte ASCII_HYPHEN_MINUS = 0x2D; // '-' + private const byte ASCII_REVERSE_SOLIDUS = 0x5C; // '\\' + private const byte ASCII_LINEFEED = 0x0A; // `\n` +#pragma warning restore SA1310 // Field name should not contain an underscore + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int WriteDouble(byte[] buffer, int cursor, double value) + { +#if NETCOREAPP3_1_OR_GREATER + if (double.IsFinite(value)) +#else + if (!double.IsInfinity(value) && !double.IsNaN(value)) +#endif + { +#if NETCOREAPP3_1_OR_GREATER + Span span = stackalloc char[128]; + + var result = value.TryFormat(span, out var cchWritten, "G", CultureInfo.InvariantCulture); + Debug.Assert(result, $"{nameof(result)} should be true."); + + for (int i = 0; i < cchWritten; i++) + { + buffer[cursor++] = unchecked((byte)span[i]); + } +#else + cursor = WriteAsciiStringNoEscape(buffer, cursor, value.ToString(CultureInfo.InvariantCulture)); +#endif + } + else if (double.IsPositiveInfinity(value)) + { + cursor = WriteAsciiStringNoEscape(buffer, cursor, "+Inf"); + } + else if (double.IsNegativeInfinity(value)) + { + cursor = WriteAsciiStringNoEscape(buffer, cursor, "-Inf"); + } + else + { + Debug.Assert(double.IsNaN(value), $"{nameof(value)} should be NaN."); + cursor = WriteAsciiStringNoEscape(buffer, cursor, "Nan"); + } + + return cursor; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int WriteLong(byte[] buffer, int cursor, long value) + { +#if NETCOREAPP3_1_OR_GREATER + Span span = stackalloc char[20]; + + var result = value.TryFormat(span, out var cchWritten, "G", CultureInfo.InvariantCulture); + Debug.Assert(result, $"{nameof(result)} should be true."); + + for (int i = 0; i < cchWritten; i++) + { + buffer[cursor++] = unchecked((byte)span[i]); + } +#else + cursor = WriteAsciiStringNoEscape(buffer, cursor, value.ToString(CultureInfo.InvariantCulture)); +#endif + + return cursor; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int WriteAsciiStringNoEscape(byte[] buffer, int cursor, string value) + { + for (int i = 0; i < value.Length; i++) + { + buffer[cursor++] = unchecked((byte)value[i]); + } + + return cursor; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int WriteUnicodeNoEscape(byte[] buffer, int cursor, ushort ordinal) + { + if (ordinal <= 0x7F) + { + buffer[cursor++] = unchecked((byte)ordinal); + } + else if (ordinal <= 0x07FF) + { + buffer[cursor++] = unchecked((byte)(0b_1100_0000 | (ordinal >> 6))); + buffer[cursor++] = unchecked((byte)(0b_1000_0000 | (ordinal & 0b_0011_1111))); + } + else if (ordinal <= 0xFFFF) + { + buffer[cursor++] = unchecked((byte)(0b_1110_0000 | (ordinal >> 12))); + buffer[cursor++] = unchecked((byte)(0b_1000_0000 | ((ordinal >> 6) & 0b_0011_1111))); + buffer[cursor++] = unchecked((byte)(0b_1000_0000 | (ordinal & 0b_0011_1111))); + } + else + { + Debug.Assert(ordinal <= 0xFFFF, ".NET string should not go beyond Unicode BMP."); + } + + return cursor; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int WriteUnicodeString(byte[] buffer, int cursor, string value) + { + for (int i = 0; i < value.Length; i++) + { + var ordinal = (ushort)value[i]; + switch (ordinal) + { + case ASCII_REVERSE_SOLIDUS: + buffer[cursor++] = ASCII_REVERSE_SOLIDUS; + buffer[cursor++] = ASCII_REVERSE_SOLIDUS; + break; + case ASCII_LINEFEED: + buffer[cursor++] = ASCII_REVERSE_SOLIDUS; + buffer[cursor++] = unchecked((byte)'n'); + break; + default: + cursor = WriteUnicodeNoEscape(buffer, cursor, ordinal); + break; + } + } + + return cursor; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int WriteLabelKey(byte[] buffer, int cursor, string value) + { + Debug.Assert(!string.IsNullOrEmpty(value), $"{nameof(value)} should not be null or empty."); + + var ordinal = (ushort)value[0]; + + if (ordinal >= (ushort)'0' && ordinal <= (ushort)'9') + { + buffer[cursor++] = unchecked((byte)'_'); + } + + for (int i = 0; i < value.Length; i++) + { + ordinal = (ushort)value[i]; + + if ((ordinal >= (ushort)'A' && ordinal <= (ushort)'Z') || + (ordinal >= (ushort)'a' && ordinal <= (ushort)'z') || + (ordinal >= (ushort)'0' && ordinal <= (ushort)'9')) + { + buffer[cursor++] = unchecked((byte)ordinal); + } + else + { + buffer[cursor++] = unchecked((byte)'_'); + } + } + + return cursor; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int WriteLabelValue(byte[] buffer, int cursor, string value) + { + Debug.Assert(value != null, $"{nameof(value)} should not be null."); + + for (int i = 0; i < value.Length; i++) + { + var ordinal = (ushort)value[i]; + switch (ordinal) + { + case ASCII_QUOTATION_MARK: + buffer[cursor++] = ASCII_REVERSE_SOLIDUS; + buffer[cursor++] = ASCII_QUOTATION_MARK; + break; + case ASCII_REVERSE_SOLIDUS: + buffer[cursor++] = ASCII_REVERSE_SOLIDUS; + buffer[cursor++] = ASCII_REVERSE_SOLIDUS; + break; + case ASCII_LINEFEED: + buffer[cursor++] = ASCII_REVERSE_SOLIDUS; + buffer[cursor++] = unchecked((byte)'n'); + break; + default: + cursor = WriteUnicodeNoEscape(buffer, cursor, ordinal); + break; + } + } + + return cursor; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int WriteLabel(byte[] buffer, int cursor, string labelKey, object labelValue) + { + cursor = WriteLabelKey(buffer, cursor, labelKey); + buffer[cursor++] = unchecked((byte)'='); + buffer[cursor++] = unchecked((byte)'"'); + + // In Prometheus, a label with an empty label value is considered equivalent to a label that does not exist. + cursor = WriteLabelValue(buffer, cursor, labelValue?.ToString() ?? string.Empty); + buffer[cursor++] = unchecked((byte)'"'); + + return cursor; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int WriteMetricName(byte[] buffer, int cursor, string metricName, string metricUnit = null) + { + Debug.Assert(!string.IsNullOrEmpty(metricName), $"{nameof(metricName)} should not be null or empty."); + + for (int i = 0; i < metricName.Length; i++) + { + var ordinal = (ushort)metricName[i]; + switch (ordinal) + { + case ASCII_FULL_STOP: + case ASCII_HYPHEN_MINUS: + buffer[cursor++] = unchecked((byte)'_'); + break; + default: + buffer[cursor++] = unchecked((byte)ordinal); + break; + } + } + + if (!string.IsNullOrEmpty(metricUnit)) + { + buffer[cursor++] = unchecked((byte)'_'); + + for (int i = 0; i < metricUnit.Length; i++) + { + var ordinal = (ushort)metricUnit[i]; + + if ((ordinal >= (ushort)'A' && ordinal <= (ushort)'Z') || + (ordinal >= (ushort)'a' && ordinal <= (ushort)'z') || + (ordinal >= (ushort)'0' && ordinal <= (ushort)'9')) + { + buffer[cursor++] = unchecked((byte)ordinal); + } + else + { + buffer[cursor++] = unchecked((byte)'_'); + } + } + } + + return cursor; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int WriteHelpText(byte[] buffer, int cursor, string metricName, string metricUnit = null, string metricDescription = null) + { + cursor = WriteAsciiStringNoEscape(buffer, cursor, "# HELP "); + cursor = WriteMetricName(buffer, cursor, metricName, metricUnit); + + if (!string.IsNullOrEmpty(metricDescription)) + { + buffer[cursor++] = unchecked((byte)' '); + cursor = WriteUnicodeString(buffer, cursor, metricDescription); + } + + buffer[cursor++] = ASCII_LINEFEED; + + return cursor; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int WriteTypeInfo(byte[] buffer, int cursor, string metricName, string metricUnit, string metricType) + { + Debug.Assert(!string.IsNullOrEmpty(metricType), $"{nameof(metricType)} should not be null or empty."); + + cursor = WriteAsciiStringNoEscape(buffer, cursor, "# TYPE "); + cursor = WriteMetricName(buffer, cursor, metricName, metricUnit); + buffer[cursor++] = unchecked((byte)' '); + cursor = WriteAsciiStringNoEscape(buffer, cursor, metricType); + + buffer[cursor++] = ASCII_LINEFEED; + + return cursor; + } + } +} diff --git a/src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusSerializerExt.cs b/src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusSerializerExt.cs new file mode 100644 index 00000000000..805a51a2488 --- /dev/null +++ b/src/OpenTelemetry.Exporter.Prometheus/Implementation/PrometheusSerializerExt.cs @@ -0,0 +1,200 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using OpenTelemetry.Metrics; + +namespace OpenTelemetry.Exporter.Prometheus +{ + /// + /// OpenTelemetry additions to the PrometheusSerializer. + /// + internal static partial class PrometheusSerializer + { + private static readonly string[] MetricTypes = new string[] + { + "untyped", "counter", "gauge", "summary", "histogram", "histogram", "histogram", "histogram", "untyped", + }; + + public static int WriteMetric(byte[] buffer, int cursor, Metric metric) + { + if (!string.IsNullOrWhiteSpace(metric.Description)) + { + cursor = WriteHelpText(buffer, cursor, metric.Name, metric.Unit, metric.Description); + } + + int metricType = (int)metric.MetricType >> 4; + cursor = WriteTypeInfo(buffer, cursor, metric.Name, metric.Unit, MetricTypes[metricType]); + + if (!metric.MetricType.IsHistogram()) + { + foreach (ref readonly var metricPoint in metric.GetMetricPoints()) + { + var tags = metricPoint.Tags; + var timestamp = metricPoint.EndTime.ToUnixTimeMilliseconds(); + + // Counter and Gauge + cursor = WriteMetricName(buffer, cursor, metric.Name, metric.Unit); + + if (tags.Count > 0) + { + buffer[cursor++] = unchecked((byte)'{'); + + foreach (var tag in tags) + { + cursor = WriteLabel(buffer, cursor, tag.Key, tag.Value); + buffer[cursor++] = unchecked((byte)','); + } + + buffer[cursor - 1] = unchecked((byte)'}'); // Note: We write the '}' over the last written comma, which is extra. + } + + buffer[cursor++] = unchecked((byte)' '); + + // TODO: MetricType is same for all MetricPoints + // within a given Metric, so this check can avoided + // for each MetricPoint + if (((int)metric.MetricType & 0b_0000_1111) == 0x0a /* I8 */) + { + if (metric.MetricType.IsSum()) + { + cursor = WriteLong(buffer, cursor, metricPoint.GetSumLong()); + } + else + { + cursor = WriteLong(buffer, cursor, metricPoint.GetGaugeLastValueLong()); + } + } + else + { + if (metric.MetricType.IsSum()) + { + cursor = WriteDouble(buffer, cursor, metricPoint.GetSumDouble()); + } + else + { + cursor = WriteDouble(buffer, cursor, metricPoint.GetGaugeLastValueDouble()); + } + } + + buffer[cursor++] = unchecked((byte)' '); + + cursor = WriteLong(buffer, cursor, timestamp); + + buffer[cursor++] = ASCII_LINEFEED; + } + } + else + { + foreach (ref readonly var metricPoint in metric.GetMetricPoints()) + { + var tags = metricPoint.Tags; + var timestamp = metricPoint.EndTime.ToUnixTimeMilliseconds(); + + long totalCount = 0; + foreach (var histogramMeasurement in metricPoint.GetHistogramBuckets()) + { + totalCount += histogramMeasurement.BucketCount; + + cursor = WriteMetricName(buffer, cursor, metric.Name, metric.Unit); + cursor = WriteAsciiStringNoEscape(buffer, cursor, "_bucket{"); + + foreach (var tag in tags) + { + cursor = WriteLabel(buffer, cursor, tag.Key, tag.Value); + buffer[cursor++] = unchecked((byte)','); + } + + cursor = WriteAsciiStringNoEscape(buffer, cursor, "le=\""); + + if (histogramMeasurement.ExplicitBound != double.PositiveInfinity) + { + cursor = WriteDouble(buffer, cursor, histogramMeasurement.ExplicitBound); + } + else + { + cursor = WriteAsciiStringNoEscape(buffer, cursor, "+Inf"); + } + + cursor = WriteAsciiStringNoEscape(buffer, cursor, "\"} "); + + cursor = WriteLong(buffer, cursor, totalCount); + buffer[cursor++] = unchecked((byte)' '); + + cursor = WriteLong(buffer, cursor, timestamp); + + buffer[cursor++] = ASCII_LINEFEED; + } + + // Histogram sum + cursor = WriteMetricName(buffer, cursor, metric.Name, metric.Unit); + cursor = WriteAsciiStringNoEscape(buffer, cursor, "_sum"); + + if (tags.Count > 0) + { + buffer[cursor++] = unchecked((byte)'{'); + + foreach (var tag in tags) + { + cursor = WriteLabel(buffer, cursor, tag.Key, tag.Value); + buffer[cursor++] = unchecked((byte)','); + } + + buffer[cursor - 1] = unchecked((byte)'}'); // Note: We write the '}' over the last written comma, which is extra. + } + + buffer[cursor++] = unchecked((byte)' '); + + cursor = WriteDouble(buffer, cursor, metricPoint.GetHistogramSum()); + buffer[cursor++] = unchecked((byte)' '); + + cursor = WriteLong(buffer, cursor, timestamp); + + buffer[cursor++] = ASCII_LINEFEED; + + // Histogram count + cursor = WriteMetricName(buffer, cursor, metric.Name, metric.Unit); + cursor = WriteAsciiStringNoEscape(buffer, cursor, "_count"); + + if (tags.Count > 0) + { + buffer[cursor++] = unchecked((byte)'{'); + + foreach (var tag in tags) + { + cursor = WriteLabel(buffer, cursor, tag.Key, tag.Value); + buffer[cursor++] = unchecked((byte)','); + } + + buffer[cursor - 1] = unchecked((byte)'}'); // Note: We write the '}' over the last written comma, which is extra. + } + + buffer[cursor++] = unchecked((byte)' '); + + cursor = WriteLong(buffer, cursor, metricPoint.GetHistogramCount()); + buffer[cursor++] = unchecked((byte)' '); + + cursor = WriteLong(buffer, cursor, timestamp); + + buffer[cursor++] = ASCII_LINEFEED; + } + } + + buffer[cursor++] = ASCII_LINEFEED; + + return cursor; + } + } +} diff --git a/src/OpenTelemetry.Exporter.Prometheus/OpenTelemetry.Exporter.Prometheus.csproj b/src/OpenTelemetry.Exporter.Prometheus/OpenTelemetry.Exporter.Prometheus.csproj index b7a7eba8475..5730aec8ab8 100644 --- a/src/OpenTelemetry.Exporter.Prometheus/OpenTelemetry.Exporter.Prometheus.csproj +++ b/src/OpenTelemetry.Exporter.Prometheus/OpenTelemetry.Exporter.Prometheus.csproj @@ -1,7 +1,7 @@  - netstandard2.0;net461 + netcoreapp3.1;net461 Prometheus exporter for OpenTelemetry .NET $(PackageTags);prometheus;metrics core- @@ -24,10 +24,11 @@ + - - + + diff --git a/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporter.cs b/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporter.cs index 7f85f1ba7e8..9470b31e67a 100644 --- a/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporter.cs +++ b/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporter.cs @@ -15,6 +15,7 @@ // using System; +using OpenTelemetry.Exporter.Prometheus; using OpenTelemetry.Metrics; namespace OpenTelemetry.Exporter @@ -22,10 +23,17 @@ namespace OpenTelemetry.Exporter /// /// Exporter of OpenTelemetry metrics to Prometheus. /// - public class PrometheusExporter : BaseExporter + [AggregationTemporality(AggregationTemporality.Cumulative)] + [ExportModes(ExportModes.Pull)] + public class PrometheusExporter : BaseExporter, IPullMetricExporter { + internal const string HttpListenerStartFailureExceptionMessage = "PrometheusExporter http listener could not be started."; internal readonly PrometheusExporterOptions Options; - internal Batch Batch; + internal Batch Metrics; // TODO: this is no longer needed, we can remove it later + private readonly PrometheusExporterHttpServer metricsHttpServer; + private Func funcCollect; + private Func, ExportResult> funcExport; + private bool disposed; /// /// Initializes a new instance of the class. @@ -34,14 +42,55 @@ public class PrometheusExporter : BaseExporter public PrometheusExporter(PrometheusExporterOptions options) { this.Options = options; + + if (options.StartHttpListener) + { + try + { + this.metricsHttpServer = new PrometheusExporterHttpServer(this); + this.metricsHttpServer.Start(); + } + catch (Exception ex) + { + throw new InvalidOperationException(HttpListenerStartFailureExceptionMessage, ex); + } + } + + this.CollectionManager = new PrometheusCollectionManager(this); } - internal Action MakePullRequest { get; set; } + public Func Collect + { + get => this.funcCollect; + set => this.funcCollect = value; + } - public override ExportResult Export(in Batch batch) + internal Func, ExportResult> OnExport { - this.Batch = batch; - return ExportResult.Success; + get => this.funcExport; + set => this.funcExport = value; + } + + internal PrometheusCollectionManager CollectionManager { get; } + + public override ExportResult Export(in Batch metrics) + { + return this.OnExport(metrics); + } + + protected override void Dispose(bool disposing) + { + if (!this.disposed) + { + if (disposing) + { + this.metricsHttpServer?.Dispose(); + } + + this.disposed = true; + } + + base.Dispose(disposing); } } } diff --git a/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporterApplicationBuilderExtensions.cs b/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporterApplicationBuilderExtensions.cs new file mode 100644 index 00000000000..f88fc1b8965 --- /dev/null +++ b/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporterApplicationBuilderExtensions.cs @@ -0,0 +1,59 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#if NETCOREAPP3_1_OR_GREATER + +using System; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using OpenTelemetry.Exporter; +using OpenTelemetry.Exporter.Prometheus; +using OpenTelemetry.Metrics; + +namespace Microsoft.AspNetCore.Builder +{ + /// + /// Provides extension methods for to add Prometheus Scraper Endpoint. + /// + public static class PrometheusExporterApplicationBuilderExtensions + { + /// + /// Adds OpenTelemetry Prometheus scraping endpoint middleware to an + /// instance. + /// + /// The to add + /// middleware to. + /// Optional + /// containing a otherwise the primary + /// SDK provider will be resolved using application services. + /// A reference to the instance after the operation has completed. + public static IApplicationBuilder UseOpenTelemetryPrometheusScrapingEndpoint(this IApplicationBuilder app, MeterProvider meterProvider = null) + { + var options = app.ApplicationServices.GetOptions(); + + string path = options.ScrapeEndpointPath ?? PrometheusExporterOptions.DefaultScrapeEndpointPath; + if (!path.StartsWith("/")) + { + path = $"/{path}"; + } + + return app.Map( + new PathString(path), + builder => builder.UseMiddleware(meterProvider ?? app.ApplicationServices.GetRequiredService())); + } + } +} +#endif diff --git a/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporterExtensions.cs b/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporterExtensions.cs deleted file mode 100644 index c2ddc69fd06..00000000000 --- a/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporterExtensions.cs +++ /dev/null @@ -1,143 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -using System.Collections.Generic; -using System.IO; -using System.Text; -using OpenTelemetry.Exporter.Prometheus.Implementation; -using OpenTelemetry.Metrics; - -namespace OpenTelemetry.Exporter -{ - /// - /// Helper to write metrics collection from exporter in Prometheus format. - /// - public static class PrometheusExporterExtensions - { - private const string PrometheusCounterType = "counter"; - private const string PrometheusSummaryType = "summary"; - private const string PrometheusSummarySumPostFix = "_sum"; - private const string PrometheusSummaryCountPostFix = "_count"; - private const string PrometheusSummaryQuantileLabelName = "quantile"; - private const string PrometheusSummaryQuantileLabelValueForMin = "0"; - private const string PrometheusSummaryQuantileLabelValueForMax = "1"; - - /// - /// Serialize to Prometheus Format. - /// - /// Prometheus Exporter. - /// StreamWriter to write to. - public static void WriteMetricsCollection(this PrometheusExporter exporter, StreamWriter writer) - { - foreach (var metricItem in exporter.Batch) - { - foreach (var metric in metricItem.Metrics) - { - var builder = new PrometheusMetricBuilder() - .WithName(metric.Name) - .WithDescription(metric.Name); - - // TODO: Use switch case for higher perf. - if (metric.MetricType == MetricType.LongSum) - { - WriteSum(writer, builder, metric.Attributes, (metric as ISumMetricLong).LongSum); - } - else if (metric.MetricType == MetricType.DoubleSum) - { - WriteSum(writer, builder, metric.Attributes, (metric as ISumMetricDouble).DoubleSum); - } - } - } - } - - /// - /// Get Metrics Collection as a string. - /// - /// Prometheus Exporter. - /// Metrics serialized to string in Prometheus format. - public static string GetMetricsCollection(this PrometheusExporter exporter) - { - using var stream = new MemoryStream(); - using var writer = new StreamWriter(stream); - WriteMetricsCollection(exporter, writer); - writer.Flush(); - - return Encoding.UTF8.GetString(stream.ToArray(), 0, (int)stream.Length); - } - - private static void WriteSum(StreamWriter writer, PrometheusMetricBuilder builder, IEnumerable> labels, double doubleValue) - { - builder = builder.WithType(PrometheusCounterType); - - var metricValueBuilder = builder.AddValue(); - metricValueBuilder = metricValueBuilder.WithValue(doubleValue); - - foreach (var label in labels) - { - metricValueBuilder.WithLabel(label.Key, label.Value.ToString()); - } - - builder.Write(writer); - } - - private static void WriteSummary( - StreamWriter writer, - PrometheusMetricBuilder builder, - IEnumerable> labels, - string metricName, - double sum, - long count, - double min, - double max) - { - builder = builder.WithType(PrometheusSummaryType); - - foreach (var label in labels) - { - /* For Summary we emit one row for Sum, Count, Min, Max. - Min,Max exports as quantile 0 and 1. - In future, when OpenTelemetry implements more aggregation - algorithms, this section will need to be revisited. - Sample output: - MyMeasure_sum{dim1="value1"} 750 1587013352982 - MyMeasure_count{dim1="value1"} 5 1587013352982 - MyMeasure{dim1="value2",quantile="0"} 150 1587013352982 - MyMeasure{dim1="value2",quantile="1"} 150 1587013352982 - */ - builder.AddValue() - .WithName(metricName + PrometheusSummarySumPostFix) - .WithLabel(label.Key, label.Value) - .WithValue(sum); - builder.AddValue() - .WithName(metricName + PrometheusSummaryCountPostFix) - .WithLabel(label.Key, label.Value) - .WithValue(count); - builder.AddValue() - .WithName(metricName) - .WithLabel(label.Key, label.Value) - .WithLabel(PrometheusSummaryQuantileLabelName, PrometheusSummaryQuantileLabelValueForMin) - .WithValue(min); - builder.AddValue() - .WithName(metricName) - .WithLabel(label.Key, label.Value) - .WithLabel(PrometheusSummaryQuantileLabelName, PrometheusSummaryQuantileLabelValueForMax) - .WithValue(max); - } - - builder.Write(writer); - } - } -} diff --git a/src/OpenTelemetry.Exporter.Prometheus/MeterProviderBuilderExtensions.cs b/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporterMeterProviderBuilderExtensions.cs similarity index 55% rename from src/OpenTelemetry.Exporter.Prometheus/MeterProviderBuilderExtensions.cs rename to src/OpenTelemetry.Exporter.Prometheus/PrometheusExporterMeterProviderBuilderExtensions.cs index b7e307ada2b..a7d5da46285 100644 --- a/src/OpenTelemetry.Exporter.Prometheus/MeterProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporterMeterProviderBuilderExtensions.cs @@ -1,4 +1,4 @@ -// +// // Copyright The OpenTelemetry Authors // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,34 +16,41 @@ using System; using OpenTelemetry.Exporter; +using OpenTelemetry.Internal; namespace OpenTelemetry.Metrics { - public static class MeterProviderBuilderExtensions + public static class PrometheusExporterMeterProviderBuilderExtensions { /// - /// Adds Console exporter to the TracerProvider. + /// Adds to the . /// /// builder to use. /// Exporter configuration options. /// The instance of to chain the calls. - [System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "The objects should not be disposed.")] public static MeterProviderBuilder AddPrometheusExporter(this MeterProviderBuilder builder, Action configure = null) { - if (builder == null) + Guard.ThrowIfNull(builder, nameof(builder)); + + if (builder is IDeferredMeterProviderBuilder deferredMeterProviderBuilder) { - throw new ArgumentNullException(nameof(builder)); + return deferredMeterProviderBuilder.Configure((sp, builder) => + { + AddPrometheusExporter(builder, sp.GetOptions(), configure); + }); } - var options = new PrometheusExporterOptions(); + return AddPrometheusExporter(builder, new PrometheusExporterOptions(), configure); + } + + private static MeterProviderBuilder AddPrometheusExporter(MeterProviderBuilder builder, PrometheusExporterOptions options, Action configure = null) + { configure?.Invoke(options); + var exporter = new PrometheusExporter(options); - var pullMetricProcessor = new PullMetricProcessor(exporter, false); - exporter.MakePullRequest = pullMetricProcessor.PullRequest; + var reader = new BaseExportingMetricReader(exporter); - var metricsHttpServer = new PrometheusExporterMetricsHttpServer(exporter); - metricsHttpServer.Start(); - return builder.AddMetricProcessor(pullMetricProcessor); + return builder.AddReader(reader); } } } diff --git a/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporterMiddleware.cs b/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporterMiddleware.cs deleted file mode 100644 index 10cb608ee31..00000000000 --- a/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporterMiddleware.cs +++ /dev/null @@ -1,60 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -using System; -using System.Threading.Tasks; - -#if NETSTANDARD2_0 -using Microsoft.AspNetCore.Http; - -namespace OpenTelemetry.Exporter -{ - /// - /// A middleware used to expose Prometheus metrics. - /// - public class PrometheusExporterMiddleware - { - private readonly PrometheusExporter exporter; - - /// - /// Initializes a new instance of the class. - /// - /// The next middleware in the pipeline. - /// The instance. - public PrometheusExporterMiddleware(RequestDelegate next, PrometheusExporter exporter) - { - this.exporter = exporter ?? throw new ArgumentNullException(nameof(exporter)); - } - - /// - /// Invoke. - /// - /// context. - /// Task. - public Task InvokeAsync(HttpContext httpContext) - { - if (httpContext is null) - { - throw new ArgumentNullException(nameof(httpContext)); - } - - var result = this.exporter.GetMetricsCollection(); - - return httpContext.Response.WriteAsync(result); - } - } -} -#endif diff --git a/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporterOptions.cs b/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporterOptions.cs index ce4aae423cf..758c22f5106 100644 --- a/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.Prometheus/PrometheusExporterOptions.cs @@ -14,16 +14,83 @@ // limitations under the License. // +using System; +using System.Collections.Generic; +using OpenTelemetry.Internal; + namespace OpenTelemetry.Exporter { /// - /// Options to run prometheus exporter. + /// options. /// public class PrometheusExporterOptions { + internal const string DefaultScrapeEndpointPath = "/metrics"; + internal Func GetUtcNowDateTimeOffset = () => DateTimeOffset.UtcNow; + + private int scrapeResponseCacheDurationMilliseconds = 10 * 1000; + private IReadOnlyCollection httpListenerPrefixes = new string[] { "http://localhost:9464/" }; + +#if NETCOREAPP3_1_OR_GREATER + /// + /// Gets or sets a value indicating whether or not an http listener + /// should be started. Default value: False. + /// + public bool StartHttpListener { get; set; } +#else /// - /// Gets or sets the port to listen to. Typically it ends with /metrics like http://localhost:9184/metrics/. + /// Gets or sets a value indicating whether or not an http listener + /// should be started. Default value: True. /// - public string Url { get; set; } = "http://localhost:9184/metrics/"; + public bool StartHttpListener { get; set; } = true; +#endif + + /// + /// Gets or sets the prefixes to use for the http listener. Default + /// value: http://localhost:9464/. + /// + public IReadOnlyCollection HttpListenerPrefixes + { + get => this.httpListenerPrefixes; + set + { + Guard.ThrowIfNull(value, nameof(this.httpListenerPrefixes)); + + foreach (string inputUri in value) + { + if (!(Uri.TryCreate(inputUri, UriKind.Absolute, out var uri) && + (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps))) + { + throw new ArgumentException( + "Prometheus server path should be a valid URI with http/https scheme.", + nameof(this.httpListenerPrefixes)); + } + } + + this.httpListenerPrefixes = value; + } + } + + /// + /// Gets or sets the path to use for the scraping endpoint. Default value: /metrics. + /// + public string ScrapeEndpointPath { get; set; } = DefaultScrapeEndpointPath; + + /// + /// Gets or sets the cache duration in milliseconds for scrape responses. Default value: 10,000 (10 seconds). + /// + /// + /// Note: Specify 0 to disable response caching. + /// + public int ScrapeResponseCacheDurationMilliseconds + { + get => this.scrapeResponseCacheDurationMilliseconds; + set + { + Guard.ThrowIfOutOfRange(value, nameof(value), min: 0); + + this.scrapeResponseCacheDurationMilliseconds = value; + } + } } } diff --git a/src/OpenTelemetry.Exporter.Prometheus/PrometheusRouteBuilderExtensions.cs b/src/OpenTelemetry.Exporter.Prometheus/PrometheusRouteBuilderExtensions.cs deleted file mode 100644 index 0b38d473419..00000000000 --- a/src/OpenTelemetry.Exporter.Prometheus/PrometheusRouteBuilderExtensions.cs +++ /dev/null @@ -1,46 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -#if NETSTANDARD2_0 - -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; - -namespace OpenTelemetry.Exporter.Prometheus -{ - /// - /// Provides extension methods for to add Prometheus Scraper Endpoint. - /// - public static class PrometheusRouteBuilderExtensions - { - private const string DefaultPath = "/metrics"; - - /// - /// Use prometheus extension. - /// - /// The to add middleware to. - /// A reference to the instance after the operation has completed. - public static IApplicationBuilder UsePrometheus(this IApplicationBuilder app) - { - var options = app.ApplicationServices.GetService(typeof(PrometheusExporterOptions)) as PrometheusExporterOptions; - var path = new PathString(options?.Url ?? DefaultPath); - return app.Map( - new PathString(path), - builder => builder.UseMiddleware()); - } - } -} -#endif diff --git a/src/OpenTelemetry.Exporter.Prometheus/README.md b/src/OpenTelemetry.Exporter.Prometheus/README.md index ed79f8d2fa8..b92b5093525 100644 --- a/src/OpenTelemetry.Exporter.Prometheus/README.md +++ b/src/OpenTelemetry.Exporter.Prometheus/README.md @@ -7,17 +7,95 @@ * [Get Prometheus](https://prometheus.io/docs/introduction/first_steps/) -## Installation +## Steps to enable OpenTelemetry.Exporter.Prometheus + +### Step 1: Install Package ```shell dotnet add package OpenTelemetry.Exporter.Prometheus ``` +### Step 2: Configure OpenTelemetry MeterProvider + +* When using OpenTelemetry.Extensions.Hosting package on .NET Core 3.1+: + + ```csharp + services.AddOpenTelemetryMetrics(builder => + { + builder.AddPrometheusExporter(); + }); + ``` + +* Or configure directly: + + Call the `AddPrometheusExporter` `MeterProviderBuilder` extension to + register the Prometheus exporter. + + ```csharp + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddPrometheusExporter() + .Build(); + ``` + +### Step 3: Configure Prometheus Scraping Endpoint + +* On .NET Core 3.1+ register Prometheus scraping middleware using the + `UseOpenTelemetryPrometheusScrapingEndpoint` extension: + + ```csharp + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseRouting(); + app.UseOpenTelemetryPrometheusScrapingEndpoint(); + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + } + ``` + +* On .NET Framework an http listener is automatically started which will respond + to scraping requests. See the [Options Properties](#options-properties) + section for details on the settings available. This may also be turned on in + .NET Core (it is OFF by default) when the ASP.NET Core pipeline is not + available for middleware registration. + ## Configuration -You can configure the `PrometheusExporter` by following the directions below: +You can configure the `PrometheusExporter` through `PrometheusExporterOptions`. + +## Options Properties + +The `PrometheusExporter` can be configured using the `PrometheusExporterOptions` +properties: + +* `StartHttpListener`: Set to `true` to start an http listener which will + respond to Prometheus scrape requests using the `HttpListenerPrefixes` and + `ScrapeEndpointPath` options. + + Defaults: + + * On .NET Framework this is `true` by default. + + * On .NET Core 3.1+ this is `false` by default. Users running ASP.NET Core + should use the `UseOpenTelemetryPrometheusScrapingEndpoint` extension to + register the scraping middleware instead of using the listener. + +* `HttpListenerPrefixes`: Defines the prefixes which will be used by the + listener when `StartHttpListener` is `true`. The default value is + `["http://localhost:9464/"]`. You may specify multiple endpoints. + + For details see: + [HttpListenerPrefixCollection.Add(String)](https://docs.microsoft.com/dotnet/api/system.net.httplistenerprefixcollection.add) + +* `ScrapeEndpointPath`: Defines the path for the Prometheus scrape endpoint for + either the http listener or the middleware registered by + `UseOpenTelemetryPrometheusScrapingEndpoint`. Default value: `"/metrics"`. -* `Url`: The url to listen to. Typically it ends with `/metrics` like `http://localhost:9184/metrics/`. +* `ScrapeResponseCacheDurationMilliseconds`: Configures scrape endpoint response + caching. Multiple scrape requests within the cache duration time period will + receive the same previously generated response. The default value is `10000` + (10 seconds). Set to `0` to disable response caching. See [`TestPrometheusExporter.cs`](../../examples/Console/TestPrometheusExporter.cs) diff --git a/src/OpenTelemetry.Exporter.ZPages/CHANGELOG.md b/src/OpenTelemetry.Exporter.ZPages/CHANGELOG.md index 7bdde8b790a..f74690c1196 100644 --- a/src/OpenTelemetry.Exporter.ZPages/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.ZPages/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## 1.0.0-rc8 + +Released 2021-Oct-08 + * Removes .NET Framework 4.5.2, 4.6 support. The minimum .NET Framework version supported is .NET 4.6.1. ([2138](https://github.com/open-telemetry/opentelemetry-dotnet/issues/2138)) diff --git a/src/OpenTelemetry.Exporter.ZPages/Implementation/ZPagesActivityTracker.cs b/src/OpenTelemetry.Exporter.ZPages/Implementation/ZPagesActivityTracker.cs index b7011c74fbb..b09279f8834 100644 --- a/src/OpenTelemetry.Exporter.ZPages/Implementation/ZPagesActivityTracker.cs +++ b/src/OpenTelemetry.Exporter.ZPages/Implementation/ZPagesActivityTracker.cs @@ -26,7 +26,7 @@ namespace OpenTelemetry.Exporter.ZPages.Implementation /// internal static class ZPagesActivityTracker { - private static long startTime; + private static readonly long StartTime; /// /// Initializes static members of the class. @@ -41,7 +41,7 @@ static ZPagesActivityTracker() TotalEndedCount = new Dictionary(); TotalErrorCount = new Dictionary(); TotalLatency = new Dictionary(); - startTime = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + StartTime = DateTimeOffset.Now.ToUnixTimeMilliseconds(); } /// @@ -103,7 +103,7 @@ public static void PurgeCurrentMinuteData(object source, ElapsedEventArgs e) CurrentMinuteList.Clear(); // Remove the stale activity information which is at the end of the list - if (DateTimeOffset.Now.ToUnixTimeMilliseconds() - startTime >= RetentionTime) + if (DateTimeOffset.Now.ToUnixTimeMilliseconds() - StartTime >= RetentionTime) { ZQueue.RemoveLast(); } diff --git a/src/OpenTelemetry.Exporter.ZPages/OpenTelemetry.Exporter.ZPages.csproj b/src/OpenTelemetry.Exporter.ZPages/OpenTelemetry.Exporter.ZPages.csproj index 14dadaf44c0..ea031a58f5c 100644 --- a/src/OpenTelemetry.Exporter.ZPages/OpenTelemetry.Exporter.ZPages.csproj +++ b/src/OpenTelemetry.Exporter.ZPages/OpenTelemetry.Exporter.ZPages.csproj @@ -10,6 +10,7 @@ + diff --git a/src/OpenTelemetry.Exporter.ZPages/ZPagesExporter.cs b/src/OpenTelemetry.Exporter.ZPages/ZPagesExporter.cs index 58db8eb0f10..c02e5905731 100644 --- a/src/OpenTelemetry.Exporter.ZPages/ZPagesExporter.cs +++ b/src/OpenTelemetry.Exporter.ZPages/ZPagesExporter.cs @@ -14,10 +14,10 @@ // limitations under the License. // -using System; using System.Diagnostics; using System.Timers; using OpenTelemetry.Exporter.ZPages.Implementation; +using OpenTelemetry.Internal; using Timer = System.Timers.Timer; namespace OpenTelemetry.Exporter.ZPages @@ -37,7 +37,9 @@ public class ZPagesExporter : BaseExporter /// Options for the exporter. public ZPagesExporter(ZPagesExporterOptions options) { - ZPagesActivityTracker.RetentionTime = options?.RetentionTime ?? throw new ArgumentNullException(nameof(options)); + Guard.ThrowIfNull(options?.RetentionTime, $"{nameof(options)}?.{nameof(options.RetentionTime)}"); + + ZPagesActivityTracker.RetentionTime = options.RetentionTime; this.Options = options; diff --git a/src/OpenTelemetry.Exporter.ZPages/ZPagesExporterHelperExtensions.cs b/src/OpenTelemetry.Exporter.ZPages/ZPagesExporterHelperExtensions.cs index 546490010b8..7dd316a5c4c 100644 --- a/src/OpenTelemetry.Exporter.ZPages/ZPagesExporterHelperExtensions.cs +++ b/src/OpenTelemetry.Exporter.ZPages/ZPagesExporterHelperExtensions.cs @@ -17,6 +17,7 @@ using System; using OpenTelemetry.Exporter.ZPages; +using OpenTelemetry.Internal; namespace OpenTelemetry.Trace { @@ -36,10 +37,7 @@ public static TracerProviderBuilder AddZPagesExporter( this TracerProviderBuilder builder, Action configure = null) { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } + Guard.ThrowIfNull(builder, nameof(builder)); var exporterOptions = new ZPagesExporterOptions(); configure?.Invoke(exporterOptions); diff --git a/src/OpenTelemetry.Exporter.ZPages/ZPagesExporterStatsHttpServer.cs b/src/OpenTelemetry.Exporter.ZPages/ZPagesExporterStatsHttpServer.cs index 5d9d8d988cf..906d81674c2 100644 --- a/src/OpenTelemetry.Exporter.ZPages/ZPagesExporterStatsHttpServer.cs +++ b/src/OpenTelemetry.Exporter.ZPages/ZPagesExporterStatsHttpServer.cs @@ -21,6 +21,7 @@ using System.Threading; using System.Threading.Tasks; using OpenTelemetry.Exporter.ZPages.Implementation; +using OpenTelemetry.Internal; namespace OpenTelemetry.Exporter.ZPages { @@ -41,7 +42,9 @@ public class ZPagesExporterStatsHttpServer : IDisposable /// The instance. public ZPagesExporterStatsHttpServer(ZPagesExporter exporter) { - this.httpListener.Prefixes.Add(exporter?.Options?.Url ?? throw new ArgumentNullException(nameof(exporter))); + Guard.ThrowIfNull(exporter?.Options?.Url, $"{nameof(exporter)}?.{nameof(exporter.Options)}?.{nameof(exporter.Options.Url)}"); + + this.httpListener.Prefixes.Add(exporter.Options.Url); } /// diff --git a/src/OpenTelemetry.Exporter.Zipkin/.publicApi/net461/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.Zipkin/.publicApi/net461/PublicAPI.Unshipped.txt index e69de29bb2d..c6d105a2186 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/.publicApi/net461/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Exporter.Zipkin/.publicApi/net461/PublicAPI.Unshipped.txt @@ -0,0 +1,2 @@ +OpenTelemetry.Exporter.ZipkinExporterOptions.HttpClientFactory.get -> System.Func +OpenTelemetry.Exporter.ZipkinExporterOptions.HttpClientFactory.set -> void \ No newline at end of file diff --git a/src/OpenTelemetry.Exporter.Zipkin/.publicApi/net452/PublicAPI.Shipped.txt b/src/OpenTelemetry.Exporter.Zipkin/.publicApi/net5.0/PublicAPI.Shipped.txt similarity index 90% rename from src/OpenTelemetry.Exporter.Zipkin/.publicApi/net452/PublicAPI.Shipped.txt rename to src/OpenTelemetry.Exporter.Zipkin/.publicApi/net5.0/PublicAPI.Shipped.txt index 4ca16ec8bad..30a9861d7bb 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/.publicApi/net452/PublicAPI.Shipped.txt +++ b/src/OpenTelemetry.Exporter.Zipkin/.publicApi/net5.0/PublicAPI.Shipped.txt @@ -7,6 +7,8 @@ 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.MaxPayloadSizeInBytes.get -> int? +OpenTelemetry.Exporter.ZipkinExporterOptions.MaxPayloadSizeInBytes.set -> void OpenTelemetry.Exporter.ZipkinExporterOptions.UseShortTraceIds.get -> bool OpenTelemetry.Exporter.ZipkinExporterOptions.UseShortTraceIds.set -> void OpenTelemetry.Exporter.ZipkinExporterOptions.ZipkinExporterOptions() -> void diff --git a/src/OpenTelemetry.Exporter.Zipkin/.publicApi/net5.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.Zipkin/.publicApi/net5.0/PublicAPI.Unshipped.txt new file mode 100644 index 00000000000..c6d105a2186 --- /dev/null +++ b/src/OpenTelemetry.Exporter.Zipkin/.publicApi/net5.0/PublicAPI.Unshipped.txt @@ -0,0 +1,2 @@ +OpenTelemetry.Exporter.ZipkinExporterOptions.HttpClientFactory.get -> System.Func +OpenTelemetry.Exporter.ZipkinExporterOptions.HttpClientFactory.set -> void \ No newline at end of file diff --git a/src/OpenTelemetry.Exporter.Zipkin/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Exporter.Zipkin/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt index e69de29bb2d..c6d105a2186 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Exporter.Zipkin/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt @@ -0,0 +1,2 @@ +OpenTelemetry.Exporter.ZipkinExporterOptions.HttpClientFactory.get -> System.Func +OpenTelemetry.Exporter.ZipkinExporterOptions.HttpClientFactory.set -> void \ No newline at end of file diff --git a/src/OpenTelemetry.Exporter.Zipkin/CHANGELOG.md b/src/OpenTelemetry.Exporter.Zipkin/CHANGELOG.md index 1e1dac0d3ff..6a1b1e315ca 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/CHANGELOG.md +++ b/src/OpenTelemetry.Exporter.Zipkin/CHANGELOG.md @@ -2,6 +2,48 @@ ## Unreleased +## 1.2.0-rc1 + +Released 2021-Nov-29 + +## 1.2.0-beta2 + +Released 2021-Nov-19 + +* Changed `ZipkinExporterOptions` constructor to throw + `FormatException` if it fails to parse any of the supported environment + variables. + +* Added `HttpClientFactory` option + ([#2654](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2654)) + +## 1.2.0-beta1 + +Released 2021-Oct-08 + +* Added .NET 5.0 target and threading optimizations + ([#2405](https://github.com/open-telemetry/opentelemetry-dotnet/issues/2405)) + +## 1.2.0-alpha4 + +Released 2021-Sep-23 + +## 1.2.0-alpha3 + +Released 2021-Sep-13 + +* `ZipkinExporterOptions.BatchExportProcessorOptions` is initialized with + `BatchExportActivityProcessorOptions` which supports field value overriding + using `OTEL_BSP_SCHEDULE_DELAY`, `OTEL_BSP_EXPORT_TIMEOUT`, + `OTEL_BSP_MAX_QUEUE_SIZE`, `OTEL_BSP_MAX_EXPORT_BATCH_SIZE` + envionmental variables as defined in the + [specification](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.5.0/specification/sdk-environment-variables.md#batch-span-processor). + ([#2219](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2219)) + +## 1.2.0-alpha2 + +Released 2021-Aug-24 + * Enabling endpoint configuration in ZipkinExporterOptions via `OTEL_EXPORTER_ZIPKIN_ENDPOINT` environment variable. ([#1453](https://github.com/open-telemetry/opentelemetry-dotnet/issues/1453)) diff --git a/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinEndpoint.cs b/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinEndpoint.cs index 19543618cd2..db850fc3f5c 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinEndpoint.cs +++ b/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinEndpoint.cs @@ -13,6 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // + using System.Collections.Generic; using System.Text.Json; @@ -54,11 +55,11 @@ public static ZipkinEndpoint Create(string serviceName) return new ZipkinEndpoint(serviceName); } - public static ZipkinEndpoint Create((string name, int port) serviceNameAndPort) + public static ZipkinEndpoint Create((string Name, int Port) serviceNameAndPort) { - var serviceName = serviceNameAndPort.port == default - ? serviceNameAndPort.name - : $"{serviceNameAndPort.name}:{serviceNameAndPort.port}"; + var serviceName = serviceNameAndPort.Port == default + ? serviceNameAndPort.Name + : $"{serviceNameAndPort.Name}:{serviceNameAndPort.Port}"; return new ZipkinEndpoint(serviceName); } diff --git a/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinExporterEventSource.cs b/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinExporterEventSource.cs index de9d6856fe1..f2d1bd94f40 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinExporterEventSource.cs +++ b/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinExporterEventSource.cs @@ -37,25 +37,10 @@ public void FailedExport(Exception ex) } } - [NonEvent] - public void FailedEndpointInitialization(Exception ex) - { - if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) - { - this.FailedEndpointInitialization(ex.ToInvariantString()); - } - } - [Event(1, Message = "Failed to export activities: '{0}'", Level = EventLevel.Error)] public void FailedExport(string exception) { this.WriteEvent(1, exception); } - - [Event(2, Message = "Error initializing Zipkin endpoint, falling back to default value: '{0}'", Level = EventLevel.Error)] - public void FailedEndpointInitialization(string exception) - { - this.WriteEvent(2, exception); - } } } diff --git a/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinSpan.cs b/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinSpan.cs index 160d646f4c5..a555a90587e 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinSpan.cs +++ b/src/OpenTelemetry.Exporter.Zipkin/Implementation/ZipkinSpan.cs @@ -14,7 +14,6 @@ // limitations under the License. // -using System; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -42,15 +41,8 @@ public ZipkinSpan( bool? debug, bool? shared) { - if (string.IsNullOrWhiteSpace(traceId)) - { - throw new ArgumentNullException(nameof(traceId)); - } - - if (string.IsNullOrWhiteSpace(id)) - { - throw new ArgumentNullException(nameof(id)); - } + Guard.ThrowIfNullOrWhitespace(traceId, nameof(traceId)); + Guard.ThrowIfNullOrWhitespace(id, nameof(id)); this.TraceId = traceId; this.ParentId = parentId; diff --git a/src/OpenTelemetry.Exporter.Zipkin/OpenTelemetry.Exporter.Zipkin.csproj b/src/OpenTelemetry.Exporter.Zipkin/OpenTelemetry.Exporter.Zipkin.csproj index bd6cfc37423..c6cda3031d3 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/OpenTelemetry.Exporter.Zipkin.csproj +++ b/src/OpenTelemetry.Exporter.Zipkin/OpenTelemetry.Exporter.Zipkin.csproj @@ -1,9 +1,11 @@  - netstandard2.0;net461 + netstandard2.0;net461;net5.0 Zipkin exporter for OpenTelemetry .NET $(PackageTags);Zipkin;distributed-tracing core- + + false @@ -14,6 +16,9 @@ + + + diff --git a/src/OpenTelemetry.Exporter.Zipkin/README.md b/src/OpenTelemetry.Exporter.Zipkin/README.md index 779d464be55..ba7775805f4 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/README.md +++ b/src/OpenTelemetry.Exporter.Zipkin/README.md @@ -26,19 +26,29 @@ take precedence over the environment variables. ### Configuration using Properties +* `BatchExportProcessorOptions`: Configuration options for the batch exporter. + Only used if ExportProcessorType is set to Batch. + +* `Endpoint`: URI address to receive telemetry (default + `http://localhost:9411/api/v2/spans`). + +* `ExportProcessorType`: Whether the exporter should use [Batch or Simple + exporting + processor](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#built-in-span-processors). + +* `HttpClientFactory`: A factory function called to create the `HttpClient` + instance that will be used at runtime to transmit spans over HTTP. See + [Configure HttpClient](#configure-httpclient) for more details. + +* `MaxPayloadSizeInBytes`: Maximum payload size of UTF8 JSON chunks sent to + Zipkin (default 4096). + * `ServiceName`: Name of the service reporting telemetry. If the `Resource` associated with the telemetry has "service.name" defined, then it'll be preferred over this option. -* `Endpoint`: URI address to receive telemetry (default `http://localhost:9411/api/v2/spans`). -* `UseShortTraceIds`: Whether the trace's ID should be shortened before - sending to Zipkin (default false). -* `MaxPayloadSizeInBytes`: Maximum payload size - for .NET versions - **other** than 4.5.2 (default 4096). -* `ExportProcessorType`: Whether the exporter should use - [Batch or Simple exporting processor](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#built-in-span-processors) - . -* `BatchExportProcessorOptions`: Configuration options for the batch exporter. - Only used if ExportProcessorType is set to Batch. + +* `UseShortTraceIds`: Whether the trace's ID should be shortened before sending + to Zipkin (default false). See [`TestZipkinExporter.cs`](../../examples/Console/TestZipkinExporter.cs) @@ -62,6 +72,41 @@ values of the `ZipkinExporterOptions`. | --------------------------------| -------------------------------- | | `OTEL_EXPORTER_ZIPKIN_ENDPOINT` | `Endpoint` | +`FormatException` is thrown in case of an invalid value for any of the +supported environment variables. + +## Configure HttpClient + +The `HttpClientFactory` option is provided on `ZipkinExporterOptions` for users +who want to configure the `HttpClient` used by the `ZipkinExporter`. Simply +replace the function with your own implementation if you want to customize the +generated `HttpClient`: + +```csharp +services.AddOpenTelemetryTracing((builder) => builder + .AddZipkinExporter(o => o.HttpClientFactory = () => + { + HttpClient client = new HttpClient(); + client.DefaultRequestHeaders.Add("X-MyCustomHeader", "value"); + return client; + })); +``` + +For users using +[IHttpClientFactory](https://docs.microsoft.com/dotnet/architecture/microservices/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests) +you may also customize the named "ZipkinExporter" `HttpClient` using the +built-in `AddHttpClient` extension: + +```csharp +services.AddHttpClient( + "ZipkinExporter", + configureClient: (client) => + client.DefaultRequestHeaders.Add("X-MyCustomHeader", "value")); +``` + +Note: The single instance returned by `HttpClientFactory` is reused by all +export requests. + ## References * [OpenTelemetry Project](https://opentelemetry.io/) diff --git a/src/OpenTelemetry.Exporter.Zipkin/ZipkinExporter.cs b/src/OpenTelemetry.Exporter.Zipkin/ZipkinExporter.cs index 08e5530bae5..899147ac9c8 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/ZipkinExporter.cs +++ b/src/OpenTelemetry.Exporter.Zipkin/ZipkinExporter.cs @@ -22,10 +22,12 @@ using System.Net.Http; using System.Net.Http.Headers; using System.Net.Sockets; +using System.Runtime.CompilerServices; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using OpenTelemetry.Exporter.Zipkin.Implementation; +using OpenTelemetry.Internal; using OpenTelemetry.Resources; namespace OpenTelemetry.Exporter @@ -46,9 +48,11 @@ public class ZipkinExporter : BaseExporter /// Http client to use to upload telemetry. public ZipkinExporter(ZipkinExporterOptions options, HttpClient client = null) { - this.options = options ?? throw new ArgumentNullException(nameof(options)); + Guard.ThrowIfNull(options, nameof(options)); + + this.options = options; this.maxPayloadSizeInBytes = (!options.MaxPayloadSizeInBytes.HasValue || options.MaxPayloadSizeInBytes <= 0) ? ZipkinExporterOptions.DefaultMaxPayloadSizeInBytes : options.MaxPayloadSizeInBytes.Value; - this.httpClient = client ?? new HttpClient(); + this.httpClient = client ?? options.HttpClientFactory?.Invoke() ?? throw new InvalidOperationException("ZipkinExporter was missing HttpClientFactory or it returned null."); } internal ZipkinEndpoint LocalEndpoint { get; private set; } @@ -73,7 +77,11 @@ public override ExportResult Export(in Batch batch) Content = new JsonContent(this, batch), }; +#if NET5_0_OR_GREATER + using var response = this.httpClient.Send(request, CancellationToken.None); +#else using var response = this.httpClient.SendAsync(request, CancellationToken.None).GetAwaiter().GetResult(); +#endif response.EnsureSuccessStatusCode(); @@ -179,7 +187,7 @@ private static string ResolveHostName() return result; } - private class JsonContent : HttpContent + private sealed class JsonContent : HttpContent { private static readonly MediaTypeHeaderValue JsonHeader = new MediaTypeHeaderValue("application/json") { @@ -198,7 +206,28 @@ public JsonContent(ZipkinExporter exporter, in Batch batch) this.Headers.ContentType = JsonHeader; } +#if NET5_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) { if (this.writer == null) { @@ -227,14 +256,6 @@ protected override Task SerializeToStreamAsync(Stream stream, TransportContext c this.writer.WriteEndArray(); this.writer.Flush(); - 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; } } } diff --git a/src/OpenTelemetry.Exporter.Zipkin/ZipkinExporterHelperExtensions.cs b/src/OpenTelemetry.Exporter.Zipkin/ZipkinExporterHelperExtensions.cs index 78c8ca3adcf..ce57441e182 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/ZipkinExporterHelperExtensions.cs +++ b/src/OpenTelemetry.Exporter.Zipkin/ZipkinExporterHelperExtensions.cs @@ -15,7 +15,10 @@ // using System; +using System.Net.Http; +using System.Reflection; using OpenTelemetry.Exporter; +using OpenTelemetry.Internal; namespace OpenTelemetry.Trace { @@ -33,26 +36,54 @@ public static class ZipkinExporterHelperExtensions [System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "The objects should not be disposed.")] public static TracerProviderBuilder AddZipkinExporter(this TracerProviderBuilder builder, Action configure = null) { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } + Guard.ThrowIfNull(builder, nameof(builder)); if (builder is IDeferredTracerProviderBuilder deferredTracerProviderBuilder) { return deferredTracerProviderBuilder.Configure((sp, builder) => { - AddZipkinExporter(builder, sp.GetOptions(), configure); + AddZipkinExporter(builder, sp.GetOptions(), configure, sp); }); } - return AddZipkinExporter(builder, new ZipkinExporterOptions(), configure); + return AddZipkinExporter(builder, new ZipkinExporterOptions(), configure, serviceProvider: null); } - private static TracerProviderBuilder AddZipkinExporter(TracerProviderBuilder builder, ZipkinExporterOptions options, Action configure = null) + private static TracerProviderBuilder AddZipkinExporter( + TracerProviderBuilder builder, + ZipkinExporterOptions options, + Action configure, + IServiceProvider serviceProvider) { configure?.Invoke(options); + if (serviceProvider != null && options.HttpClientFactory == ZipkinExporterOptions.DefaultHttpClientFactory) + { + options.HttpClientFactory = () => + { + Type httpClientFactoryType = Type.GetType("System.Net.Http.IHttpClientFactory, Microsoft.Extensions.Http", throwOnError: false); + if (httpClientFactoryType != null) + { + object httpClientFactory = serviceProvider.GetService(httpClientFactoryType); + if (httpClientFactory != null) + { + MethodInfo createClientMethod = httpClientFactoryType.GetMethod( + "CreateClient", + BindingFlags.Public | BindingFlags.Instance, + binder: null, + new Type[] { typeof(string) }, + modifiers: null); + if (createClientMethod != null) + { + return (HttpClient)createClientMethod.Invoke(httpClientFactory, new object[] { "ZipkinExporter" }); + } + } + } + + return new HttpClient(); + }; + } + var zipkinExporter = new ZipkinExporter(options); if (options.ExportProcessorType == ExportProcessorType.Simple) diff --git a/src/OpenTelemetry.Exporter.Zipkin/ZipkinExporterOptions.cs b/src/OpenTelemetry.Exporter.Zipkin/ZipkinExporterOptions.cs index 21df9181fd7..9c354574a72 100644 --- a/src/OpenTelemetry.Exporter.Zipkin/ZipkinExporterOptions.cs +++ b/src/OpenTelemetry.Exporter.Zipkin/ZipkinExporterOptions.cs @@ -16,33 +16,38 @@ using System; using System.Diagnostics; -using OpenTelemetry.Exporter.Zipkin.Implementation; +using System.Net.Http; +using OpenTelemetry.Internal; +using OpenTelemetry.Trace; namespace OpenTelemetry.Exporter { /// - /// Zipkin trace exporter options. + /// Zipkin span exporter options. + /// OTEL_EXPORTER_ZIPKIN_ENDPOINT + /// environment variables are parsed during object construction. /// + /// + /// The constructor throws if it fails to parse + /// any of the supported environment variables. + /// public sealed class ZipkinExporterOptions { internal const int DefaultMaxPayloadSizeInBytes = 4096; internal const string ZipkinEndpointEnvVar = "OTEL_EXPORTER_ZIPKIN_ENDPOINT"; internal const string DefaultZipkinEndpoint = "http://localhost:9411/api/v2/spans"; + internal static readonly Func DefaultHttpClientFactory = () => new HttpClient(); + /// /// Initializes a new instance of the class. /// Initializes zipkin endpoint. /// public ZipkinExporterOptions() { - try - { - this.Endpoint = new Uri(Environment.GetEnvironmentVariable(ZipkinEndpointEnvVar) ?? DefaultZipkinEndpoint); - } - catch (Exception ex) + if (EnvironmentVariableHelper.LoadUri(ZipkinEndpointEnvVar, out Uri endpoint)) { - this.Endpoint = new Uri(DefaultZipkinEndpoint); - ZipkinExporterEventSource.Log.FailedEndpointInitialization(ex); + this.Endpoint = endpoint; } } @@ -50,7 +55,7 @@ public ZipkinExporterOptions() /// Gets or sets Zipkin endpoint address. See https://zipkin.io/zipkin-api/#/default/post_spans. /// Typically https://zipkin-server-name:9411/api/v2/spans. /// - public Uri Endpoint { get; set; } + public Uri Endpoint { get; set; } = new Uri(DefaultZipkinEndpoint); /// /// Gets or sets a value indicating whether short trace id should be used. @@ -70,6 +75,24 @@ public ZipkinExporterOptions() /// /// Gets or sets the BatchExportProcessor options. Ignored unless ExportProcessorType is BatchExporter. /// - public BatchExportProcessorOptions BatchExportProcessorOptions { get; set; } = new BatchExportProcessorOptions(); + public BatchExportProcessorOptions BatchExportProcessorOptions { get; set; } = new BatchExportActivityProcessorOptions(); + + /// + /// Gets or sets the factory function called to create the instance that will be used at runtime to + /// transmit spans over HTTP. The returned instance will be reused for + /// all export invocations. + /// + /// + /// Note: The default behavior when using the extension is if an IHttpClientFactory + /// instance can be resolved through the application then an will be + /// created through the factory with the name "ZipkinExporter" otherwise + /// an will be instantiated directly. + /// + public Func HttpClientFactory { get; set; } = DefaultHttpClientFactory; } } diff --git a/src/OpenTelemetry.Extensions.Hosting/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Extensions.Hosting/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt index 9058cbe86ff..42d38da2ef3 100644 --- a/src/OpenTelemetry.Extensions.Hosting/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Extensions.Hosting/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt @@ -1,7 +1,15 @@ Microsoft.Extensions.DependencyInjection.OpenTelemetryServicesExtensions +OpenTelemetry.Metrics.MeterProviderBuilderExtensions OpenTelemetry.Trace.TracerProviderBuilderExtensions +static Microsoft.Extensions.DependencyInjection.OpenTelemetryServicesExtensions.AddOpenTelemetryMetrics(this Microsoft.Extensions.DependencyInjection.IServiceCollection services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection +static Microsoft.Extensions.DependencyInjection.OpenTelemetryServicesExtensions.AddOpenTelemetryMetrics(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, System.Action configure) -> Microsoft.Extensions.DependencyInjection.IServiceCollection static Microsoft.Extensions.DependencyInjection.OpenTelemetryServicesExtensions.AddOpenTelemetryTracing(this Microsoft.Extensions.DependencyInjection.IServiceCollection services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection static Microsoft.Extensions.DependencyInjection.OpenTelemetryServicesExtensions.AddOpenTelemetryTracing(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, System.Action configure) -> Microsoft.Extensions.DependencyInjection.IServiceCollection +static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder meterProviderBuilder) -> OpenTelemetry.Metrics.MeterProviderBuilder +static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddReader(this OpenTelemetry.Metrics.MeterProviderBuilder meterProviderBuilder) -> OpenTelemetry.Metrics.MeterProviderBuilder +static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.Build(this OpenTelemetry.Metrics.MeterProviderBuilder meterProviderBuilder, System.IServiceProvider serviceProvider) -> OpenTelemetry.Metrics.MeterProvider +static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.Configure(this OpenTelemetry.Metrics.MeterProviderBuilder meterProviderBuilder, System.Action configure) -> OpenTelemetry.Metrics.MeterProviderBuilder +static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.GetServices(this OpenTelemetry.Metrics.MeterProviderBuilder meterProviderBuilder) -> Microsoft.Extensions.DependencyInjection.IServiceCollection static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder tracerProviderBuilder) -> OpenTelemetry.Trace.TracerProviderBuilder static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddProcessor(this OpenTelemetry.Trace.TracerProviderBuilder tracerProviderBuilder) -> OpenTelemetry.Trace.TracerProviderBuilder static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddSelfDiagnosticsLogging(this OpenTelemetry.Trace.TracerProviderBuilder tracerProviderBuilder) -> OpenTelemetry.Trace.TracerProviderBuilder diff --git a/src/OpenTelemetry.Extensions.Hosting/CHANGELOG.md b/src/OpenTelemetry.Extensions.Hosting/CHANGELOG.md index e3801c6ffbb..03c76fd5a3e 100644 --- a/src/OpenTelemetry.Extensions.Hosting/CHANGELOG.md +++ b/src/OpenTelemetry.Extensions.Hosting/CHANGELOG.md @@ -2,9 +2,19 @@ ## Unreleased +## 1.0.0-rc8 + +Released 2021-Oct-08 + * Removes upper constraint for Microsoft.Extensions.Hosting.Abstractions dependency. ([#2179](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2179)) +* Added `AddOpenTelemetryMetrics` extensions on `IServiceCollection` to register + OpenTelemetry `MeterProvider` with application services. Added + `AddInstrumentation`, `AddReader`, and `Configure` extensions on + `MeterProviderBuilder` to support dependency injection scenarios. + ([#2412](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2412)) + ## 1.0.0-rc7 Released 2021-Jul-12 diff --git a/src/OpenTelemetry.Extensions.Hosting/Implementation/MeterProviderBuilderHosting.cs b/src/OpenTelemetry.Extensions.Hosting/Implementation/MeterProviderBuilderHosting.cs new file mode 100644 index 00000000000..4ee28ab5aac --- /dev/null +++ b/src/OpenTelemetry.Extensions.Hosting/Implementation/MeterProviderBuilderHosting.cs @@ -0,0 +1,62 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.DependencyInjection; +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Metrics +{ + /// + /// A with support for deferred initialization using for dependency injection. + /// + internal sealed class MeterProviderBuilderHosting : MeterProviderBuilderBase, IDeferredMeterProviderBuilder + { + private readonly List> configurationActions = new List>(); + + public MeterProviderBuilderHosting(IServiceCollection services) + { + Guard.ThrowIfNull(services, nameof(services)); + + this.Services = services; + } + + public IServiceCollection Services { get; } + + public MeterProviderBuilder Configure(Action configure) + { + Guard.ThrowIfNull(configure, nameof(configure)); + + this.configurationActions.Add(configure); + return this; + } + + public MeterProvider Build(IServiceProvider serviceProvider) + { + Guard.ThrowIfNull(serviceProvider, nameof(serviceProvider)); + + // Note: Not using a foreach loop because additional actions can be + // added during each call. + for (int i = 0; i < this.configurationActions.Count; i++) + { + this.configurationActions[i](serviceProvider, this); + } + + return this.Build(); + } + } +} diff --git a/src/OpenTelemetry.Extensions.Hosting/Implementation/TelemetryHostedService.cs b/src/OpenTelemetry.Extensions.Hosting/Implementation/TelemetryHostedService.cs index ae29353ecce..1551667aa86 100644 --- a/src/OpenTelemetry.Extensions.Hosting/Implementation/TelemetryHostedService.cs +++ b/src/OpenTelemetry.Extensions.Hosting/Implementation/TelemetryHostedService.cs @@ -19,6 +19,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using OpenTelemetry.Metrics; using OpenTelemetry.Trace; namespace OpenTelemetry.Extensions.Hosting.Implementation @@ -36,12 +37,15 @@ public Task StartAsync(CancellationToken cancellationToken) { try { - // The sole purpose of this HostedService is to ensure - // all instrumentations are created and started. - // This method is invoked when host starts, and - // by requesting the TracerProvider from DI - // it ensures all instrumentations gets started. - this.serviceProvider.GetRequiredService(); + // The sole purpose of this HostedService is to ensure all + // instrumentations, exporters, etc., are created and started. + var meterProvider = this.serviceProvider.GetService(); + var tracerProvider = this.serviceProvider.GetService(); + + if (meterProvider == null && tracerProvider == null) + { + throw new InvalidOperationException("Could not resolve either MeterProvider or TracerProvider through application ServiceProvider, OpenTelemetry SDK has not been initialized."); + } } catch (Exception ex) { diff --git a/src/OpenTelemetry.Extensions.Hosting/Implementation/TracerProviderBuilderHosting.cs b/src/OpenTelemetry.Extensions.Hosting/Implementation/TracerProviderBuilderHosting.cs index 4d0b1658fae..a5a5dc06be2 100644 --- a/src/OpenTelemetry.Extensions.Hosting/Implementation/TracerProviderBuilderHosting.cs +++ b/src/OpenTelemetry.Extensions.Hosting/Implementation/TracerProviderBuilderHosting.cs @@ -17,29 +17,29 @@ using System; using System.Collections.Generic; using Microsoft.Extensions.DependencyInjection; +using OpenTelemetry.Internal; namespace OpenTelemetry.Trace { /// /// A with support for deferred initialization using for dependency injection. /// - internal class TracerProviderBuilderHosting : TracerProviderBuilderBase, IDeferredTracerProviderBuilder + internal sealed class TracerProviderBuilderHosting : TracerProviderBuilderBase, IDeferredTracerProviderBuilder { private readonly List> configurationActions = new List>(); public TracerProviderBuilderHosting(IServiceCollection services) { - this.Services = services ?? throw new ArgumentNullException(nameof(services)); + Guard.ThrowIfNull(services, nameof(services)); + + this.Services = services; } public IServiceCollection Services { get; } public TracerProviderBuilder Configure(Action configure) { - if (configure == null) - { - throw new ArgumentNullException(nameof(configure)); - } + Guard.ThrowIfNull(configure, nameof(configure)); this.configurationActions.Add(configure); return this; @@ -47,10 +47,13 @@ public TracerProviderBuilder Configure(Action +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using Microsoft.Extensions.DependencyInjection; + +namespace OpenTelemetry.Metrics +{ + /// + /// Contains extension methods for the class. + /// + public static class MeterProviderBuilderExtensions + { + /// + /// Adds instrumentation to the provider. + /// + /// Instrumentation type. + /// . + /// The supplied for chaining. + public static MeterProviderBuilder AddInstrumentation(this MeterProviderBuilder meterProviderBuilder) + where T : class + { + if (meterProviderBuilder is MeterProviderBuilderHosting meterProviderBuilderHosting) + { + meterProviderBuilderHosting.Configure((sp, builder) => builder + .AddInstrumentation(() => sp.GetRequiredService())); + } + + return meterProviderBuilder; + } + + /// + /// Adds a reader to the provider. + /// + /// Reader type. + /// . + /// The supplied for chaining. + public static MeterProviderBuilder AddReader(this MeterProviderBuilder meterProviderBuilder) + where T : MetricReader + { + if (meterProviderBuilder is MeterProviderBuilderHosting meterProviderBuilderHosting) + { + meterProviderBuilderHosting.Configure((sp, builder) => builder + .AddReader(sp.GetRequiredService())); + } + + return meterProviderBuilder; + } + + /// + /// Register a callback action to configure the once the application is available. + /// + /// . + /// Configuration callback. + /// The supplied for chaining. + public static MeterProviderBuilder Configure(this MeterProviderBuilder meterProviderBuilder, Action configure) + { + if (meterProviderBuilder is IDeferredMeterProviderBuilder deferredMeterProviderBuilder) + { + deferredMeterProviderBuilder.Configure(configure); + } + + return meterProviderBuilder; + } + + /// + /// Gets the application attached to + /// the . + /// + /// . + /// or + /// if services are unavailable. + public static IServiceCollection GetServices(this MeterProviderBuilder meterProviderBuilder) + { + if (meterProviderBuilder is MeterProviderBuilderHosting meterProviderBuilderHosting) + { + return meterProviderBuilderHosting.Services; + } + + return null; + } + + /// + /// Run the configured actions to initialize the . + /// + /// . + /// . + /// . + public static MeterProvider Build(this MeterProviderBuilder meterProviderBuilder, IServiceProvider serviceProvider) + { + if (meterProviderBuilder is MeterProviderBuilderHosting meterProviderBuilderHosting) + { + return meterProviderBuilderHosting.Build(serviceProvider); + } + + if (meterProviderBuilder is MeterProviderBuilderBase meterProviderBuilderBase) + { + return meterProviderBuilderBase.Build(); + } + + return null; + } + } +} diff --git a/src/OpenTelemetry.Extensions.Hosting/OpenTelemetry.Extensions.Hosting.csproj b/src/OpenTelemetry.Extensions.Hosting/OpenTelemetry.Extensions.Hosting.csproj index 9c40cdb9c8d..b8b9142b8b0 100644 --- a/src/OpenTelemetry.Extensions.Hosting/OpenTelemetry.Extensions.Hosting.csproj +++ b/src/OpenTelemetry.Extensions.Hosting/OpenTelemetry.Extensions.Hosting.csproj @@ -11,6 +11,7 @@ + diff --git a/src/OpenTelemetry.Extensions.Hosting/OpenTelemetryServicesExtensions.cs b/src/OpenTelemetry.Extensions.Hosting/OpenTelemetryServicesExtensions.cs index 441d29bd668..8ec12882673 100644 --- a/src/OpenTelemetry.Extensions.Hosting/OpenTelemetryServicesExtensions.cs +++ b/src/OpenTelemetry.Extensions.Hosting/OpenTelemetryServicesExtensions.cs @@ -15,7 +15,12 @@ // using System; +using System.Diagnostics; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; using OpenTelemetry.Extensions.Hosting.Implementation; +using OpenTelemetry.Internal; +using OpenTelemetry.Metrics; using OpenTelemetry.Trace; namespace Microsoft.Extensions.DependencyInjection @@ -43,16 +48,38 @@ public static IServiceCollection AddOpenTelemetryTracing(this IServiceCollection /// The so that additional calls can be chained. public static IServiceCollection AddOpenTelemetryTracing(this IServiceCollection services, Action configure) { - if (configure is null) - { - throw new ArgumentNullException(nameof(configure)); - } + Guard.ThrowIfNull(configure, nameof(configure)); var builder = new TracerProviderBuilderHosting(services); configure(builder); return services.AddOpenTelemetryTracing(sp => builder.Build(sp)); } + /// + /// Adds OpenTelemetry MeterProvider to the specified . + /// + /// The to add services to. + /// The so that additional calls can be chained. + public static IServiceCollection AddOpenTelemetryMetrics(this IServiceCollection services) + { + return services.AddOpenTelemetryMetrics(builder => { }); + } + + /// + /// Adds OpenTelemetry MeterProvider to the specified . + /// + /// The to add services to. + /// Callback action to configure the . + /// The so that additional calls can be chained. + public static IServiceCollection AddOpenTelemetryMetrics(this IServiceCollection services, Action configure) + { + Guard.ThrowIfNull(configure, nameof(configure)); + + var builder = new MeterProviderBuilderHosting(services); + configure(builder); + return services.AddOpenTelemetryMetrics(sp => builder.Build(sp)); + } + /// /// Adds OpenTelemetry TracerProvider to the specified . /// @@ -61,20 +88,37 @@ public static IServiceCollection AddOpenTelemetryTracing(this IServiceCollection /// The so that additional calls can be chained. private static IServiceCollection AddOpenTelemetryTracing(this IServiceCollection services, Func createTracerProvider) { - if (services is null) + Guard.ThrowIfNull(services, nameof(services)); + Guard.ThrowIfNull(createTracerProvider, nameof(createTracerProvider)); + + try { - throw new ArgumentNullException(nameof(services)); + services.TryAddEnumerable(ServiceDescriptor.Singleton()); + return services.AddSingleton(s => createTracerProvider(s)); } - - if (createTracerProvider is null) + catch (Exception ex) { - throw new ArgumentNullException(nameof(createTracerProvider)); + HostingExtensionsEventSource.Log.FailedInitialize(ex); } + return services; + } + + /// + /// Adds OpenTelemetry MeterProvider to the specified . + /// + /// The to add services to. + /// A delegate that provides the tracer provider to be registered. + /// The so that additional calls can be chained. + private static IServiceCollection AddOpenTelemetryMetrics(this IServiceCollection services, Func createMeterProvider) + { + Debug.Assert(services != null, $"{nameof(services)} must not be null"); + Debug.Assert(createMeterProvider != null, $"{nameof(createMeterProvider)} must not be null"); + try { services.AddHostedService(); - return services.AddSingleton(s => createTracerProvider(s)); + return services.AddSingleton(s => createMeterProvider(s)); } catch (Exception ex) { diff --git a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/.publicApi/net461/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/.publicApi/net461/PublicAPI.Unshipped.txt index 7e00f288d20..367f7a1e6e6 100644 --- a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/.publicApi/net461/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/.publicApi/net461/PublicAPI.Unshipped.txt @@ -1,9 +1,16 @@ -OpenTelemetry.Instrumentation.AspNet.ActivityExtensions +const OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.AspNetActivityName = "Microsoft.AspNet.HttpReqIn" -> string +const OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.AspNetSourceName = "OpenTelemetry.Instrumentation.AspNet.Telemetry" -> string OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.Dispose() -> void OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.Init(System.Web.HttpApplication context) -> void -OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.ParseHeaders.get -> bool -OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.ParseHeaders.set -> void OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.TelemetryHttpModule() -> void -static OpenTelemetry.Instrumentation.AspNet.ActivityExtensions.Extract(this System.Diagnostics.Activity activity, System.Collections.Specialized.NameValueCollection requestHeaders) -> bool -static OpenTelemetry.Instrumentation.AspNet.ActivityExtensions.TryParse(this System.Diagnostics.Activity activity, System.Collections.Specialized.NameValueCollection requestHeaders) -> bool \ No newline at end of file +OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModuleOptions +OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModuleOptions.OnExceptionCallback.get -> System.Action +OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModuleOptions.OnExceptionCallback.set -> void +OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModuleOptions.OnRequestStartedCallback.get -> System.Action +OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModuleOptions.OnRequestStartedCallback.set -> void +OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModuleOptions.OnRequestStoppedCallback.get -> System.Action +OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModuleOptions.OnRequestStoppedCallback.set -> void +OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModuleOptions.TextMapPropagator.get -> OpenTelemetry.Context.Propagation.TextMapPropagator +OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModuleOptions.TextMapPropagator.set -> void +static OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.Options.get -> OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModuleOptions diff --git a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/ActivityExtensions.cs b/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/ActivityExtensions.cs deleted file mode 100644 index e5a542c76fb..00000000000 --- a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/ActivityExtensions.cs +++ /dev/null @@ -1,159 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -using System; -using System.Collections.Specialized; -using System.ComponentModel; -using System.Diagnostics; - -namespace OpenTelemetry.Instrumentation.AspNet -{ - /// - /// Extensions of Activity class. - /// - [EditorBrowsable(EditorBrowsableState.Never)] - public static class ActivityExtensions - { - /// - /// Http header name to carry the Request Id: https://github.com/dotnet/corefx/blob/master/src/System.Diagnostics.DiagnosticSource/src/HttpCorrelationProtocol.md. - /// - internal const string RequestIdHeaderName = "Request-Id"; - - /// - /// Http header name to carry the traceparent: https://www.w3.org/TR/trace-context/. - /// - internal const string TraceparentHeaderName = "traceparent"; - - /// - /// Http header name to carry the tracestate: https://www.w3.org/TR/trace-context/. - /// - internal const string TracestateHeaderName = "tracestate"; - - /// - /// Http header name to carry the correlation context. - /// - internal const string CorrelationContextHeaderName = "Correlation-Context"; - - /// - /// Maximum length of Correlation-Context header value. - /// - internal const int MaxCorrelationContextLength = 1024; - - /// - /// Reads Request-Id and Correlation-Context headers and sets ParentId and Baggage on Activity. - /// - /// Instance of activity that has not been started yet. - /// Request headers collection. - /// true if request was parsed successfully, false - otherwise. - public static bool Extract(this Activity activity, NameValueCollection requestHeaders) - { - if (activity == null) - { - AspNetTelemetryEventSource.Log.ActvityExtractionError("activity is null"); - return false; - } - - if (activity.ParentId != null) - { - AspNetTelemetryEventSource.Log.ActvityExtractionError("ParentId is already set on activity"); - return false; - } - - if (activity.Id != null) - { - AspNetTelemetryEventSource.Log.ActvityExtractionError("Activity is already started"); - return false; - } - - var parents = requestHeaders.GetValues(TraceparentHeaderName); - if (parents == null || parents.Length == 0) - { - parents = requestHeaders.GetValues(RequestIdHeaderName); - } - - if (parents != null && parents.Length > 0 && !string.IsNullOrEmpty(parents[0])) - { - // there may be several Request-Id or traceparent headers, but we only read the first one - activity.SetParentId(parents[0]); - - var tracestates = requestHeaders.GetValues(TracestateHeaderName); - if (tracestates != null && tracestates.Length > 0) - { - if (tracestates.Length == 1 && !string.IsNullOrEmpty(tracestates[0])) - { - activity.TraceStateString = tracestates[0]; - } - else - { - activity.TraceStateString = string.Join(",", tracestates); - } - } - - // Header format - Correlation-Context: key1=value1, key2=value2 - var baggages = requestHeaders.GetValues(CorrelationContextHeaderName); - if (baggages != null) - { - int correlationContextLength = -1; - - // there may be several Correlation-Context header - foreach (var item in baggages) - { - if (correlationContextLength >= MaxCorrelationContextLength) - { - break; - } - - foreach (var pair in item.Split(',')) - { - correlationContextLength += pair.Length + 1; // pair and comma - - if (correlationContextLength >= MaxCorrelationContextLength) - { - break; - } - - if (NameValueHeaderValue.TryParse(pair, out NameValueHeaderValue baggageItem)) - { - activity.AddBaggage(baggageItem.Name, baggageItem.Value); - } - else - { - AspNetTelemetryEventSource.Log.HeaderParsingError(CorrelationContextHeaderName, pair); - } - } - } - } - - return true; - } - - return false; - } - - /// - /// Reads Request-Id and Correlation-Context headers and sets ParentId and Baggage on Activity. - /// - /// Instance of activity that has not been started yet. - /// Request headers collection. - /// true if request was parsed successfully, false - otherwise. - [Obsolete("Method is obsolete, use Extract method instead", true)] - [EditorBrowsable(EditorBrowsableState.Never)] - public static bool TryParse(this Activity activity, NameValueCollection requestHeaders) - { - return Extract(activity, requestHeaders); - } - } -} diff --git a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/ActivityHelper.cs b/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/ActivityHelper.cs index 7826968996e..62bdd71d030 100644 --- a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/ActivityHelper.cs +++ b/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/ActivityHelper.cs @@ -15,9 +15,12 @@ // using System; -using System.Collections; +using System.Collections.Generic; using System.Diagnostics; +using System.Runtime.CompilerServices; using System.Web; +using OpenTelemetry.Context; +using OpenTelemetry.Context.Propagation; namespace OpenTelemetry.Instrumentation.AspNet { @@ -27,97 +30,164 @@ namespace OpenTelemetry.Instrumentation.AspNet internal static class ActivityHelper { /// - /// Listener name. + /// Key to store the state in HttpContext. /// - public const string AspNetListenerName = "OpenTelemetry.Instrumentation.AspNet.Telemetry"; + internal const string ContextKey = "__AspnetInstrumentationContext__"; + internal static readonly object StartedButNotSampledObj = new object(); - /// - /// Activity name for http request. - /// - public const string AspNetActivityName = "Microsoft.AspNet.HttpReqIn"; - - /// - /// Event name for the activity start event. - /// - public const string AspNetActivityStartName = "Microsoft.AspNet.HttpReqIn.Start"; + private const string BaggageSlotName = "otel.baggage"; + private static readonly Func> HttpRequestHeaderValuesGetter = (request, name) => request.Headers.GetValues(name); + private static readonly ActivitySource AspNetSource = new ActivitySource( + TelemetryHttpModule.AspNetSourceName, + typeof(ActivityHelper).Assembly.GetName().Version.ToString()); /// - /// Key to store the activity in HttpContext. + /// Try to get the started for the running . /// - public const string ActivityKey = "__AspnetActivity__"; + /// . + /// Started or if 1) start has not been called or 2) start was + /// called but sampling decided not to create an instance. + /// if start has been called. + public static bool HasStarted(HttpContext context, out Activity aspNetActivity) + { + Debug.Assert(context != null, "Context is null."); - private static readonly DiagnosticListener AspNetListener = new DiagnosticListener(AspNetListenerName); + object itemValue = context.Items[ContextKey]; + if (itemValue is ContextHolder contextHolder) + { + aspNetActivity = contextHolder.Activity; + return true; + } - private static readonly object EmptyPayload = new object(); + aspNetActivity = null; + return itemValue == StartedButNotSampledObj; + } /// - /// Stops the activity and notifies listeners about it. + /// Creates root (first level) activity that describes incoming request. /// - /// HttpContext.Items. - public static void StopAspNetActivity(IDictionary contextItems) + /// . + /// . + /// Callback action. + /// New root activity. + public static Activity StartAspNetActivity(TextMapPropagator textMapPropagator, HttpContext context, Action onRequestStartedCallback) { - var currentActivity = Activity.Current; - Activity aspNetActivity = (Activity)contextItems[ActivityKey]; + Debug.Assert(context != null, "Context is null."); - if (currentActivity != aspNetActivity) + PropagationContext propagationContext = textMapPropagator.Extract(default, context.Request, HttpRequestHeaderValuesGetter); + + Activity activity = AspNetSource.StartActivity(TelemetryHttpModule.AspNetActivityName, ActivityKind.Server, propagationContext.ActivityContext); + + if (activity != null) { - Activity.Current = aspNetActivity; - currentActivity = aspNetActivity; - } + if (!(textMapPropagator is TraceContextPropagator)) + { + Baggage.Current = propagationContext.Baggage; - if (currentActivity != null) + context.Items[ContextKey] = new ContextHolder { Activity = activity, Baggage = RuntimeContext.GetValue(BaggageSlotName) }; + } + else + { + context.Items[ContextKey] = new ContextHolder { Activity = activity }; + } + + try + { + onRequestStartedCallback?.Invoke(activity, context); + } + catch (Exception callbackEx) + { + AspNetTelemetryEventSource.Log.CallbackException(activity, "OnStarted", callbackEx); + } + + AspNetTelemetryEventSource.Log.ActivityStarted(activity); + } + else { - // stop Activity with Stop event - AspNetListener.StopActivity(currentActivity, EmptyPayload); - contextItems[ActivityKey] = null; + context.Items[ContextKey] = StartedButNotSampledObj; } - AspNetTelemetryEventSource.Log.ActivityStopped(currentActivity?.Id, currentActivity?.OperationName); + return activity; } /// - /// Creates root (first level) activity that describes incoming request. + /// Stops the activity and notifies listeners about it. /// - /// Current HttpContext. - /// Determines if headers should be parsed get correlation ids. - /// New root activity. - public static Activity CreateRootActivity(HttpContext context, bool parseHeaders) + /// . + /// . + /// . + /// Callback action. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void StopAspNetActivity(TextMapPropagator textMapPropagator, Activity aspNetActivity, HttpContext context, Action onRequestStoppedCallback) { - if (AspNetListener.IsEnabled() && AspNetListener.IsEnabled(AspNetActivityName)) + Debug.Assert(context != null, "Context is null."); + + if (aspNetActivity == null) { - var rootActivity = new Activity(AspNetActivityName); + Debug.Assert(context.Items[ContextKey] == StartedButNotSampledObj, "Context item is not StartedButNotSampledObj."); - if (parseHeaders) - { - rootActivity.Extract(context.Request.Unvalidated.Headers); - } + // This is the case where a start was called but no activity was + // created due to a sampling decision. + context.Items[ContextKey] = null; + return; + } - AspNetListener.OnActivityImport(rootActivity, null); + Debug.Assert(context.Items[ContextKey] is ContextHolder, "Context item is not an ContextHolder instance."); - if (StartAspNetActivity(rootActivity)) - { - context.Items[ActivityKey] = rootActivity; - AspNetTelemetryEventSource.Log.ActivityStarted(rootActivity.Id); - return rootActivity; - } + var currentActivity = Activity.Current; + + aspNetActivity.Stop(); + context.Items[ContextKey] = null; + + try + { + onRequestStoppedCallback?.Invoke(aspNetActivity, context); + } + catch (Exception callbackEx) + { + AspNetTelemetryEventSource.Log.CallbackException(aspNetActivity, "OnStopped", callbackEx); + } + + AspNetTelemetryEventSource.Log.ActivityStopped(currentActivity); + + if (!(textMapPropagator is TraceContextPropagator)) + { + Baggage.Current = default; } - return null; + if (currentActivity != aspNetActivity) + { + Activity.Current = currentActivity; + } } - public static void WriteActivityException(IDictionary contextItems, Exception exception) + /// + /// Notifies listeners about an unhandled exception thrown on the . + /// + /// . + /// . + /// . + /// Callback action. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteActivityException(Activity aspNetActivity, HttpContext context, Exception exception, Action onExceptionCallback) { - Activity aspNetActivity = (Activity)contextItems[ActivityKey]; + Debug.Assert(context != null, "Context is null."); + Debug.Assert(exception != null, "Exception is null."); if (aspNetActivity != null) { - if (Activity.Current != aspNetActivity) + try + { + onExceptionCallback?.Invoke(aspNetActivity, context, exception); + } + catch (Exception callbackEx) { - Activity.Current = aspNetActivity; + AspNetTelemetryEventSource.Log.CallbackException(aspNetActivity, "OnException", callbackEx); } - AspNetListener.Write(aspNetActivity.OperationName + ".Exception", exception); - AspNetTelemetryEventSource.Log.ActivityException(aspNetActivity.Id, aspNetActivity.OperationName, exception); + AspNetTelemetryEventSource.Log.ActivityException(aspNetActivity, exception); } } @@ -127,36 +197,28 @@ public static void WriteActivityException(IDictionary contextItems, Exception ex /// This method is intended to restore the current activity in order to correlate the child /// activities with the root activity of the request. /// - /// HttpContext.Items dictionary. - internal static void RestoreActivityIfNeeded(IDictionary contextItems) + /// . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static void RestoreContextIfNeeded(HttpContext context) { - if (Activity.Current == null) - { - Activity aspNetActivity = (Activity)contextItems[ActivityKey]; - if (aspNetActivity != null) - { - Activity.Current = aspNetActivity; - } - } - } + Debug.Assert(context != null, "Context is null."); - private static bool StartAspNetActivity(Activity activity) - { - if (AspNetListener.IsEnabled(AspNetActivityName, activity, EmptyPayload)) + if (context.Items[ContextKey] is ContextHolder contextHolder && Activity.Current != contextHolder.Activity) { - if (AspNetListener.IsEnabled(AspNetActivityStartName)) - { - AspNetListener.StartActivity(activity, EmptyPayload); - } - else + Activity.Current = contextHolder.Activity; + if (contextHolder.Baggage != null) { - activity.Start(); + RuntimeContext.SetValue(BaggageSlotName, contextHolder.Baggage); } - return true; + AspNetTelemetryEventSource.Log.ActivityRestored(contextHolder.Activity); } + } - return false; + internal class ContextHolder + { + public Activity Activity; + public object Baggage; } } } diff --git a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/AspNetTelemetryEventSource.cs b/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/AspNetTelemetryEventSource.cs index 602c5df99e9..9fa3a5ef871 100644 --- a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/AspNetTelemetryEventSource.cs +++ b/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/AspNetTelemetryEventSource.cs @@ -15,7 +15,9 @@ // using System; +using System.Diagnostics; using System.Diagnostics.Tracing; +using OpenTelemetry.Internal; namespace OpenTelemetry.Instrumentation.AspNet { @@ -31,78 +33,90 @@ internal sealed class AspNetTelemetryEventSource : EventSource public static readonly AspNetTelemetryEventSource Log = new AspNetTelemetryEventSource(); [NonEvent] - public void ActivityException(string id, string eventName, Exception ex) + public void ActivityStarted(Activity activity) { - if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) + if (this.IsEnabled(EventLevel.Verbose, EventKeywords.All)) { - this.ActivityException(id, eventName, ex.ToString()); + this.ActivityStarted(activity?.Id); } } - [Event(1, Message = "Callback='{0}'", Level = EventLevel.Verbose)] - public void TraceCallback(string callback) + [NonEvent] + public void ActivityStopped(Activity activity) { - this.WriteEvent(1, callback); + if (this.IsEnabled(EventLevel.Verbose, EventKeywords.All)) + { + this.ActivityStopped(activity?.Id); + } } - [Event(2, Message = "Activity started, Id='{0}'", Level = EventLevel.Verbose)] - public void ActivityStarted(string id) + [NonEvent] + public void ActivityRestored(Activity activity) { - this.WriteEvent(2, id); + if (this.IsEnabled(EventLevel.Informational, EventKeywords.All)) + { + this.ActivityRestored(activity?.Id); + } } - [Event(3, Message = "Activity stopped, Id='{0}', Name='{1}'", Level = EventLevel.Verbose)] - public void ActivityStopped(string id, string eventName) + [NonEvent] + public void ActivityException(Activity activity, Exception ex) { - this.WriteEvent(3, id, eventName); + if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) + { + this.ActivityException(activity?.Id, ex.ToInvariantString()); + } } - [Event(4, Message = "Failed to parse header '{0}', value: '{1}'", Level = EventLevel.Informational)] - public void HeaderParsingError(string headerName, string headerValue) + [NonEvent] + public void CallbackException(Activity activity, string eventName, Exception ex) { - this.WriteEvent(4, headerName, headerValue); + if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) + { + this.CallbackException(activity?.Id, eventName, ex.ToInvariantString()); + } } - [Event(5, Message = "Failed to extract activity, reason '{0}'", Level = EventLevel.Error)] - public void ActvityExtractionError(string reason) + [Event(1, Message = "Callback='{0}'", Level = EventLevel.Verbose)] + public void TraceCallback(string callback) { - this.WriteEvent(5, reason); + this.WriteEvent(1, callback); } - [Event(6, Message = "Finished Activity is detected on the stack, Id: '{0}', Name: '{1}'", Level = EventLevel.Error)] - public void FinishedActivityIsDetected(string id, string name) + [Event(2, Message = "Activity started, Id='{0}'", Level = EventLevel.Verbose)] + public void ActivityStarted(string id) { - this.WriteEvent(6, id, name); + this.WriteEvent(2, id); } - [Event(7, Message = "System.Diagnostics.Activity stack is too deep. This is a code authoring error, Activity will not be stopped.", Level = EventLevel.Error)] - public void ActivityStackIsTooDeepError() + [Event(3, Message = "Activity stopped, Id='{0}'", Level = EventLevel.Verbose)] + public void ActivityStopped(string id) { - this.WriteEvent(7); + this.WriteEvent(3, id); } - [Event(8, Message = "Activity restored, Id='{0}'", Level = EventLevel.Informational)] + [Event(4, Message = "Activity restored, Id='{0}'", Level = EventLevel.Informational)] public void ActivityRestored(string id) { - this.WriteEvent(8, id); + this.WriteEvent(4, id); } - [Event(9, Message = "Failed to invoke OnExecuteRequestStep, Error='{0}'", Level = EventLevel.Error)] + [Event(5, Message = "Failed to invoke OnExecuteRequestStep, Error='{0}'", Level = EventLevel.Error)] public void OnExecuteRequestStepInvokationError(string error) { - this.WriteEvent(9, error); + this.WriteEvent(5, error); } - [Event(10, Message = "System.Diagnostics.Activity stack is too deep. Current Id: '{0}', Name: '{1}'", Level = EventLevel.Warning)] - public void ActivityStackIsTooDeepDetails(string id, string name) + [Event(6, Message = "Activity exception, Id='{0}': {1}", Level = EventLevel.Error)] + public void ActivityException(string id, string ex) { - this.WriteEvent(10, id, name); + this.WriteEvent(6, id, ex); } - [Event(11, Message = "Activity exception, Id='{0}', Name='{1}': {2}", Level = EventLevel.Error)] - public void ActivityException(string id, string eventName, string ex) + [Event(7, Message = "Callback exception, Id='{0}', Name='{1}': {2}", Level = EventLevel.Error)] + public void CallbackException(string id, string eventName, string ex) { - this.WriteEvent(11, id, eventName, ex); + this.WriteEvent(7, id, eventName, ex); } } } diff --git a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/AssemblyInfo.cs b/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/AssemblyInfo.cs index 3d679575e65..5f10011c2fd 100644 --- a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/AssemblyInfo.cs +++ b/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/AssemblyInfo.cs @@ -21,6 +21,8 @@ #if SIGNED [assembly: InternalsVisibleTo("OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010051c1562a090fb0c9f391012a32198b5e5d9a60e9b80fa2d7b434c9e5ccb7259bd606e66f9660676afc6692b8cdc6793d190904551d2103b7b22fa636dcbb8208839785ba402ea08fc00c8f1500ccef28bbf599aa64ffb1e1d5dc1bf3420a3777badfe697856e9d52070a50c3ea5821c80bef17ca3acffa28f89dd413f096f898")] +[assembly: InternalsVisibleTo("OpenTelemetry.Instrumentation.AspNet.Tests, PublicKey=002400000480000094000000060200000024000052534131000400000100010051c1562a090fb0c9f391012a32198b5e5d9a60e9b80fa2d7b434c9e5ccb7259bd606e66f9660676afc6692b8cdc6793d190904551d2103b7b22fa636dcbb8208839785ba402ea08fc00c8f1500ccef28bbf599aa64ffb1e1d5dc1bf3420a3777badfe697856e9d52070a50c3ea5821c80bef17ca3acffa28f89dd413f096f898")] #else [assembly: InternalsVisibleTo("OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.Tests")] +[assembly: InternalsVisibleTo("OpenTelemetry.Instrumentation.AspNet.Tests")] #endif diff --git a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/CHANGELOG.md index 7f0685a7419..de8c950c389 100644 --- a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/CHANGELOG.md +++ b/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/CHANGELOG.md @@ -2,6 +2,15 @@ ## Unreleased +## 1.0.0-rc8 + +Released 2021-Oct-08 + +* Adopted the donation + [Microsoft.AspNet.TelemetryCorrelation](https://github.com/aspnet/Microsoft.AspNet.TelemetryCorrelation) + from [.NET Foundation](https://dotnetfoundation.org/) + ([#2223](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2223)) + * Renamed the module, refactored existing code ([#2224](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2224) [#2225](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2225) @@ -12,7 +21,12 @@ [#2238](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2238) [#2240](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2240)) -* Adopted the donation - [Microsoft.AspNet.TelemetryCorrelation](https://github.com/aspnet/Microsoft.AspNet.TelemetryCorrelation) - from [.NET Foundation](https://dotnetfoundation.org/) - ([#2223](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2223)) +* Updated to use + [ActivitySource](https://docs.microsoft.com/dotnet/api/system.diagnostics.activitysource) + & OpenTelemetry.API + ([#2249](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2249) & + follow-ups (linked to #2249)) + +* TelemetryHttpModule will now restore Baggage on .NET 4.7.1+ runtimes when IIS + switches threads + ([#2314](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2314)) diff --git a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/Internal/BaseHeaderParser.cs b/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/Internal/BaseHeaderParser.cs deleted file mode 100644 index 4c429212023..00000000000 --- a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/Internal/BaseHeaderParser.cs +++ /dev/null @@ -1,81 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -namespace OpenTelemetry.Instrumentation.AspNet -{ - // Adoptation of code from https://github.com/aspnet/HttpAbstractions/blob/07d115400e4f8c7a66ba239f230805f03a14ee3d/src/Microsoft.Net.Http.Headers/BaseHeaderParser.cs - internal abstract class BaseHeaderParser : HttpHeaderParser - { - protected BaseHeaderParser(bool supportsMultipleValues) - : base(supportsMultipleValues) - { - } - - public sealed override bool TryParseValue(string value, ref int index, out T parsedValue) - { - parsedValue = default; - - // If multiple values are supported (i.e. list of values), then accept an empty string: The header may - // be added multiple times to the request/response message. E.g. - // Accept: text/xml; q=1 - // Accept: - // Accept: text/plain; q=0.2 - if (string.IsNullOrEmpty(value) || (index == value.Length)) - { - return this.SupportsMultipleValues; - } - - var current = HeaderUtilities.GetNextNonEmptyOrWhitespaceIndex(value, index, this.SupportsMultipleValues, out var separatorFound); - - if (separatorFound && !this.SupportsMultipleValues) - { - return false; // leading separators not allowed if we don't support multiple values. - } - - if (current == value.Length) - { - if (this.SupportsMultipleValues) - { - index = current; - } - - return this.SupportsMultipleValues; - } - - var length = this.GetParsedValueLength(value, current, out var result); - - if (length == 0) - { - return false; - } - - current += length; - current = HeaderUtilities.GetNextNonEmptyOrWhitespaceIndex(value, current, this.SupportsMultipleValues, out separatorFound); - - // If we support multiple values and we've not reached the end of the string, then we must have a separator. - if ((separatorFound && !this.SupportsMultipleValues) || (!separatorFound && (current < value.Length))) - { - return false; - } - - index = current; - parsedValue = result; - return true; - } - - protected abstract int GetParsedValueLength(string value, int startIndex, out T parsedValue); - } -} diff --git a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/Internal/GenericHeaderParser.cs b/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/Internal/GenericHeaderParser.cs deleted file mode 100644 index 37824a0d021..00000000000 --- a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/Internal/GenericHeaderParser.cs +++ /dev/null @@ -1,39 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -using System; - -namespace OpenTelemetry.Instrumentation.AspNet -{ - // Adoptation of code from https://github.com/aspnet/HttpAbstractions/blob/07d115400e4f8c7a66ba239f230805f03a14ee3d/src/Microsoft.Net.Http.Headers/GenericHeaderParser.cs - internal sealed class GenericHeaderParser : BaseHeaderParser - { - private readonly GetParsedValueLengthDelegate getParsedValueLength; - - internal GenericHeaderParser(bool supportsMultipleValues, GetParsedValueLengthDelegate getParsedValueLength) - : base(supportsMultipleValues) - { - this.getParsedValueLength = getParsedValueLength ?? throw new ArgumentNullException(nameof(getParsedValueLength)); - } - - internal delegate int GetParsedValueLengthDelegate(string value, int startIndex, out T parsedValue); - - protected override int GetParsedValueLength(string value, int startIndex, out T parsedValue) - { - return this.getParsedValueLength(value, startIndex, out parsedValue); - } - } -} diff --git a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/Internal/HeaderUtilities.cs b/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/Internal/HeaderUtilities.cs deleted file mode 100644 index 05644b3a8bc..00000000000 --- a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/Internal/HeaderUtilities.cs +++ /dev/null @@ -1,59 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -using System.Diagnostics.Contracts; - -namespace OpenTelemetry.Instrumentation.AspNet -{ - // Adoption of the code from https://github.com/aspnet/HttpAbstractions/blob/07d115400e4f8c7a66ba239f230805f03a14ee3d/src/Microsoft.Net.Http.Headers/HeaderUtilities.cs - internal static class HeaderUtilities - { - internal static int GetNextNonEmptyOrWhitespaceIndex( - string input, - int startIndex, - bool skipEmptyValues, - out bool separatorFound) - { - Contract.Requires(input != null); - Contract.Requires(startIndex <= input.Length); // it's OK if index == value.Length. - - separatorFound = false; - var current = startIndex + HttpRuleParser.GetWhitespaceLength(input, startIndex); - - if ((current == input.Length) || (input[current] != ',')) - { - return current; - } - - // If we have a separator, skip the separator and all following whitespaces. If we support - // empty values, continue until the current character is neither a separator nor a whitespace. - separatorFound = true; - current++; // skip delimiter. - current += HttpRuleParser.GetWhitespaceLength(input, current); - - if (skipEmptyValues) - { - while ((current < input.Length) && (input[current] == ',')) - { - current++; // skip delimiter. - current += HttpRuleParser.GetWhitespaceLength(input, current); - } - } - - return current; - } - } -} diff --git a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/Internal/HttpHeaderParser.cs b/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/Internal/HttpHeaderParser.cs deleted file mode 100644 index 05584edcd8d..00000000000 --- a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/Internal/HttpHeaderParser.cs +++ /dev/null @@ -1,40 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -namespace OpenTelemetry.Instrumentation.AspNet -{ - // Adoptation of code from https://github.com/aspnet/HttpAbstractions/blob/07d115400e4f8c7a66ba239f230805f03a14ee3d/src/Microsoft.Net.Http.Headers/HttpHeaderParser.cs - internal abstract class HttpHeaderParser - { - private bool supportsMultipleValues; - - protected HttpHeaderParser(bool supportsMultipleValues) - { - this.supportsMultipleValues = supportsMultipleValues; - } - - public bool SupportsMultipleValues - { - get { return this.supportsMultipleValues; } - } - - // If a parser supports multiple values, a call to ParseValue/TryParseValue should return a value for 'index' - // pointing to the next non-whitespace character after a delimiter. E.g. if called with a start index of 0 - // for string "value , second_value", then after the call completes, 'index' must point to 's', i.e. the first - // non-whitespace after the separator ','. - public abstract bool TryParseValue(string value, ref int index, out T parsedValue); - } -} diff --git a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/Internal/HttpRuleParser.cs b/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/Internal/HttpRuleParser.cs deleted file mode 100644 index bd3b86b30f6..00000000000 --- a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/Internal/HttpRuleParser.cs +++ /dev/null @@ -1,281 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -using System.Diagnostics.Contracts; - -namespace OpenTelemetry.Instrumentation.AspNet -{ - // Adoptation of code from https://github.com/aspnet/HttpAbstractions/blob/07d115400e4f8c7a66ba239f230805f03a14ee3d/src/Microsoft.Net.Http.Headers/HttpRuleParser.cs - internal static class HttpRuleParser - { - internal const char CR = '\r'; - internal const char LF = '\n'; - internal const char SP = ' '; - internal const char Tab = '\t'; - internal const int MaxInt64Digits = 19; - internal const int MaxInt32Digits = 10; - - private const int MaxNestedCount = 5; - private static readonly bool[] TokenChars = CreateTokenChars(); - - internal static bool IsTokenChar(char character) - { - // Must be between 'space' (32) and 'DEL' (127) - if (character > 127) - { - return false; - } - - return TokenChars[character]; - } - - internal static int GetTokenLength(string input, int startIndex) - { - Contract.Requires(input != null); - Contract.Ensures((Contract.Result() >= 0) && (Contract.Result() <= (input.Length - startIndex))); - - if (startIndex >= input.Length) - { - return 0; - } - - var current = startIndex; - - while (current < input.Length) - { - if (!IsTokenChar(input[current])) - { - return current - startIndex; - } - - current++; - } - - return input.Length - startIndex; - } - - internal static int GetWhitespaceLength(string input, int startIndex) - { - Contract.Requires(input != null); - Contract.Ensures((Contract.Result() >= 0) && (Contract.Result() <= (input.Length - startIndex))); - - if (startIndex >= input.Length) - { - return 0; - } - - var current = startIndex; - - char c; - while (current < input.Length) - { - c = input[current]; - - if ((c == SP) || (c == Tab)) - { - current++; - continue; - } - - if (c == CR) - { - // If we have a #13 char, it must be followed by #10 and then at least one SP or HT. - if ((current + 2 < input.Length) && (input[current + 1] == LF)) - { - char spaceOrTab = input[current + 2]; - if ((spaceOrTab == SP) || (spaceOrTab == Tab)) - { - current += 3; - continue; - } - } - } - - return current - startIndex; - } - - // All characters between startIndex and the end of the string are LWS characters. - return input.Length - startIndex; - } - - internal static HttpParseResult GetQuotedStringLength(string input, int startIndex, out int length) - { - var nestedCount = 0; - return GetExpressionLength(input, startIndex, '"', '"', false, ref nestedCount, out length); - } - - // quoted-pair = "\" CHAR - // CHAR = - internal static HttpParseResult GetQuotedPairLength(string input, int startIndex, out int length) - { - Contract.Requires(input != null); - Contract.Requires((startIndex >= 0) && (startIndex < input.Length)); - Contract.Ensures((Contract.ValueAtReturn(out _) >= 0) && - (Contract.ValueAtReturn(out _) <= (input.Length - startIndex))); - - length = 0; - - if (input[startIndex] != '\\') - { - return HttpParseResult.NotParsed; - } - - // Quoted-char has 2 characters. Check whether there are 2 chars left ('\' + char) - // If so, check whether the character is in the range 0-127. If not, it's an invalid value. - if ((startIndex + 2 > input.Length) || (input[startIndex + 1] > 127)) - { - return HttpParseResult.InvalidFormat; - } - - // We don't care what the char next to '\' is. - length = 2; - return HttpParseResult.Parsed; - } - - private static bool[] CreateTokenChars() - { - // token = 1* - // CTL = - var tokenChars = new bool[128]; // everything is false - - for (int i = 33; i < 127; i++) - { - // skip Space (32) & DEL (127) - tokenChars[i] = true; - } - - // remove separators: these are not valid token characters - tokenChars[(byte)'('] = false; - tokenChars[(byte)')'] = false; - tokenChars[(byte)'<'] = false; - tokenChars[(byte)'>'] = false; - tokenChars[(byte)'@'] = false; - tokenChars[(byte)','] = false; - tokenChars[(byte)';'] = false; - tokenChars[(byte)':'] = false; - tokenChars[(byte)'\\'] = false; - tokenChars[(byte)'"'] = false; - tokenChars[(byte)'/'] = false; - tokenChars[(byte)'['] = false; - tokenChars[(byte)']'] = false; - tokenChars[(byte)'?'] = false; - tokenChars[(byte)'='] = false; - tokenChars[(byte)'{'] = false; - tokenChars[(byte)'}'] = false; - - return tokenChars; - } - - // TEXT = - // LWS = [CRLF] 1*( SP | HT ) - // CTL = - // - // Since we don't really care about the content of a quoted string or comment, we're more tolerant and - // allow these characters. We only want to find the delimiters ('"' for quoted string and '(', ')' for comment). - // - // 'nestedCount': Comments can be nested. We allow a depth of up to 5 nested comments, i.e. something like - // "(((((comment)))))". If we wouldn't define a limit an attacker could send a comment with hundreds of nested - // comments, resulting in a stack overflow exception. In addition having more than 1 nested comment (if any) - // is unusual. - private static HttpParseResult GetExpressionLength( - string input, - int startIndex, - char openChar, - char closeChar, - bool supportsNesting, - ref int nestedCount, - out int length) - { - Contract.Requires(input != null); - Contract.Requires((startIndex >= 0) && (startIndex < input.Length)); - Contract.Ensures((Contract.Result() != HttpParseResult.Parsed) || - (Contract.ValueAtReturn(out _) > 0)); - - length = 0; - - if (input[startIndex] != openChar) - { - return HttpParseResult.NotParsed; - } - - var current = startIndex + 1; // Start parsing with the character next to the first open-char - while (current < input.Length) - { - // Only check whether we have a quoted char, if we have at least 3 characters left to read (i.e. - // quoted char + closing char). Otherwise the closing char may be considered part of the quoted char. - if ((current + 2 < input.Length) && - (GetQuotedPairLength(input, current, out var quotedPairLength) == HttpParseResult.Parsed)) - { - // We ignore invalid quoted-pairs. Invalid quoted-pairs may mean that it looked like a quoted pair, - // but we actually have a quoted-string: e.g. '\' followed by a char >127 - quoted-pair only - // allows ASCII chars after '\'; qdtext allows both '\' and >127 chars. - current += quotedPairLength; - continue; - } - - // If we support nested expressions and we find an open-char, then parse the nested expressions. - if (supportsNesting && (input[current] == openChar)) - { - nestedCount++; - try - { - // Check if we exceeded the number of nested calls. - if (nestedCount > MaxNestedCount) - { - return HttpParseResult.InvalidFormat; - } - - HttpParseResult nestedResult = GetExpressionLength(input, current, openChar, closeChar, supportsNesting, ref nestedCount, out var nestedLength); - - switch (nestedResult) - { - case HttpParseResult.Parsed: - current += nestedLength; // add the length of the nested expression and continue. - break; - - case HttpParseResult.NotParsed: - Contract.Assert(false, "'NotParsed' is unexpected: We started nested expression parsing, because we found the open-char. So either it's a valid nested expression or it has invalid format."); - break; - - case HttpParseResult.InvalidFormat: - // If the nested expression is invalid, we can't continue, so we fail with invalid format. - return HttpParseResult.InvalidFormat; - - default: - Contract.Assert(false, "Unknown enum result: " + nestedResult); - break; - } - } - finally - { - nestedCount--; - } - } - - if (input[current] == closeChar) - { - length = current - startIndex + 1; - return HttpParseResult.Parsed; - } - - current++; - } - - // We didn't see the final quote, therefore we have an invalid expression string. - return HttpParseResult.InvalidFormat; - } - } -} diff --git a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/Internal/NameValueHeaderValue.cs b/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/Internal/NameValueHeaderValue.cs deleted file mode 100644 index 5add297c6f4..00000000000 --- a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/Internal/NameValueHeaderValue.cs +++ /dev/null @@ -1,134 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -using System.Diagnostics.Contracts; - -namespace OpenTelemetry.Instrumentation.AspNet -{ - // Adoptation of code from https://github.com/aspnet/HttpAbstractions/blob/07d115400e4f8c7a66ba239f230805f03a14ee3d/src/Microsoft.Net.Http.Headers/NameValueHeaderValue.cs - - // According to the RFC, in places where a "parameter" is required, the value is mandatory - // (e.g. Media-Type, Accept). However, we don't introduce a dedicated type for it. So NameValueHeaderValue supports - // name-only values in addition to name/value pairs. - internal class NameValueHeaderValue - { - private static readonly HttpHeaderParser SingleValueParser - = new GenericHeaderParser(false, GetNameValueLength); - - private string name; - private string value; - - private NameValueHeaderValue() - { - // Used by the parser to create a new instance of this type. - } - - public string Name - { - get { return this.name; } - } - - public string Value - { - get { return this.value; } - } - - public static bool TryParse(string input, out NameValueHeaderValue parsedValue) - { - var index = 0; - return SingleValueParser.TryParseValue(input, ref index, out parsedValue); - } - - internal static int GetValueLength(string input, int startIndex) - { - Contract.Requires(input != null); - - if (startIndex >= input.Length) - { - return 0; - } - - var valueLength = HttpRuleParser.GetTokenLength(input, startIndex); - - if (valueLength == 0) - { - // A value can either be a token or a quoted string. Check if it is a quoted string. - if (HttpRuleParser.GetQuotedStringLength(input, startIndex, out valueLength) != HttpParseResult.Parsed) - { - // We have an invalid value. Reset the name and return. - return 0; - } - } - - return valueLength; - } - - private static int GetNameValueLength(string input, int startIndex, out NameValueHeaderValue parsedValue) - { - Contract.Requires(input != null); - Contract.Requires(startIndex >= 0); - - parsedValue = null; - - if (string.IsNullOrEmpty(input) || (startIndex >= input.Length)) - { - return 0; - } - - // Parse the name, i.e. in name/value string "=". Caller must remove - // leading whitespaces. - var nameLength = HttpRuleParser.GetTokenLength(input, startIndex); - - if (nameLength == 0) - { - return 0; - } - - var name = input.Substring(startIndex, nameLength); - var current = startIndex + nameLength; - current += HttpRuleParser.GetWhitespaceLength(input, current); - - // Parse the separator between name and value - if ((current == input.Length) || (input[current] != '=')) - { - // We only have a name and that's OK. Return. - parsedValue = new NameValueHeaderValue - { - name = name, - }; - current += HttpRuleParser.GetWhitespaceLength(input, current); // skip whitespaces - return current - startIndex; - } - - current++; // skip delimiter. - current += HttpRuleParser.GetWhitespaceLength(input, current); - - // Parse the value, i.e. in name/value string "=" - int valueLength = GetValueLength(input, current); - - // Value after the '=' may be empty - // Use parameterless ctor to avoid double-parsing of name and value, i.e. skip public ctor validation. - parsedValue = new NameValueHeaderValue - { - name = name, - value = input.Substring(current, valueLength), - }; - current += valueLength; - current += HttpRuleParser.GetWhitespaceLength(input, current); // skip whitespaces - return current - startIndex; - } - } -} diff --git a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.csproj b/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.csproj index 896fbed7bdd..3af39924213 100644 --- a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.csproj +++ b/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.csproj @@ -5,12 +5,17 @@ $(PackageTags);distributed-tracing;AspNet;MVC;WebAPI + + + + + - + diff --git a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/README.md b/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/README.md index f9e7a90cb9c..c2d88b999b0 100644 --- a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/README.md +++ b/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/README.md @@ -1,59 +1,136 @@ -# Telemetry correlation http module +# ASP.NET Telemetry HttpModule for OpenTelemetry [![NuGet](https://img.shields.io/nuget/v/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.svg)](https://www.nuget.org/packages/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/) -Telemetry correlation http module enables cross tier telemetry tracking. +The ASP.NET Telemetry HttpModule enables distributed tracing of incoming ASP.NET +requests using the OpenTelemetry API. ## Usage -1. Install NuGet for your app. -2. Enable diagnostics source listener using code below. Note, some telemetry - vendors like Azure Application Insights will enable it automatically. - - ``` csharp - public class NoopDiagnosticsListener : IObserver> +### Step 1: Install NuGet package + +If you are using the traditional `packages.config` reference style, a +`web.config` transform should run automatically and configure the +`TelemetryHttpModule` for you. If you are using the more modern PackageReference +style, this may be needed to be done manually. For more information, see: +[Migrate from packages.config to +PackageReference](https://docs.microsoft.com/nuget/consume-packages/migrate-packages-config-to-package-reference). + +To configure your `web.config` manually, add this: + +```xml + + + + + +``` + +### Step 2: Register a listener + +`TelemetryHttpModule` registers an +[ActivitySource](https://docs.microsoft.com/dotnet/api/system.diagnostics.activitysource) +with the name `OpenTelemetry.Instrumentation.AspNet.Telemetry`. By default, .NET +`ActivitySource` will not generate any `Activity` objects unless there is a +registered listener. + +To register a listener automatically using OpenTelemetry, please use the +[OpenTelemetry.Instrumentation.AspNet](https://www.nuget.org/packages/OpenTelemetry.Instrumentation.AspNet/) +NuGet package. + +To register a listener manually, use code such as the following: + +```csharp +using System.Diagnostics; +using System.Web; +using System.Web.Http; +using System.Web.Mvc; +using System.Web.Routing; +using OpenTelemetry.Instrumentation.AspNet; + +namespace Examples.AspNet +{ + public class WebApiApplication : HttpApplication { - public void OnCompleted() { } + private ActivityListener aspNetActivityListener; - public void OnError(Exception error) { } + protected void Application_Start() + { + this.aspNetActivityListener = new ActivityListener + { + ShouldListenTo = (activitySource) => + { + // Only listen to TelemetryHttpModule's ActivitySource. + return activitySource.Name == TelemetryHttpModule.AspNetSourceName; + }, + Sample = (ref ActivityCreationOptions options) => + { + // Sample everything created by TelemetryHttpModule's ActivitySource. + return ActivitySamplingResult.AllDataAndRecorded; + }, + }; + + ActivitySource.AddActivityListener(this.aspNetActivityListener); + + GlobalConfiguration.Configure(WebApiConfig.Register); + + AreaRegistration.RegisterAllAreas(); + RouteConfig.RegisterRoutes(RouteTable.Routes); + } - public void OnNext(KeyValuePair evnt) + protected void Application_End() { + this.aspNetActivityListener?.Dispose(); } } +} +``` - public class NoopSubscriber : IObserver - { - public void OnCompleted() { } +## Options - public void OnError(Exception error) { } +`TelemetryHttpModule` provides a static options property +(`TelemetryHttpModule.Options`) which can be used to configure the +`TelemetryHttpModule` and listen to events it fires. - public void OnNext(DiagnosticListener listener) - { - if (listener.Name == "OpenTelemetry.Instrumentation.AspNet.Telemetry") - { - listener.Subscribe(new NoopDiagnosticsListener()); - } - } - } - ``` +### TextMapPropagator -3. Double check that http module was registered in `web.config` for your app. +`TextMapPropagator` controls how trace context will be extracted from incoming +Http request messages. By default, [W3C Trace +Context](https://www.w3.org/TR/trace-context/) is enabled. -Once enabled - this http module will: +The OpenTelemetry API ships with a handful of [standard +implementations](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/src/OpenTelemetry.Api/Context/Propagation) +which may be used, or you can write your own by deriving from the +`TextMapPropagator` class. + +To add support for +[Baggage](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/baggage/api.md) +propagation in addition to W3C Trace Context, use: + +```csharp +TelemetryHttpModuleOptions.TextMapPropagator = new CompositeTextMapPropagator( + new TextMapPropagator[] + { + new TraceContextPropagator(), + new BaggagePropagator(), + }); +``` -- Reads correlation http headers -- Start/Stops Activity for the http request -- Ensure the Activity ambient state is transferred thru the IIS callbacks +Note: When using the `OpenTelemetry.Instrumentation.AspNet` +`TelemetryHttpModuleOptions.TextMapPropagator` is automatically initialized to +the SDK default propagator (`Propagators.DefaultTextMapPropagator`) which by +default supports W3C Trace Context & Baggage. -See http protocol [specifications][http-protocol-specification] for details. +### Events -This http module is used by Application Insights. See -[documentation][usage-in-ai-docs] and [code][usage-in-ai-code]. +`OnRequestStartedCallback`, `OnRequestStoppedCallback`, & `OnExceptionCallback` +are provided on `TelemetryHttpModuleOptions` and will be fired by the +`TelemetryHttpModule` as requests are processed. -[http-protocol-specification]: -https://github.com/dotnet/corefx/blob/master/src/System.Diagnostics.DiagnosticSource/src/HttpCorrelationProtocol.md -[usage-in-ai-docs]: -https://docs.microsoft.com/azure/application-insights/application-insights-correlation -[usage-in-ai-code]: -https://github.com/Microsoft/ApplicationInsights-dotnet-server +A typical use case for these events is to add information (tags, events, and/or +links) to the created `Activity` based on the request, response, and/or +exception event being fired. diff --git a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/TelemetryHttpModule.cs b/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/TelemetryHttpModule.cs index 953f047773a..64117bf6cea 100644 --- a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/TelemetryHttpModule.cs +++ b/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/TelemetryHttpModule.cs @@ -15,7 +15,7 @@ // using System; -using System.ComponentModel; +using System.Diagnostics; using System.Reflection; using System.Web; @@ -26,7 +26,15 @@ namespace OpenTelemetry.Instrumentation.AspNet /// public class TelemetryHttpModule : IHttpModule { - private const string BeginCalledFlag = "OpenTelemetry.Instrumentation.AspNet.BeginCalled"; + /// + /// OpenTelemetry.Instrumentation.AspNet name. + /// + public const string AspNetSourceName = "OpenTelemetry.Instrumentation.AspNet.Telemetry"; + + /// + /// for OpenTelemetry.Instrumentation.AspNet created objects. + /// + public const string AspNetActivityName = "Microsoft.AspNet.HttpReqIn"; // ServerVariable set only on rewritten HttpContext by URL Rewrite module. private const string URLRewriteRewrittenRequest = "IIS_WasUrlRewritten"; @@ -34,13 +42,12 @@ public class TelemetryHttpModule : IHttpModule // ServerVariable set on every request if URL module is registered in HttpModule pipeline. private const string URLRewriteModuleVersion = "IIS_UrlRewriteModule"; - private static readonly MethodInfo OnStepMethodInfo = typeof(HttpApplication).GetMethod("OnExecuteRequestStep"); + private static readonly MethodInfo OnExecuteRequestStepMethodInfo = typeof(HttpApplication).GetMethod("OnExecuteRequestStep"); /// - /// Gets or sets a value indicating whether TelemetryHttpModule should parse headers to get correlation ids. + /// Gets the applied to requests processed by the handler. /// - [EditorBrowsable(EditorBrowsableState.Never)] - public bool ParseHeaders { get; set; } = true; + public static TelemetryHttpModuleOptions Options { get; } = new TelemetryHttpModuleOptions(); /// public void Dispose() @@ -54,60 +61,36 @@ public void Init(HttpApplication context) context.EndRequest += this.Application_EndRequest; context.Error += this.Application_Error; - // OnExecuteRequestStep is availabile starting with 4.7.1 - // If this is executed in 4.7.1 runtime (regardless of targeted .NET version), - // we will use it to restore lost activity, otherwise keep PreRequestHandlerExecute - if (OnStepMethodInfo != null && HttpRuntime.UsingIntegratedPipeline) + if (HttpRuntime.UsingIntegratedPipeline && OnExecuteRequestStepMethodInfo != null) { + // OnExecuteRequestStep is availabile starting with 4.7.1 try { - OnStepMethodInfo.Invoke(context, new object[] { (Action)this.OnExecuteRequestStep }); + OnExecuteRequestStepMethodInfo.Invoke(context, new object[] { (Action)this.OnExecuteRequestStep }); } catch (Exception e) { AspNetTelemetryEventSource.Log.OnExecuteRequestStepInvokationError(e.Message); } } - else - { - context.PreRequestHandlerExecute += this.Application_PreRequestHandlerExecute; - } - } - - /// - /// Restores Activity before each pipeline step if it was lost. - /// - /// HttpContext instance. - /// Step to be executed. - internal void OnExecuteRequestStep(HttpContextBase context, Action step) - { - // Once we have public Activity.Current setter (https://github.com/dotnet/corefx/issues/29207) this method will be - // simplified to just assign Current if is was lost. - // In the mean time, we are creating child Activity to restore the context. We have to send - // event with this Activity to tracing system. It created a lot of issues for listeners as - // we may potentially have a lot of them for different stages. - // To reduce amount of events, we only care about ExecuteRequestHandler stage - restore activity here and - // stop/report it to tracing system in EndRequest. - if (context.CurrentNotification == RequestNotification.ExecuteRequestHandler && !context.IsPostNotification) - { - ActivityHelper.RestoreActivityIfNeeded(context.Items); - } - - step(); } private void Application_BeginRequest(object sender, EventArgs e) { - var context = ((HttpApplication)sender).Context; AspNetTelemetryEventSource.Log.TraceCallback("Application_BeginRequest"); - ActivityHelper.CreateRootActivity(context, this.ParseHeaders); - context.Items[BeginCalledFlag] = true; + ActivityHelper.StartAspNetActivity(Options.TextMapPropagator, ((HttpApplication)sender).Context, Options.OnRequestStartedCallback); } - private void Application_PreRequestHandlerExecute(object sender, EventArgs e) + private void OnExecuteRequestStep(HttpContextBase context, Action step) { - AspNetTelemetryEventSource.Log.TraceCallback("Application_PreRequestHandlerExecute"); - ActivityHelper.RestoreActivityIfNeeded(((HttpApplication)sender).Context.Items); + // Called only on 4.7.1+ runtimes + + if (context.CurrentNotification == RequestNotification.ExecuteRequestHandler && !context.IsPostNotification) + { + ActivityHelper.RestoreContextIfNeeded(context.ApplicationInstance.Context); + } + + step(); } private void Application_EndRequest(object sender, EventArgs e) @@ -117,9 +100,7 @@ private void Application_EndRequest(object sender, EventArgs e) var context = ((HttpApplication)sender).Context; - // EndRequest does it's best effort to notify that request has ended - // BeginRequest has never been called - if (!context.Items.Contains(BeginCalledFlag)) + if (!ActivityHelper.HasStarted(context, out Activity aspNetActivity)) { // Rewrite: In case of rewrite, a new request context is created, called the child request, and it goes through the entire IIS/ASP.NET integrated pipeline. // The child request can be mapped to any of the handlers configured in IIS, and it's execution is no different than it would be if it was received via the HTTP stack. @@ -135,13 +116,13 @@ private void Application_EndRequest(object sender, EventArgs e) else { // Activity has never been started - ActivityHelper.CreateRootActivity(context, this.ParseHeaders); + aspNetActivity = ActivityHelper.StartAspNetActivity(Options.TextMapPropagator, context, Options.OnRequestStartedCallback); } } if (trackActivity) { - ActivityHelper.StopAspNetActivity(context.Items); + ActivityHelper.StopAspNetActivity(Options.TextMapPropagator, aspNetActivity, context, Options.OnRequestStoppedCallback); } } @@ -154,12 +135,12 @@ private void Application_Error(object sender, EventArgs e) var exception = context.Error; if (exception != null) { - if (!context.Items.Contains(BeginCalledFlag)) + if (!ActivityHelper.HasStarted(context, out Activity aspNetActivity)) { - ActivityHelper.CreateRootActivity(context, this.ParseHeaders); + aspNetActivity = ActivityHelper.StartAspNetActivity(Options.TextMapPropagator, context, Options.OnRequestStartedCallback); } - ActivityHelper.WriteActivityException(context.Items, exception); + ActivityHelper.WriteActivityException(aspNetActivity, context, exception, Options.OnExceptionCallback); } } } diff --git a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/TelemetryHttpModuleOptions.cs b/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/TelemetryHttpModuleOptions.cs new file mode 100644 index 00000000000..258ac86de74 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/TelemetryHttpModuleOptions.cs @@ -0,0 +1,67 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Diagnostics; +using System.Web; +using OpenTelemetry.Context.Propagation; +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Instrumentation.AspNet +{ + /// + /// Stores options for the . + /// + public class TelemetryHttpModuleOptions + { + private TextMapPropagator textMapPropagator = new TraceContextPropagator(); + + internal TelemetryHttpModuleOptions() + { + } + + /// + /// Gets or sets the to use to + /// extract from incoming requests. + /// + public TextMapPropagator TextMapPropagator + { + get => this.textMapPropagator; + set + { + Guard.ThrowIfNull(value, nameof(value)); + + this.textMapPropagator = value; + } + } + + /// + /// Gets or sets a callback action to be fired when a request is started. + /// + public Action OnRequestStartedCallback { get; set; } + + /// + /// Gets or sets a callback action to be fired when a request is stopped. + /// + public Action OnRequestStoppedCallback { get; set; } + + /// + /// Gets or sets a callback action to be fired when an unhandled + /// exception is thrown processing a request. + /// + public Action OnExceptionCallback { get; set; } + } +} diff --git a/src/OpenTelemetry.Instrumentation.AspNet/.publicApi/net461/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Instrumentation.AspNet/.publicApi/net461/PublicAPI.Unshipped.txt index 569931e171a..35b5f87638f 100644 --- a/src/OpenTelemetry.Instrumentation.AspNet/.publicApi/net461/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Instrumentation.AspNet/.publicApi/net461/PublicAPI.Unshipped.txt @@ -4,5 +4,7 @@ OpenTelemetry.Instrumentation.AspNet.AspNetInstrumentationOptions.Enrich.get -> OpenTelemetry.Instrumentation.AspNet.AspNetInstrumentationOptions.Enrich.set -> void OpenTelemetry.Instrumentation.AspNet.AspNetInstrumentationOptions.Filter.get -> System.Func OpenTelemetry.Instrumentation.AspNet.AspNetInstrumentationOptions.Filter.set -> void +OpenTelemetry.Instrumentation.AspNet.AspNetInstrumentationOptions.RecordException.get -> bool +OpenTelemetry.Instrumentation.AspNet.AspNetInstrumentationOptions.RecordException.set -> void OpenTelemetry.Trace.TracerProviderBuilderExtensions static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddAspNetInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action configureAspNetInstrumentationOptions = null) -> OpenTelemetry.Trace.TracerProviderBuilder diff --git a/src/OpenTelemetry.Instrumentation.AspNet/AspNetInstrumentation.cs b/src/OpenTelemetry.Instrumentation.AspNet/AspNetInstrumentation.cs index 3360b08abea..2bd2a988be7 100644 --- a/src/OpenTelemetry.Instrumentation.AspNet/AspNetInstrumentation.cs +++ b/src/OpenTelemetry.Instrumentation.AspNet/AspNetInstrumentation.cs @@ -13,6 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // + using System; using OpenTelemetry.Instrumentation.AspNet.Implementation; @@ -21,11 +22,9 @@ namespace OpenTelemetry.Instrumentation.AspNet /// /// Asp.Net Requests instrumentation. /// - internal class AspNetInstrumentation : IDisposable + internal sealed class AspNetInstrumentation : IDisposable { - internal const string AspNetDiagnosticListenerName = "OpenTelemetry.Instrumentation.AspNet.Telemetry"; - - private readonly DiagnosticSourceSubscriber diagnosticSourceSubscriber; + private readonly HttpInListener httpInListener; /// /// Initializes a new instance of the class. @@ -33,17 +32,13 @@ internal class AspNetInstrumentation : IDisposable /// Configuration options for ASP.NET instrumentation. public AspNetInstrumentation(AspNetInstrumentationOptions options) { - this.diagnosticSourceSubscriber = new DiagnosticSourceSubscriber( - name => new HttpInListener(name, options), - listener => listener.Name == AspNetDiagnosticListenerName, - null); - this.diagnosticSourceSubscriber.Subscribe(); + this.httpInListener = new HttpInListener(options); } /// public void Dispose() { - this.diagnosticSourceSubscriber?.Dispose(); + this.httpInListener?.Dispose(); } } } diff --git a/src/OpenTelemetry.Instrumentation.AspNet/AspNetInstrumentationOptions.cs b/src/OpenTelemetry.Instrumentation.AspNet/AspNetInstrumentationOptions.cs index 613b3f922f7..4209ce23e05 100644 --- a/src/OpenTelemetry.Instrumentation.AspNet/AspNetInstrumentationOptions.cs +++ b/src/OpenTelemetry.Instrumentation.AspNet/AspNetInstrumentationOptions.cs @@ -26,11 +26,19 @@ namespace OpenTelemetry.Instrumentation.AspNet public class AspNetInstrumentationOptions { /// - /// Gets or sets a Filter function that determines whether or not to collect telemetry about requests on a per request basis. - /// The Filter gets the HttpContext, and should return a boolean. - /// If Filter returns true, the request is collected. - /// If Filter returns false or throw exception, the request is filtered out. + /// Gets or sets a filter callback function that determines on a per + /// request basis whether or not to collect telemetry. /// + /// + /// The filter callback receives the for the + /// current request and should return a boolean. + /// + /// If filter returns the request is + /// collected. + /// If filter returns or throws an + /// exception the request is filtered out (NOT collected). + /// + /// public Func Filter { get; set; } /// @@ -43,5 +51,13 @@ public class AspNetInstrumentationOptions /// The type of this object depends on the event, which is given by the above parameter. /// public Action Enrich { get; set; } + + /// + /// Gets or sets a value indicating whether the exception will be recorded as ActivityEvent or not. + /// + /// + /// See: . + /// + public bool RecordException { get; set; } } } diff --git a/src/OpenTelemetry.Instrumentation.AspNet/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.AspNet/CHANGELOG.md index 26c381d70a1..366345e667c 100644 --- a/src/OpenTelemetry.Instrumentation.AspNet/CHANGELOG.md +++ b/src/OpenTelemetry.Instrumentation.AspNet/CHANGELOG.md @@ -2,9 +2,30 @@ ## Unreleased +## 1.0.0-rc8 + +Released 2021-Oct-08 + * Removes .NET Framework 4.5.2, .NET 4.6 support. The minimum .NET Framework version supported is .NET 4.6.1. ([#2138](https://github.com/open-telemetry/opentelemetry-dotnet/issues/2138)) +* Replaced `http.path` tag on activity with `http.target`. + ([#2266](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2266)) + +* ASP.NET instrumentation now uses + [OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule](https://www.nuget.org/packages/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/) + instead of + [Microsoft.AspNet.TelemetryCorrelation](https://www.nuget.org/packages/Microsoft.AspNet.TelemetryCorrelation/) + to listen for incoming http requests to the process. Please see the (Step 2: + Modify + Web.config)[https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/src/OpenTelemetry.Instrumentation.AspNet#step-2-modify-webconfig] + README section for details on the new HttpModule definition required. + ([#2222](https://github.com/open-telemetry/opentelemetry-dotnet/issues/2222)) + +* Added `RecordException` option. Specify `true` to have unhandled exception + details automatically captured on spans. + ([#2256](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2256)) + ## 1.0.0-rc7 Released 2021-Jul-12 diff --git a/src/OpenTelemetry.Instrumentation.AspNet/Implementation/AspNetInstrumentationEventSource.cs b/src/OpenTelemetry.Instrumentation.AspNet/Implementation/AspNetInstrumentationEventSource.cs index c66228b74df..e7184746b22 100644 --- a/src/OpenTelemetry.Instrumentation.AspNet/Implementation/AspNetInstrumentationEventSource.cs +++ b/src/OpenTelemetry.Instrumentation.AspNet/Implementation/AspNetInstrumentationEventSource.cs @@ -24,50 +24,44 @@ namespace OpenTelemetry.Instrumentation.AspNet.Implementation /// EventSource events emitted from the project. /// [EventSource(Name = "OpenTelemetry-Instrumentation-AspNet")] - internal class AspNetInstrumentationEventSource : EventSource + internal sealed class AspNetInstrumentationEventSource : EventSource { public static AspNetInstrumentationEventSource Log = new AspNetInstrumentationEventSource(); [NonEvent] - public void RequestFilterException(Exception ex) + public void RequestFilterException(string operationName, Exception ex) { if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) { - this.RequestFilterException(ex.ToInvariantString()); + this.RequestFilterException(operationName, ex.ToInvariantString()); } } - [Event(1, Message = "Payload is NULL in event '{1}' from handler '{0}', span will not be recorded.", Level = EventLevel.Warning)] - public void NullPayload(string handlerName, string eventName) - { - this.WriteEvent(1, handlerName, eventName); - } - - [Event(2, Message = "Request is filtered out.", Level = EventLevel.Verbose)] - public void RequestIsFilteredOut(string eventName) + [NonEvent] + public void EnrichmentException(string eventName, Exception ex) { - this.WriteEvent(2, eventName); + if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) + { + this.EnrichmentException(eventName, ex.ToInvariantString()); + } } - [Event(3, Message = "InstrumentationFilter threw exception. Request will not be collected. Exception {0}.", Level = EventLevel.Error)] - public void RequestFilterException(string exception) + [Event(1, Message = "Request is filtered out and will not be collected. Operation='{0}'", Level = EventLevel.Verbose)] + public void RequestIsFilteredOut(string operationName) { - this.WriteEvent(3, exception); + this.WriteEvent(1, operationName); } - [NonEvent] - public void EnrichmentException(Exception ex) + [Event(2, Message = "Filter callback threw an exception. Request will not be collected. Operation='{0}': {1}", Level = EventLevel.Error)] + public void RequestFilterException(string operationName, string exception) { - if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) - { - this.EnrichmentException(ex.ToInvariantString()); - } + this.WriteEvent(2, operationName, exception); } - [Event(4, Message = "Enrichment threw exception. Exception {0}.", Level = EventLevel.Error)] - public void EnrichmentException(string exception) + [Event(3, Message = "Enrich callback threw an exception. Event='{0}': {1}", Level = EventLevel.Error)] + public void EnrichmentException(string eventName, string exception) { - this.WriteEvent(4, exception); + this.WriteEvent(3, eventName, exception); } } } diff --git a/src/OpenTelemetry.Instrumentation.AspNet/Implementation/HttpInListener.cs b/src/OpenTelemetry.Instrumentation.AspNet/Implementation/HttpInListener.cs index 07867a036c1..8bb28b9b45f 100644 --- a/src/OpenTelemetry.Instrumentation.AspNet/Implementation/HttpInListener.cs +++ b/src/OpenTelemetry.Instrumentation.AspNet/Implementation/HttpInListener.cs @@ -15,103 +15,68 @@ // using System; -using System.Collections.Generic; using System.Diagnostics; -using System.Reflection; using System.Web; using System.Web.Routing; using OpenTelemetry.Context.Propagation; +using OpenTelemetry.Internal; using OpenTelemetry.Trace; namespace OpenTelemetry.Instrumentation.AspNet.Implementation { - internal class HttpInListener : ListenerHandler + internal sealed class HttpInListener : IDisposable { - internal const string ActivityNameByHttpInListener = "ActivityCreatedByHttpInListener"; - internal const string ActivityOperationName = "Microsoft.AspNet.HttpReqIn"; - internal static readonly AssemblyName AssemblyName = typeof(HttpInListener).Assembly.GetName(); - internal static readonly string ActivitySourceName = AssemblyName.Name; - internal static readonly Version Version = AssemblyName.Version; - internal static readonly ActivitySource ActivitySource = new ActivitySource(ActivitySourceName, Version.ToString()); - private static readonly Func> HttpRequestHeaderValuesGetter = (request, name) => request.Headers.GetValues(name); private readonly PropertyFetcher routeFetcher = new PropertyFetcher("Route"); private readonly PropertyFetcher routeTemplateFetcher = new PropertyFetcher("RouteTemplate"); private readonly AspNetInstrumentationOptions options; - public HttpInListener(string name, AspNetInstrumentationOptions options) - : base(name) + public HttpInListener(AspNetInstrumentationOptions options) { - this.options = options ?? throw new ArgumentNullException(nameof(options)); - } - - [System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "Activity is retrieved from Activity.Current later and disposed.")] - public override void OnStartActivity(Activity activity, object payload) - { - // The overall flow of what AspNet library does is as below: - // Activity.Start() - // DiagnosticSource.WriteEvent("Start", payload) - // DiagnosticSource.WriteEvent("Stop", payload) - // Activity.Stop() + Guard.ThrowIfNull(options, nameof(options)); - // This method is in the WriteEvent("Start", payload) path. - // By this time, samplers have already run and - // activity.IsAllDataRequested populated accordingly. + this.options = options; - if (Sdk.SuppressInstrumentation) - { - return; - } + TelemetryHttpModule.Options.TextMapPropagator = Propagators.DefaultTextMapPropagator; - // Ensure context extraction irrespective of sampling decision - var context = HttpContext.Current; - if (context == null) - { - AspNetInstrumentationEventSource.Log.NullPayload(nameof(HttpInListener), nameof(this.OnStartActivity)); - return; - } + TelemetryHttpModule.Options.OnRequestStartedCallback += this.OnStartActivity; + TelemetryHttpModule.Options.OnRequestStoppedCallback += this.OnStopActivity; + TelemetryHttpModule.Options.OnExceptionCallback += this.OnException; + } - var request = context.Request; - var requestValues = request.Unvalidated; - var textMapPropagator = Propagators.DefaultTextMapPropagator; + public void Dispose() + { + TelemetryHttpModule.Options.OnRequestStartedCallback -= this.OnStartActivity; + TelemetryHttpModule.Options.OnRequestStoppedCallback -= this.OnStopActivity; + TelemetryHttpModule.Options.OnExceptionCallback -= this.OnException; + } - if (!(textMapPropagator is TraceContextPropagator)) + /// + /// Gets the OpenTelemetry standard uri tag value for a span based on its request . + /// + /// . + /// Span uri value. + private static string GetUriTagValueFromRequestUri(Uri uri) + { + if (string.IsNullOrEmpty(uri.UserInfo)) { - var ctx = textMapPropagator.Extract(default, request, HttpRequestHeaderValuesGetter); - - if (ctx.ActivityContext.IsValid() - && ctx.ActivityContext != new ActivityContext(activity.TraceId, activity.ParentSpanId, activity.ActivityTraceFlags, activity.TraceStateString, true)) - { - // Create a new activity with its parent set from the extracted context. - // This makes the new activity as a "sibling" of the activity created by - // ASP.NET. - Activity newOne = new Activity(ActivityNameByHttpInListener); - newOne.SetParentId(ctx.ActivityContext.TraceId, ctx.ActivityContext.SpanId, ctx.ActivityContext.TraceFlags); - newOne.TraceStateString = ctx.ActivityContext.TraceState; - - // Starting the new activity make it the Activity.Current one. - newOne.Start(); - - // Both new activity and old one store the other activity - // inside them. This is required in the Stop step to - // correctly stop and restore Activity.Current. - newOne.SetCustomProperty("OTel.ActivityByAspNet", activity); - activity.SetCustomProperty("OTel.ActivityByHttpInListener", newOne); - - // Set IsAllDataRequested to false for the activity created by the framework to only export the sibling activity and not the framework activity - activity.IsAllDataRequested = false; - activity = newOne; - } - - if (ctx.Baggage != default) - { - Baggage.Current = ctx.Baggage; - } + return uri.ToString(); } + return string.Concat(uri.Scheme, Uri.SchemeDelimiter, uri.Authority, uri.PathAndQuery, uri.Fragment); + } + + private void OnStartActivity(Activity activity, HttpContext context) + { if (activity.IsAllDataRequested) { try { + // todo: Ideally we would also check + // Sdk.SuppressInstrumentation here to prevent tagging a + // span that will not be collected but we can't do that + // without an SDK reference. Need the spec to come around on + // this. + if (this.options.Filter?.Invoke(context) == false) { AspNetInstrumentationEventSource.Log.RequestIsFilteredOut(activity.OperationName); @@ -122,14 +87,14 @@ public override void OnStartActivity(Activity activity, object payload) } catch (Exception ex) { - AspNetInstrumentationEventSource.Log.RequestFilterException(ex); + AspNetInstrumentationEventSource.Log.RequestFilterException(activity.OperationName, ex); activity.IsAllDataRequested = false; activity.ActivityTraceFlags &= ~ActivityTraceFlags.Recorded; return; } - ActivityInstrumentationHelper.SetActivitySourceProperty(activity, ActivitySource); - ActivityInstrumentationHelper.SetKindProperty(activity, ActivityKind.Server); + var request = context.Request; + var requestValues = request.Unvalidated; // see the spec https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/semantic_conventions/http.md var path = requestValues.Path; @@ -145,7 +110,7 @@ public override void OnStartActivity(Activity activity, object payload) } activity.SetTag(SemanticConventions.AttributeHttpMethod, request.HttpMethod); - activity.SetTag(SpanAttributeConstants.HttpPathKey, path); + activity.SetTag(SemanticConventions.AttributeHttpTarget, path); activity.SetTag(SemanticConventions.AttributeHttpUserAgent, request.UserAgent); activity.SetTag(SemanticConventions.AttributeHttpUrl, GetUriTagValueFromRequestUri(request.Url)); @@ -155,52 +120,22 @@ public override void OnStartActivity(Activity activity, object payload) } catch (Exception ex) { - AspNetInstrumentationEventSource.Log.EnrichmentException(ex); + AspNetInstrumentationEventSource.Log.EnrichmentException("OnStartActivity", ex); } } } - public override void OnStopActivity(Activity activity, object payload) + private void OnStopActivity(Activity activity, HttpContext context) { - Activity activityToEnrich = activity; - Activity createdActivity = null; - - var textMapPropagator = Propagators.DefaultTextMapPropagator; - bool isCustomPropagator = !(textMapPropagator is TraceContextPropagator); - - if (isCustomPropagator) - { - // If using custom context propagator, then the activity here - // could be either the one from Asp.Net, or the one - // this instrumentation created in Start. - // This is because Asp.Net, under certain circumstances, restores Activity.Current - // to its own activity. - if (activity.OperationName.Equals(ActivityOperationName, StringComparison.Ordinal)) - { - // This block is hit if Asp.Net did restore Current to its own activity, - // and we need to retrieve the one created by HttpInListener, - // or an additional activity was never created. - createdActivity = (Activity)activity.GetCustomProperty("OTel.ActivityByHttpInListener"); - activityToEnrich = createdActivity ?? activity; - } - } - - if (activityToEnrich.IsAllDataRequested) + if (activity.IsAllDataRequested) { - var context = HttpContext.Current; - if (context == null) - { - AspNetInstrumentationEventSource.Log.NullPayload(nameof(HttpInListener), nameof(this.OnStopActivity)); - return; - } - var response = context.Response; - activityToEnrich.SetTag(SemanticConventions.AttributeHttpStatusCode, response.StatusCode); + activity.SetTag(SemanticConventions.AttributeHttpStatusCode, response.StatusCode); - if (activityToEnrich.GetStatus().StatusCode == StatusCode.Unset) + if (activity.GetStatus().StatusCode == StatusCode.Unset) { - activityToEnrich.SetStatus(SpanHelper.ResolveSpanStatusForHttpStatusCode(response.StatusCode)); + activity.SetStatus(SpanHelper.ResolveSpanStatusForHttpStatusCode(response.StatusCode)); } var routeData = context.Request.RequestContext.RouteData; @@ -227,58 +162,41 @@ public override void OnStopActivity(Activity activity, object payload) if (!string.IsNullOrEmpty(template)) { // Override the name that was previously set to the path part of URL. - activityToEnrich.DisplayName = template; - activityToEnrich.SetTag(SemanticConventions.AttributeHttpRoute, template); + activity.DisplayName = template; + activity.SetTag(SemanticConventions.AttributeHttpRoute, template); } try { - this.options.Enrich?.Invoke(activityToEnrich, "OnStopActivity", response); + this.options.Enrich?.Invoke(activity, "OnStopActivity", response); } catch (Exception ex) { - AspNetInstrumentationEventSource.Log.EnrichmentException(ex); + AspNetInstrumentationEventSource.Log.EnrichmentException("OnStopActivity", ex); } } + } - if (isCustomPropagator) + private void OnException(Activity activity, HttpContext context, Exception exception) + { + if (activity.IsAllDataRequested) { - if (activity.OperationName.Equals(ActivityNameByHttpInListener, StringComparison.Ordinal)) + if (this.options.RecordException) { - // If instrumentation started a new Activity, it must - // be stopped here. - activity.Stop(); + activity.RecordException(exception); + } + + activity.SetStatus(Status.Error.WithDescription(exception.Message)); - // Restore the original activity as Current. - var activityByAspNet = (Activity)activity.GetCustomProperty("OTel.ActivityByAspNet"); - Activity.Current = activityByAspNet; + try + { + this.options.Enrich?.Invoke(activity, "OnException", exception); } - else if (createdActivity != null) + catch (Exception ex) { - // This block is hit if Asp.Net did restore Current to its own activity, - // then we need to retrieve the one created by HttpInListener - // and stop it. - createdActivity.Stop(); - - // Restore current back to the one created by Asp.Net - Activity.Current = activity; + AspNetInstrumentationEventSource.Log.EnrichmentException("OnException", ex); } } } - - /// - /// Gets the OpenTelemetry standard uri tag value for a span based on its request . - /// - /// . - /// Span uri value. - private static string GetUriTagValueFromRequestUri(Uri uri) - { - if (string.IsNullOrEmpty(uri.UserInfo)) - { - return uri.ToString(); - } - - return string.Concat(uri.Scheme, Uri.SchemeDelimiter, uri.Authority, uri.PathAndQuery, uri.Fragment); - } } } diff --git a/src/OpenTelemetry.Instrumentation.AspNet/OpenTelemetry.Instrumentation.AspNet.csproj b/src/OpenTelemetry.Instrumentation.AspNet/OpenTelemetry.Instrumentation.AspNet.csproj index 46790741b95..27a45714bda 100644 --- a/src/OpenTelemetry.Instrumentation.AspNet/OpenTelemetry.Instrumentation.AspNet.csproj +++ b/src/OpenTelemetry.Instrumentation.AspNet/OpenTelemetry.Instrumentation.AspNet.csproj @@ -3,7 +3,7 @@ net461 ASP.NET instrumentation for OpenTelemetry .NET $(PackageTags);distributed-tracing;AspNet;MVC;WebAPI - true + true @@ -11,7 +11,12 @@ - + + + + + + diff --git a/src/OpenTelemetry.Instrumentation.AspNet/README.md b/src/OpenTelemetry.Instrumentation.AspNet/README.md index 9a465608fbd..d9637ce611c 100644 --- a/src/OpenTelemetry.Instrumentation.AspNet/README.md +++ b/src/OpenTelemetry.Instrumentation.AspNet/README.md @@ -31,11 +31,11 @@ following shows changes required to your `Web.config` when using IIS web server. ```xml - + ``` @@ -143,6 +143,12 @@ general extensibility point to add additional properties to any activity. The `Enrich` option is specific to this instrumentation, and is provided to get access to `HttpRequest` and `HttpResponse`. +### RecordException + +This instrumentation automatically sets Activity Status to Error if an unhandled +exception is thrown. Additionally, `RecordException` feature may be turned on, +to store the exception to the Activity itself as ActivityEvent. + ## References * [ASP.NET](https://dotnet.microsoft.com/apps/aspnet) diff --git a/src/OpenTelemetry.Instrumentation.AspNet/TracerProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.AspNet/TracerProviderBuilderExtensions.cs index 372c7c38731..39f41ef969e 100644 --- a/src/OpenTelemetry.Instrumentation.AspNet/TracerProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.AspNet/TracerProviderBuilderExtensions.cs @@ -16,7 +16,7 @@ using System; using OpenTelemetry.Instrumentation.AspNet; -using OpenTelemetry.Instrumentation.AspNet.Implementation; +using OpenTelemetry.Internal; namespace OpenTelemetry.Trace { @@ -35,18 +35,13 @@ public static TracerProviderBuilder AddAspNetInstrumentation( this TracerProviderBuilder builder, Action configureAspNetInstrumentationOptions = null) { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } + Guard.ThrowIfNull(builder, nameof(builder)); var aspnetOptions = new AspNetInstrumentationOptions(); configureAspNetInstrumentationOptions?.Invoke(aspnetOptions); builder.AddInstrumentation(() => new AspNetInstrumentation(aspnetOptions)); - builder.AddSource(HttpInListener.ActivitySourceName); - builder.AddLegacySource("Microsoft.AspNet.HttpReqIn"); // for the activities created by AspNetCore - builder.AddLegacySource("ActivityCreatedByHttpInListener"); // for the sibling activities created by the instrumentation library + builder.AddSource(TelemetryHttpModule.AspNetSourceName); return builder; } diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/net5.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/net5.0/PublicAPI.Unshipped.txt index eecdfa07bb1..fb182712e2b 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/net5.0/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/net5.0/PublicAPI.Unshipped.txt @@ -8,5 +8,7 @@ OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.Filter OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.Filter.set -> void OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.RecordException.get -> bool OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.RecordException.set -> void +OpenTelemetry.Metrics.MeterProviderBuilderExtensions OpenTelemetry.Trace.TracerProviderBuilderExtensions +static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddAspNetCoreInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder builder) -> OpenTelemetry.Metrics.MeterProviderBuilder static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddAspNetCoreInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action configureAspNetCoreInstrumentationOptions = null) -> OpenTelemetry.Trace.TracerProviderBuilder diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/net6.0/PublicAPI.Shipped.txt b/src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/net6.0/PublicAPI.Shipped.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/net6.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/net6.0/PublicAPI.Unshipped.txt new file mode 100644 index 00000000000..87d29939cca --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/net6.0/PublicAPI.Unshipped.txt @@ -0,0 +1,14 @@ +OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions +OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.AspNetCoreInstrumentationOptions() -> void +OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.EnableGrpcAspNetCoreSupport.get -> bool +OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.EnableGrpcAspNetCoreSupport.set -> void +OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.Enrich.get -> System.Action +OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.Enrich.set -> void +OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.Filter.get -> System.Func +OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.Filter.set -> void +OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.RecordException.get -> bool +OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.RecordException.set -> void +OpenTelemetry.Metrics.MeterProviderBuilderExtensions +OpenTelemetry.Trace.TracerProviderBuilderExtensions +static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddAspNetCoreInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder builder) -> OpenTelemetry.Metrics.MeterProviderBuilder +static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddAspNetCoreInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action configureAspNetCoreInstrumentationOptions = null) -> OpenTelemetry.Trace.TracerProviderBuilder \ No newline at end of file diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/netcoreapp3.1/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/netcoreapp3.1/PublicAPI.Unshipped.txt index eecdfa07bb1..fb182712e2b 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/netcoreapp3.1/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/netcoreapp3.1/PublicAPI.Unshipped.txt @@ -8,5 +8,7 @@ OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.Filter OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.Filter.set -> void OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.RecordException.get -> bool OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.RecordException.set -> void +OpenTelemetry.Metrics.MeterProviderBuilderExtensions OpenTelemetry.Trace.TracerProviderBuilderExtensions +static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddAspNetCoreInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder builder) -> OpenTelemetry.Metrics.MeterProviderBuilder static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddAspNetCoreInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action configureAspNetCoreInstrumentationOptions = null) -> OpenTelemetry.Trace.TracerProviderBuilder diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt index efdff3874e8..87ab1614549 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt @@ -6,5 +6,7 @@ OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.Filter OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.Filter.set -> void OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.RecordException.get -> bool OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.RecordException.set -> void +OpenTelemetry.Metrics.MeterProviderBuilderExtensions OpenTelemetry.Trace.TracerProviderBuilderExtensions +static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddAspNetCoreInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder builder) -> OpenTelemetry.Metrics.MeterProviderBuilder static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddAspNetCoreInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action configureAspNetCoreInstrumentationOptions = null) -> OpenTelemetry.Trace.TracerProviderBuilder diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt index eecdfa07bb1..fb182712e2b 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/.publicApi/netstandard2.1/PublicAPI.Unshipped.txt @@ -8,5 +8,7 @@ OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.Filter OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.Filter.set -> void OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.RecordException.get -> bool OpenTelemetry.Instrumentation.AspNetCore.AspNetCoreInstrumentationOptions.RecordException.set -> void +OpenTelemetry.Metrics.MeterProviderBuilderExtensions OpenTelemetry.Trace.TracerProviderBuilderExtensions +static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddAspNetCoreInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder builder) -> OpenTelemetry.Metrics.MeterProviderBuilder static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddAspNetCoreInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action configureAspNetCoreInstrumentationOptions = null) -> OpenTelemetry.Trace.TracerProviderBuilder diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentation.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentation.cs index b3ccdc7be92..2f9022782d1 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentation.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentation.cs @@ -25,13 +25,9 @@ internal class AspNetCoreInstrumentation : IDisposable { private readonly DiagnosticSourceSubscriber diagnosticSourceSubscriber; - /// - /// Initializes a new instance of the class. - /// - /// Configuration options for ASP.NET Core instrumentation. - public AspNetCoreInstrumentation(AspNetCoreInstrumentationOptions options) + public AspNetCoreInstrumentation(HttpInListener httpInListener) { - this.diagnosticSourceSubscriber = new DiagnosticSourceSubscriber(new HttpInListener("Microsoft.AspNetCore", options), null); + this.diagnosticSourceSubscriber = new DiagnosticSourceSubscriber(httpInListener, null); this.diagnosticSourceSubscriber.Subscribe(); } diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationOptions.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationOptions.cs index 8937e193b63..7bd2f7c1e4d 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationOptions.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/AspNetCoreInstrumentationOptions.cs @@ -52,7 +52,7 @@ public class AspNetCoreInstrumentationOptions /// public bool RecordException { get; set; } -#if NETSTANDARD2_1 || NETCOREAPP3_1 || NET5_0 +#if NETSTANDARD2_1 || NETCOREAPP3_1 || NET5_0_OR_GREATER /// /// Gets or sets a value indicating whether RPC attributes are added to an Activity when using Grpc.AspNetCore. Default is true. /// diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.AspNetCore/CHANGELOG.md index e0fb6be07a1..10fb3463e61 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/CHANGELOG.md +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/CHANGELOG.md @@ -2,6 +2,13 @@ ## Unreleased +## 1.0.0-rc8 + +Released 2021-Oct-08 + +* Replaced `http.path` tag on activity with `http.target`. + ([#2266](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2266)) + ## 1.0.0-rc7 Released 2021-Jul-12 diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs index 8fa39bbfbb5..eb25f895cce 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInListener.cs @@ -19,11 +19,16 @@ using System.Diagnostics; using System.Linq; using System.Reflection; +#if !NETSTANDARD2_0 using System.Runtime.CompilerServices; +#endif using System.Text; using Microsoft.AspNetCore.Http; using OpenTelemetry.Context.Propagation; +using OpenTelemetry.Internal; +#if !NETSTANDARD2_0 using OpenTelemetry.Instrumentation.GrpcNetClient; +#endif using OpenTelemetry.Trace; namespace OpenTelemetry.Instrumentation.AspNetCore.Implementation @@ -35,6 +40,7 @@ internal class HttpInListener : ListenerHandler internal static readonly string ActivitySourceName = AssemblyName.Name; internal static readonly Version Version = AssemblyName.Version; internal static readonly ActivitySource ActivitySource = new ActivitySource(ActivitySourceName, Version.ToString()); + private const string DiagnosticSourceName = "Microsoft.AspNetCore"; private const string UnknownHostName = "UNKNOWN-HOST"; private static readonly Func> HttpRequestHeaderValuesGetter = (request, name) => request.Headers[name]; private readonly PropertyFetcher startContextFetcher = new PropertyFetcher("HttpContext"); @@ -43,14 +49,14 @@ internal class HttpInListener : ListenerHandler private readonly PropertyFetcher beforeActionActionDescriptorFetcher = new PropertyFetcher("actionDescriptor"); private readonly PropertyFetcher beforeActionAttributeRouteInfoFetcher = new PropertyFetcher("AttributeRouteInfo"); private readonly PropertyFetcher beforeActionTemplateFetcher = new PropertyFetcher("Template"); - private readonly bool hostingSupportsW3C; private readonly AspNetCoreInstrumentationOptions options; - public HttpInListener(string name, AspNetCoreInstrumentationOptions options) - : base(name) + public HttpInListener(AspNetCoreInstrumentationOptions options) + : base(DiagnosticSourceName) { - this.hostingSupportsW3C = typeof(HttpRequest).Assembly.GetName().Version.Major >= 3; - this.options = options ?? throw new ArgumentNullException(nameof(options)); + Guard.ThrowIfNull(options, nameof(options)); + + this.options = options; } [System.Diagnostics.CodeAnalysis.SuppressMessage("Reliability", "CA2000:Dispose objects before losing scope", Justification = "The objects should not be disposed.")] @@ -81,7 +87,7 @@ public override void OnStartActivity(Activity activity, object payload) // Ensure context extraction irrespective of sampling decision var request = context.Request; var textMapPropagator = Propagators.DefaultTextMapPropagator; - if (!this.hostingSupportsW3C || !(textMapPropagator is TraceContextPropagator)) + if (!(textMapPropagator is TraceContextPropagator)) { var ctx = textMapPropagator.Extract(default, request, HttpRequestHeaderValuesGetter); @@ -105,10 +111,7 @@ public override void OnStartActivity(Activity activity, object payload) activity = newOne; } - if (ctx.Baggage != default) - { - Baggage.Current = ctx.Baggage; - } + Baggage.Current = ctx.Baggage; } // enrich Activity from payload only if sampling decision @@ -151,7 +154,7 @@ public override void OnStartActivity(Activity activity, object payload) } activity.SetTag(SemanticConventions.AttributeHttpMethod, request.Method); - activity.SetTag(SpanAttributeConstants.HttpPathKey, path); + activity.SetTag(SemanticConventions.AttributeHttpTarget, path); activity.SetTag(SemanticConventions.AttributeHttpUrl, GetUri(request)); var userAgent = request.Headers["User-Agent"].FirstOrDefault(); @@ -186,17 +189,14 @@ public override void OnStopActivity(Activity activity, object payload) activity.SetTag(SemanticConventions.AttributeHttpStatusCode, response.StatusCode); -#if NETSTANDARD2_1 || NETCOREAPP3_1 || NET5_0 +#if !NETSTANDARD2_0 if (this.options.EnableGrpcAspNetCoreSupport && TryGetGrpcMethod(activity, out var grpcMethod)) { AddGrpcAttributes(activity, grpcMethod, context); } - else + else if (activity.GetStatus().StatusCode == StatusCode.Unset) { - if (activity.GetStatus().StatusCode == StatusCode.Unset) - { - activity.SetStatus(SpanHelper.ResolveSpanStatusForHttpStatusCode(response.StatusCode)); - } + activity.SetStatus(SpanHelper.ResolveSpanStatusForHttpStatusCode(response.StatusCode)); } #else if (activity.GetStatus().StatusCode == StatusCode.Unset) @@ -233,6 +233,12 @@ public override void OnStopActivity(Activity activity, object payload) // the one created by the instrumentation. // And retrieve it here, and set it to Current. } + + var textMapPropagator = Propagators.DefaultTextMapPropagator; + if (!(textMapPropagator is TraceContextPropagator)) + { + Baggage.Current = default; + } } public override void OnCustom(string name, Activity activity, object payload) @@ -327,7 +333,7 @@ private static string GetUri(HttpRequest request) return builder.ToString(); } -#if NETSTANDARD2_1 || NETCOREAPP3_1 || NET5_0 +#if !NETSTANDARD2_0 [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool TryGetGrpcMethod(Activity activity, out string grpcMethod) { diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInMetricsListener.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInMetricsListener.cs index 3856299dd2a..c8f605a7aa0 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInMetricsListener.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/Implementation/HttpInMetricsListener.cs @@ -27,22 +27,13 @@ internal class HttpInMetricsListener : ListenerHandler private readonly PropertyFetcher stopContextFetcher = new PropertyFetcher("HttpContext"); private readonly Meter meter; - private Counter httpServerRequestCount; + private Histogram httpServerDuration; public HttpInMetricsListener(string name, Meter meter) : base(name) { this.meter = meter; - - // TODO: - // In the future, this instrumentation should produce the http.server.duration metric which will likely be represented as a histogram. - // See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/semantic_conventions/http-metrics.md#http-server - // - // Histograms are not yet supported by the SDK. - // - // For now we produce a count metric called http.server.request_count just for demonstration purposes. - // This metric is not defined by the in the semantic conventions. - this.httpServerRequestCount = meter.CreateCounter("http.server.request_count", null, "The number of HTTP requests processed."); + this.httpServerDuration = meter.CreateHistogram("http.server.duration", "ms", "measures the duration of the inbound HTTP request"); } public override void OnStopActivity(Activity activity, object payload) @@ -72,7 +63,7 @@ public override void OnStopActivity(Activity activity, object payload) new KeyValuePair(SemanticConventions.AttributeHttpFlavor, context.Request.Protocol), }; - this.httpServerRequestCount.Add(1, tags); + this.httpServerDuration.Record(activity.Duration.TotalMilliseconds, tags); } } } diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/MeterProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/MeterProviderBuilderExtensions.cs index 8f32fea696b..50ed827437d 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/MeterProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/MeterProviderBuilderExtensions.cs @@ -14,8 +14,8 @@ // limitations under the License. // -using System; using OpenTelemetry.Instrumentation.AspNetCore; +using OpenTelemetry.Internal; namespace OpenTelemetry.Metrics { @@ -32,10 +32,7 @@ public static class MeterProviderBuilderExtensions public static MeterProviderBuilder AddAspNetCoreInstrumentation( this MeterProviderBuilder builder) { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } + Guard.ThrowIfNull(builder, nameof(builder)); // TODO: Implement an IDeferredMeterProviderBuilder @@ -46,7 +43,7 @@ public static MeterProviderBuilder AddAspNetCoreInstrumentation( // EnableGrpcAspNetCoreSupport - this instrumentation will also need to also handle gRPC requests var instrumentation = new AspNetCoreMetrics(); - builder.AddSource(AspNetCoreMetrics.InstrumentationName); + builder.AddMeter(AspNetCoreMetrics.InstrumentationName); return builder.AddInstrumentation(() => instrumentation); } } diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/OpenTelemetry.Instrumentation.AspNetCore.csproj b/src/OpenTelemetry.Instrumentation.AspNetCore/OpenTelemetry.Instrumentation.AspNetCore.csproj index 6fd27b06c5c..ef84689e041 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/OpenTelemetry.Instrumentation.AspNetCore.csproj +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/OpenTelemetry.Instrumentation.AspNetCore.csproj @@ -1,6 +1,6 @@  - netstandard2.0;netstandard2.1;netcoreapp3.1;net5.0 + netstandard2.0;netstandard2.1;netcoreapp3.1;net5.0;net6.0 ASP.NET Core instrumentation for OpenTelemetry .NET $(PackageTags);distributed-tracing;AspNetCore true @@ -10,6 +10,7 @@ + @@ -33,4 +34,8 @@ + + + + diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/README.md b/src/OpenTelemetry.Instrumentation.AspNetCore/README.md index d439ec06d16..91fab9f38b8 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/README.md +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/README.md @@ -43,11 +43,10 @@ using OpenTelemetry.Trace; public void ConfigureServices(IServiceCollection services) { - services.AddOpenTelemetryTracing( - (builder) => builder - .AddAspNetCoreInstrumentation() - .AddJaegerExporter() - ); + services.AddOpenTelemetryTracing((builder) => builder + .AddAspNetCoreInstrumentation() + .AddJaegerExporter() + ); } ``` @@ -65,19 +64,18 @@ method of you applications `Startup` class as shown below. ```csharp // Configure services.Configure(options => - { - options.Filter = (httpContext) => - { - // only collect telemetry about HTTP GET requests - return httpContext.Request.Method.Equals("GET"); - }; - }); - -services.AddOpenTelemetryTracing( - (builder) => builder - .AddAspNetCoreInstrumentation() - .AddJaegerExporter() - ); +{ + options.Filter = (httpContext) => + { + // only collect telemetry about HTTP GET requests + return httpContext.Request.Method.Equals("GET"); + }; +}); + +services.AddOpenTelemetryTracing((builder) => builder + .AddAspNetCoreInstrumentation() + .AddJaegerExporter() +); ``` ### Filter @@ -93,17 +91,14 @@ The following code snippet shows how to use `Filter` to only allow GET requests. ```csharp -services.AddOpenTelemetryTracing( - (builder) => builder - .AddAspNetCoreInstrumentation( - (options) => options.Filter = - (httpContext) => - { - // only collect telemetry about HTTP GET requests - return httpContext.Request.Method.Equals("GET"); - }) +services.AddOpenTelemetryTracing((builder) => builder + .AddAspNetCoreInstrumentation((options) => options.Filter = httpContext => + { + // only collect telemetry about HTTP GET requests + return httpContext.Request.Method.Equals("GET"); + }) .AddJaegerExporter() - ); +); ``` It is important to note that this `Filter` option is specific to this @@ -126,8 +121,7 @@ The following code snippet shows how to add additional tags using `Enrich`. ```csharp services.AddOpenTelemetryTracing((builder) => { - builder - .AddAspNetCoreInstrumentation((options) => options.Enrich + builder.AddAspNetCoreInstrumentation((options) => options.Enrich = (activity, eventName, rawObject) => { if (eventName.Equals("OnStartActivity")) @@ -155,10 +149,9 @@ get access to `HttpRequest` and `HttpResponse`. ### RecordException -This instrumentation automatically sets Activity Status to Error if the -Http StatusCode is >= 400. -Additionally, `RecordException` feature may be turned on, to store the exception -to the Activity itself as ActivityEvent. +This instrumentation automatically sets Activity Status to Error if an unhandled +exception is thrown. Additionally, `RecordException` feature may be turned on, +to store the exception to the Activity itself as ActivityEvent. ## References diff --git a/src/OpenTelemetry.Instrumentation.AspNetCore/TracerProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.AspNetCore/TracerProviderBuilderExtensions.cs index 8d48fa00a33..1df5ec14ac2 100644 --- a/src/OpenTelemetry.Instrumentation.AspNetCore/TracerProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.AspNetCore/TracerProviderBuilderExtensions.cs @@ -17,6 +17,7 @@ using System; using OpenTelemetry.Instrumentation.AspNetCore; using OpenTelemetry.Instrumentation.AspNetCore.Implementation; +using OpenTelemetry.Internal; namespace OpenTelemetry.Trace { @@ -35,10 +36,7 @@ public static TracerProviderBuilder AddAspNetCoreInstrumentation( this TracerProviderBuilder builder, Action configureAspNetCoreInstrumentationOptions = null) { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } + Guard.ThrowIfNull(builder, nameof(builder)); if (builder is IDeferredTracerProviderBuilder deferredTracerProviderBuilder) { @@ -51,13 +49,24 @@ public static TracerProviderBuilder AddAspNetCoreInstrumentation( return AddAspNetCoreInstrumentation(builder, new AspNetCoreInstrumentationOptions(), configureAspNetCoreInstrumentationOptions); } - private static TracerProviderBuilder AddAspNetCoreInstrumentation(TracerProviderBuilder builder, AspNetCoreInstrumentationOptions options, Action configure = null) + internal static TracerProviderBuilder AddAspNetCoreInstrumentation( + this TracerProviderBuilder builder, + AspNetCoreInstrumentation instrumentation) { - configure?.Invoke(options); - var instrumentation = new AspNetCoreInstrumentation(options); builder.AddSource(HttpInListener.ActivitySourceName); builder.AddLegacySource(HttpInListener.ActivityOperationName); // for the activities created by AspNetCore return builder.AddInstrumentation(() => instrumentation); } + + private static TracerProviderBuilder AddAspNetCoreInstrumentation( + TracerProviderBuilder builder, + AspNetCoreInstrumentationOptions options, + Action configure = null) + { + configure?.Invoke(options); + return AddAspNetCoreInstrumentation( + builder, + new AspNetCoreInstrumentation(new HttpInListener(options))); + } } } diff --git a/src/OpenTelemetry.Instrumentation.GrpcNetClient/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.GrpcNetClient/CHANGELOG.md index bb21a6c0221..368d6991b54 100644 --- a/src/OpenTelemetry.Instrumentation.GrpcNetClient/CHANGELOG.md +++ b/src/OpenTelemetry.Instrumentation.GrpcNetClient/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## 1.0.0-rc8 + +Released 2021-Oct-08 + ## 1.0.0-rc7 Released 2021-Jul-12 diff --git a/src/OpenTelemetry.Instrumentation.GrpcNetClient/Implementation/GrpcClientDiagnosticListener.cs b/src/OpenTelemetry.Instrumentation.GrpcNetClient/Implementation/GrpcClientDiagnosticListener.cs index 1faaff01537..fdb0266e5fe 100644 --- a/src/OpenTelemetry.Instrumentation.GrpcNetClient/Implementation/GrpcClientDiagnosticListener.cs +++ b/src/OpenTelemetry.Instrumentation.GrpcNetClient/Implementation/GrpcClientDiagnosticListener.cs @@ -24,7 +24,7 @@ namespace OpenTelemetry.Instrumentation.GrpcNetClient.Implementation { - internal class GrpcClientDiagnosticListener : ListenerHandler + internal sealed class GrpcClientDiagnosticListener : ListenerHandler { internal static readonly AssemblyName AssemblyName = typeof(GrpcClientDiagnosticListener).Assembly.GetName(); internal static readonly string ActivitySourceName = AssemblyName.Name; diff --git a/src/OpenTelemetry.Instrumentation.GrpcNetClient/OpenTelemetry.Instrumentation.GrpcNetClient.csproj b/src/OpenTelemetry.Instrumentation.GrpcNetClient/OpenTelemetry.Instrumentation.GrpcNetClient.csproj index b599e57b18f..e7c5b916221 100644 --- a/src/OpenTelemetry.Instrumentation.GrpcNetClient/OpenTelemetry.Instrumentation.GrpcNetClient.csproj +++ b/src/OpenTelemetry.Instrumentation.GrpcNetClient/OpenTelemetry.Instrumentation.GrpcNetClient.csproj @@ -8,6 +8,7 @@ + diff --git a/src/OpenTelemetry.Instrumentation.GrpcNetClient/TracerProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.GrpcNetClient/TracerProviderBuilderExtensions.cs index 1da31b89e96..9c4580a39ad 100644 --- a/src/OpenTelemetry.Instrumentation.GrpcNetClient/TracerProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.GrpcNetClient/TracerProviderBuilderExtensions.cs @@ -17,6 +17,7 @@ using System; using OpenTelemetry.Instrumentation.GrpcNetClient; using OpenTelemetry.Instrumentation.GrpcNetClient.Implementation; +using OpenTelemetry.Internal; namespace OpenTelemetry.Trace { @@ -36,10 +37,7 @@ public static TracerProviderBuilder AddGrpcClientInstrumentation( this TracerProviderBuilder builder, Action configure = null) { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } + Guard.ThrowIfNull(builder, nameof(builder)); var grpcOptions = new GrpcClientInstrumentationOptions(); configure?.Invoke(grpcOptions); diff --git a/src/OpenTelemetry.Instrumentation.Http/.publicApi/net461/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Instrumentation.Http/.publicApi/net461/PublicAPI.Unshipped.txt index 6474a2acaf0..b4630d8b7a4 100644 --- a/src/OpenTelemetry.Instrumentation.Http/.publicApi/net461/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Instrumentation.Http/.publicApi/net461/PublicAPI.Unshipped.txt @@ -18,5 +18,7 @@ OpenTelemetry.Instrumentation.Http.HttpWebRequestInstrumentationOptions.RecordEx OpenTelemetry.Instrumentation.Http.HttpWebRequestInstrumentationOptions.RecordException.set -> void OpenTelemetry.Instrumentation.Http.HttpWebRequestInstrumentationOptions.SetHttpFlavor.get -> bool OpenTelemetry.Instrumentation.Http.HttpWebRequestInstrumentationOptions.SetHttpFlavor.set -> void +OpenTelemetry.Metrics.MeterProviderBuilderExtensions OpenTelemetry.Trace.TracerProviderBuilderExtensions +static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddHttpClientInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder builder) -> OpenTelemetry.Metrics.MeterProviderBuilder static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddHttpClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action configureHttpWebRequestInstrumentationOptions = null) -> OpenTelemetry.Trace.TracerProviderBuilder diff --git a/src/OpenTelemetry.Instrumentation.Http/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Instrumentation.Http/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt index edbcb6a7be7..04b9e3c9f61 100644 --- a/src/OpenTelemetry.Instrumentation.Http/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry.Instrumentation.Http/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt @@ -8,5 +8,7 @@ OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.RecordExcept OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.RecordException.set -> void OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.SetHttpFlavor.get -> bool OpenTelemetry.Instrumentation.Http.HttpClientInstrumentationOptions.SetHttpFlavor.set -> void +OpenTelemetry.Metrics.MeterProviderBuilderExtensions OpenTelemetry.Trace.TracerProviderBuilderExtensions +static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddHttpClientInstrumentation(this OpenTelemetry.Metrics.MeterProviderBuilder builder) -> OpenTelemetry.Metrics.MeterProviderBuilder static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddHttpClientInstrumentation(this OpenTelemetry.Trace.TracerProviderBuilder builder, System.Action configureHttpClientInstrumentationOptions = null) -> OpenTelemetry.Trace.TracerProviderBuilder diff --git a/src/OpenTelemetry.Instrumentation.Http/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.Http/CHANGELOG.md index de6cd0e0736..4441a861cbd 100644 --- a/src/OpenTelemetry.Instrumentation.Http/CHANGELOG.md +++ b/src/OpenTelemetry.Instrumentation.Http/CHANGELOG.md @@ -2,9 +2,21 @@ ## Unreleased +* Fixed an issue with `Filter` and `Enrich` callbacks not firing under certain + conditions when gRPC is used + ([#2698](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2698)) + +## 1.0.0-rc8 + +Released 2021-Oct-08 + * Removes .NET Framework 4.5.2 support. The minimum .NET Framework version supported is .NET 4.6.1. ([#2138](https://github.com/open-telemetry/opentelemetry-dotnet/issues/2138)) +* `HttpClient` instances created before `AddHttpClientInstrumentation` is called + on .NET Framework will now also be instrumented + ([#2364](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2364)) + ## 1.0.0-rc7 Released 2021-Jul-12 diff --git a/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpHandlerDiagnosticListener.cs b/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpHandlerDiagnosticListener.cs index 579cec6f361..6168796f28a 100644 --- a/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpHandlerDiagnosticListener.cs +++ b/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpHandlerDiagnosticListener.cs @@ -28,7 +28,7 @@ namespace OpenTelemetry.Instrumentation.Http.Implementation { - internal class HttpHandlerDiagnosticListener : ListenerHandler + internal sealed class HttpHandlerDiagnosticListener : ListenerHandler { internal static readonly AssemblyName AssemblyName = typeof(HttpHandlerDiagnosticListener).Assembly.GetName(); internal static readonly string ActivitySourceName = AssemblyName.Name; diff --git a/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpWebRequestActivitySource.netfx.cs b/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpWebRequestActivitySource.netfx.cs index a0dcd27815d..b4e6d545cad 100644 --- a/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpWebRequestActivitySource.netfx.cs +++ b/src/OpenTelemetry.Instrumentation.Http/Implementation/HttpWebRequestActivitySource.netfx.cs @@ -291,9 +291,8 @@ private static void HookOrProcessResult(HttpWebRequest request) private static void ProcessResult(IAsyncResult asyncResult, AsyncCallback asyncCallback, Activity activity, object result, bool forceResponseCopy) { - // We could be executing on a different thread now so set the activity. - Debug.Assert(Activity.Current == null || Activity.Current == activity, "There was an unexpected active Activity on the result thread."); - if (Activity.Current == null) + // We could be executing on a different thread now so restore the activity if needed. + if (Activity.Current != activity) { Activity.Current = activity; } @@ -454,9 +453,68 @@ private static void PerformInjection() Hashtable originalTable = servicePointTableField.GetValue(null) as Hashtable; ServicePointHashtable newTable = new ServicePointHashtable(originalTable ?? new Hashtable()); + foreach (DictionaryEntry existingServicePoint in originalTable) + { + HookServicePoint(existingServicePoint.Value); + } + servicePointTableField.SetValue(null, newTable); } + private static void HookServicePoint(object value) + { + if (value is WeakReference weakRef + && weakRef.IsAlive + && weakRef.Target is ServicePoint servicePoint) + { + // Replace the ConnectionGroup hashtable inside this ServicePoint object, + // which allows us to intercept each new ConnectionGroup object added under + // this ServicePoint. + Hashtable originalTable = connectionGroupListField.GetValue(servicePoint) as Hashtable; + ConnectionGroupHashtable newTable = new ConnectionGroupHashtable(originalTable ?? new Hashtable()); + + foreach (DictionaryEntry existingConnectionGroup in originalTable) + { + HookConnectionGroup(existingConnectionGroup.Value); + } + + connectionGroupListField.SetValue(servicePoint, newTable); + } + } + + private static void HookConnectionGroup(object value) + { + if (connectionGroupType.IsInstanceOfType(value)) + { + // Replace the Connection arraylist inside this ConnectionGroup object, + // which allows us to intercept each new Connection object added under + // this ConnectionGroup. + ArrayList originalArrayList = connectionListField.GetValue(value) as ArrayList; + ConnectionArrayList newArrayList = new ConnectionArrayList(originalArrayList ?? new ArrayList()); + + foreach (object connection in originalArrayList) + { + HookConnection(connection); + } + + connectionListField.SetValue(value, newArrayList); + } + } + + private static void HookConnection(object value) + { + if (connectionType.IsInstanceOfType(value)) + { + // Replace the HttpWebRequest arraylist inside this Connection object, + // which allows us to intercept each new HttpWebRequest object added under + // this Connection. + ArrayList originalArrayList = writeListField.GetValue(value) as ArrayList; + HttpWebRequestArrayList newArrayList = new HttpWebRequestArrayList(originalArrayList ?? new ArrayList()); + + writeListField.SetValue(value, newArrayList); + } + } + private static Func CreateFieldGetter(string fieldName, BindingFlags flags) where TClass : class { @@ -676,20 +734,7 @@ public override object this[object key] get => base[key]; set { - if (value is WeakReference weakRef && weakRef.IsAlive) - { - if (weakRef.Target is ServicePoint servicePoint) - { - // Replace the ConnectionGroup hashtable inside this ServicePoint object, - // which allows us to intercept each new ConnectionGroup object added under - // this ServicePoint. - Hashtable originalTable = connectionGroupListField.GetValue(servicePoint) as Hashtable; - ConnectionGroupHashtable newTable = new ConnectionGroupHashtable(originalTable ?? new Hashtable()); - - connectionGroupListField.SetValue(servicePoint, newTable); - } - } - + HookServicePoint(value); base[key] = value; } } @@ -712,17 +757,7 @@ public override object this[object key] get => base[key]; set { - if (connectionGroupType.IsInstanceOfType(value)) - { - // Replace the Connection arraylist inside this ConnectionGroup object, - // which allows us to intercept each new Connection object added under - // this ConnectionGroup. - ArrayList originalArrayList = connectionListField.GetValue(value) as ArrayList; - ConnectionArrayList newArrayList = new ConnectionArrayList(originalArrayList ?? new ArrayList()); - - connectionListField.SetValue(value, newArrayList); - } - + HookConnectionGroup(value); base[key] = value; } } @@ -952,17 +987,7 @@ public ConnectionArrayList(ArrayList list) public override int Add(object value) { - if (connectionType.IsInstanceOfType(value)) - { - // Replace the HttpWebRequest arraylist inside this Connection object, - // which allows us to intercept each new HttpWebRequest object added under - // this Connection. - ArrayList originalArrayList = writeListField.GetValue(value) as ArrayList; - HttpWebRequestArrayList newArrayList = new HttpWebRequestArrayList(originalArrayList ?? new ArrayList()); - - writeListField.SetValue(value, newArrayList); - } - + HookConnection(value); return base.Add(value); } } diff --git a/src/OpenTelemetry.Instrumentation.Http/MeterProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.Http/MeterProviderBuilderExtensions.cs index f7c969e6ebd..e92ad676897 100644 --- a/src/OpenTelemetry.Instrumentation.Http/MeterProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.Http/MeterProviderBuilderExtensions.cs @@ -14,8 +14,8 @@ // limitations under the License. // -using System; using OpenTelemetry.Instrumentation.Http; +using OpenTelemetry.Internal; namespace OpenTelemetry.Metrics { @@ -32,10 +32,7 @@ public static class MeterProviderBuilderExtensions public static MeterProviderBuilder AddHttpClientInstrumentation( this MeterProviderBuilder builder) { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } + Guard.ThrowIfNull(builder, nameof(builder)); // TODO: Implement an IDeferredMeterProviderBuilder @@ -46,7 +43,7 @@ public static MeterProviderBuilder AddHttpClientInstrumentation( // RecordException - probably doesn't make sense for metric instrumentation var instrumentation = new HttpClientMetrics(); - builder.AddSource(HttpClientMetrics.InstrumentationName); + builder.AddMeter(HttpClientMetrics.InstrumentationName); return builder.AddInstrumentation(() => instrumentation); } } diff --git a/src/OpenTelemetry.Instrumentation.Http/OpenTelemetry.Instrumentation.Http.csproj b/src/OpenTelemetry.Instrumentation.Http/OpenTelemetry.Instrumentation.Http.csproj index 95ad43be5f7..55ab8e552f3 100644 --- a/src/OpenTelemetry.Instrumentation.Http/OpenTelemetry.Instrumentation.Http.csproj +++ b/src/OpenTelemetry.Instrumentation.Http/OpenTelemetry.Instrumentation.Http.csproj @@ -7,6 +7,10 @@ true + + + + diff --git a/src/OpenTelemetry.Instrumentation.Http/README.md b/src/OpenTelemetry.Instrumentation.Http/README.md index 33ff2acb0ef..302f631cb04 100644 --- a/src/OpenTelemetry.Instrumentation.Http/README.md +++ b/src/OpenTelemetry.Instrumentation.Http/README.md @@ -139,31 +139,31 @@ Example: using System.Net.Http; var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddHttpClientInstrumentation((options) => options.Enrich - = (activity, eventName, rawObject) => - { - if (eventName.Equals("OnStartActivity")) - { - if (rawObject is HttpWebRequest request) - { - activity.SetTag("requestVersion", request.Version); - } - } - else if (eventName.Equals("OnStopActivity")) - { - if (rawObject is HttpWebResponse response) - { - activity.SetTag("responseVersion", response.Version); - } - } - else if (eventName.Equals("OnException")) - { - if (rawObject is Exception exception) - { - activity.SetTag("stackTrace", exception.StackTrace); - } - } - }).Build(); + .AddHttpClientInstrumentation((options) => options.Enrich + = (activity, eventName, rawObject) => + { + if (eventName.Equals("OnStartActivity")) + { + if (rawObject is HttpRequestMessage request) + { + activity.SetTag("requestVersion", request.Version); + } + } + else if (eventName.Equals("OnStopActivity")) + { + if (rawObject is HttpResponseMessage response) + { + activity.SetTag("responseVersion", response.Version); + } + } + else if (eventName.Equals("OnException")) + { + if (rawObject is Exception exception) + { + activity.SetTag("stackTrace", exception.StackTrace); + } + } + }).Build(); ``` #### HttpWebRequestInstrumentationOptions @@ -183,31 +183,31 @@ Example: using System.Net; var tracerProvider = Sdk.CreateTracerProviderBuilder() - .AddHttpClientInstrumentation((options) => options.Enrich - = (activity, eventName, rawObject) => - { - if (eventName.Equals("OnStartActivity")) - { - if (rawObject is HttpWebRequest request) - { - activity.SetTag("requestVersion", request.ProtocolVersion); - } - } - else if (eventName.Equals("OnStopActivity")) - { - if (rawObject is HttpWebResponse response) - { - activity.SetTag("responseVersion", response.ProtocolVersion); - } - } - else if (eventName.Equals("OnException")) - { - if (rawObject is Exception exception) - { - activity.SetTag("stackTrace", exception.StackTrace); - } - } - }).Build(); + .AddHttpClientInstrumentation((options) => options.Enrich + = (activity, eventName, rawObject) => + { + if (eventName.Equals("OnStartActivity")) + { + if (rawObject is HttpWebRequest request) + { + activity.SetTag("requestVersion", request.ProtocolVersion); + } + } + else if (eventName.Equals("OnStopActivity")) + { + if (rawObject is HttpWebResponse response) + { + activity.SetTag("responseVersion", response.ProtocolVersion); + } + } + else if (eventName.Equals("OnException")) + { + if (rawObject is Exception exception) + { + activity.SetTag("stackTrace", exception.StackTrace); + } + } + }).Build(); ``` [Processor](../../docs/trace/extending-the-sdk/README.md#processor), diff --git a/src/OpenTelemetry.Instrumentation.Http/TracerProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.Http/TracerProviderBuilderExtensions.cs index 6aa9a00ac0c..feef1d353de 100644 --- a/src/OpenTelemetry.Instrumentation.Http/TracerProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.Http/TracerProviderBuilderExtensions.cs @@ -17,6 +17,9 @@ using System; using OpenTelemetry.Instrumentation.Http; using OpenTelemetry.Instrumentation.Http.Implementation; +#if !NETFRAMEWORK +using OpenTelemetry.Internal; +#endif namespace OpenTelemetry.Trace { @@ -58,10 +61,7 @@ public static TracerProviderBuilder AddHttpClientInstrumentation( this TracerProviderBuilder builder, Action configureHttpClientInstrumentationOptions = null) { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } + Guard.ThrowIfNull(builder, nameof(builder)); var httpClientOptions = new HttpClientInstrumentationOptions(); diff --git a/src/OpenTelemetry.Instrumentation.SqlClient/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.SqlClient/CHANGELOG.md index fc9eda4dfbc..f30545c29f1 100644 --- a/src/OpenTelemetry.Instrumentation.SqlClient/CHANGELOG.md +++ b/src/OpenTelemetry.Instrumentation.SqlClient/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## 1.0.0-rc8 + +Released 2021-Oct-08 + * Removes .NET Framework 4.5.2 support. The minimum .NET Framework version supported is .NET 4.6.1. ([#2138](https://github.com/open-telemetry/opentelemetry-dotnet/issues/2138)) diff --git a/src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlClientDiagnosticListener.cs b/src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlClientDiagnosticListener.cs index ce88d82ec03..bb67119ed1c 100644 --- a/src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlClientDiagnosticListener.cs +++ b/src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlClientDiagnosticListener.cs @@ -21,7 +21,7 @@ namespace OpenTelemetry.Instrumentation.SqlClient.Implementation { - internal class SqlClientDiagnosticListener : ListenerHandler + internal sealed class SqlClientDiagnosticListener : ListenerHandler { public const string SqlDataBeforeExecuteCommand = "System.Data.SqlClient.WriteCommandBefore"; public const string SqlMicrosoftBeforeExecuteCommand = "Microsoft.Data.SqlClient.WriteCommandBefore"; diff --git a/src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlEventSourceListener.netfx.cs b/src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlEventSourceListener.netfx.cs index aaf97663e68..9417df4798d 100644 --- a/src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlEventSourceListener.netfx.cs +++ b/src/OpenTelemetry.Instrumentation.SqlClient/Implementation/SqlEventSourceListener.netfx.cs @@ -38,7 +38,7 @@ namespace OpenTelemetry.Instrumentation.SqlClient.Implementation /// /// "Microsoft.Data.SqlClient.EventSource" doesn't have that issue. /// - internal class SqlEventSourceListener : EventListener + internal sealed class SqlEventSourceListener : EventListener { internal const string AdoNetEventSourceName = "Microsoft-AdoNet-SystemData"; internal const string MdsEventSourceName = "Microsoft.Data.SqlClient.EventSource"; @@ -75,12 +75,12 @@ protected override void OnEventSourceCreated(EventSource eventSource) if (eventSource?.Name.StartsWith(AdoNetEventSourceName, StringComparison.Ordinal) == true) { this.adoNetEventSource = eventSource; - this.EnableEvents(eventSource, EventLevel.Informational, (EventKeywords)1); + this.EnableEvents(eventSource, EventLevel.Informational, EventKeywords.All); } else if (eventSource?.Name.StartsWith(MdsEventSourceName, StringComparison.Ordinal) == true) { this.mdsEventSource = eventSource; - this.EnableEvents(eventSource, EventLevel.Informational, (EventKeywords)1); + this.EnableEvents(eventSource, EventLevel.Informational, EventKeywords.All); } base.OnEventSourceCreated(eventSource); diff --git a/src/OpenTelemetry.Instrumentation.SqlClient/OpenTelemetry.Instrumentation.SqlClient.csproj b/src/OpenTelemetry.Instrumentation.SqlClient/OpenTelemetry.Instrumentation.SqlClient.csproj index 0ade9df7361..662610993cd 100644 --- a/src/OpenTelemetry.Instrumentation.SqlClient/OpenTelemetry.Instrumentation.SqlClient.csproj +++ b/src/OpenTelemetry.Instrumentation.SqlClient/OpenTelemetry.Instrumentation.SqlClient.csproj @@ -7,6 +7,10 @@ true + + + + diff --git a/src/OpenTelemetry.Instrumentation.SqlClient/TracerProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.SqlClient/TracerProviderBuilderExtensions.cs index c7ecae29ede..de11906f615 100644 --- a/src/OpenTelemetry.Instrumentation.SqlClient/TracerProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.SqlClient/TracerProviderBuilderExtensions.cs @@ -17,6 +17,7 @@ using System; using OpenTelemetry.Instrumentation.SqlClient; using OpenTelemetry.Instrumentation.SqlClient.Implementation; +using OpenTelemetry.Internal; namespace OpenTelemetry.Trace { @@ -35,10 +36,7 @@ public static TracerProviderBuilder AddSqlClientInstrumentation( this TracerProviderBuilder builder, Action configureSqlClientInstrumentationOptions = null) { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } + Guard.ThrowIfNull(builder, nameof(builder)); var sqlOptions = new SqlClientInstrumentationOptions(); configureSqlClientInstrumentationOptions?.Invoke(sqlOptions); diff --git a/src/OpenTelemetry.Instrumentation.StackExchangeRedis/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/CHANGELOG.md index fb149110925..84e6023146d 100644 --- a/src/OpenTelemetry.Instrumentation.StackExchangeRedis/CHANGELOG.md +++ b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## 1.0.0-rc8 + +Released 2021-Oct-08 + * Adds SetVerboseDatabaseStatements option to allow setting more detailed database statement tag values. * Adds Enrich option to allow enriching activities from the source profiled command diff --git a/src/OpenTelemetry.Instrumentation.StackExchangeRedis/OpenTelemetry.Instrumentation.StackExchangeRedis.csproj b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/OpenTelemetry.Instrumentation.StackExchangeRedis.csproj index 9890827d021..c0f8d415e4e 100644 --- a/src/OpenTelemetry.Instrumentation.StackExchangeRedis/OpenTelemetry.Instrumentation.StackExchangeRedis.csproj +++ b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/OpenTelemetry.Instrumentation.StackExchangeRedis.csproj @@ -9,6 +9,7 @@ + diff --git a/src/OpenTelemetry.Instrumentation.StackExchangeRedis/StackExchangeRedisCallsInstrumentation.cs b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/StackExchangeRedisCallsInstrumentation.cs index 5a85e3d416a..81c2e790d6d 100644 --- a/src/OpenTelemetry.Instrumentation.StackExchangeRedis/StackExchangeRedisCallsInstrumentation.cs +++ b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/StackExchangeRedisCallsInstrumentation.cs @@ -20,6 +20,7 @@ using System.Diagnostics; using System.Threading; using OpenTelemetry.Instrumentation.StackExchangeRedis.Implementation; +using OpenTelemetry.Internal; using OpenTelemetry.Trace; using StackExchange.Redis; using StackExchange.Redis.Profiling; @@ -58,10 +59,7 @@ internal class StackExchangeRedisCallsInstrumentation : IDisposable /// Configuration options for redis instrumentation. public StackExchangeRedisCallsInstrumentation(IConnectionMultiplexer connection, StackExchangeRedisCallsInstrumentationOptions options) { - if (connection == null) - { - throw new ArgumentNullException(nameof(connection)); - } + Guard.ThrowIfNull(connection, nameof(connection)); this.options = options ?? new StackExchangeRedisCallsInstrumentationOptions(); diff --git a/src/OpenTelemetry.Instrumentation.StackExchangeRedis/StackExchangeRedisCallsInstrumentationOptions.cs b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/StackExchangeRedisCallsInstrumentationOptions.cs index 735bc172984..ee32c511f0e 100644 --- a/src/OpenTelemetry.Instrumentation.StackExchangeRedis/StackExchangeRedisCallsInstrumentationOptions.cs +++ b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/StackExchangeRedisCallsInstrumentationOptions.cs @@ -15,7 +15,6 @@ // using System; -using System.Data; using System.Diagnostics; using OpenTelemetry.Trace; using StackExchange.Redis.Profiling; diff --git a/src/OpenTelemetry.Instrumentation.StackExchangeRedis/TracerProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/TracerProviderBuilderExtensions.cs index 78d9d154d95..7814ed11671 100644 --- a/src/OpenTelemetry.Instrumentation.StackExchangeRedis/TracerProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.StackExchangeRedis/TracerProviderBuilderExtensions.cs @@ -16,6 +16,7 @@ using System; using OpenTelemetry.Instrumentation.StackExchangeRedis; +using OpenTelemetry.Internal; using StackExchange.Redis; namespace OpenTelemetry.Trace @@ -42,38 +43,35 @@ public static TracerProviderBuilder AddRedisInstrumentation( IConnectionMultiplexer connection = null, Action configure = null) { - if (builder == null) + Guard.ThrowIfNull(builder, nameof(builder)); + + if (builder is not IDeferredTracerProviderBuilder deferredTracerProviderBuilder) { - throw new ArgumentNullException(nameof(builder)); + if (connection == null) + { + throw new NotSupportedException($"StackExchange.Redis {nameof(IConnectionMultiplexer)} must be supplied when dependency injection is unavailable - to enable dependency injection use the OpenTelemetry.Extensions.Hosting package"); + } + + return AddRedisInstrumentation(builder, connection, new StackExchangeRedisCallsInstrumentationOptions(), configure); } - if (builder is IDeferredTracerProviderBuilder deferredTracerProviderBuilder) + return deferredTracerProviderBuilder.Configure((sp, builder) => { - return deferredTracerProviderBuilder.Configure((sp, builder) => + if (connection == null) { + connection = (IConnectionMultiplexer)sp.GetService(typeof(IConnectionMultiplexer)); if (connection == null) { - connection = (IConnectionMultiplexer)sp.GetService(typeof(IConnectionMultiplexer)); - if (connection == null) - { - throw new InvalidOperationException("StackExchange.Redis IConnectionMultiplexer could not be resolved through application IServiceProvider."); - } + throw new InvalidOperationException($"StackExchange.Redis {nameof(IConnectionMultiplexer)} could not be resolved through application {nameof(IServiceProvider)}"); } + } - AddRedisInstrumentation( - builder, - connection, - sp.GetOptions(), - configure); - }); - } - - if (connection == null) - { - throw new NotSupportedException("StackExchange.Redis IConnectionMultiplexer must be supplied when dependency injection is unavailable. To enable dependency injection use the OpenTelemetry.Extensions.Hosting package."); - } - - return AddRedisInstrumentation(builder, connection, new StackExchangeRedisCallsInstrumentationOptions(), configure); + AddRedisInstrumentation( + builder, + connection, + sp.GetOptions(), + configure); + }); } private static TracerProviderBuilder AddRedisInstrumentation( diff --git a/src/OpenTelemetry.Shared/.publicApi/net461/PublicAPI.Shipped.txt b/src/OpenTelemetry.Shared/.publicApi/net461/PublicAPI.Shipped.txt deleted file mode 100644 index 5f282702bb0..00000000000 --- a/src/OpenTelemetry.Shared/.publicApi/net461/PublicAPI.Shipped.txt +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/OpenTelemetry.Shared/.publicApi/net461/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Shared/.publicApi/net461/PublicAPI.Unshipped.txt deleted file mode 100644 index d5d40948982..00000000000 --- a/src/OpenTelemetry.Shared/.publicApi/net461/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,9 +0,0 @@ -OpenTelemetry.Shared.IPersistentBlob -OpenTelemetry.Shared.IPersistentBlob.Delete() -> void -OpenTelemetry.Shared.IPersistentBlob.Lease(int leasePeriodMilliseconds) -> OpenTelemetry.Shared.IPersistentBlob -OpenTelemetry.Shared.IPersistentBlob.Read() -> byte[] -OpenTelemetry.Shared.IPersistentBlob.Write(byte[] buffer, int leasePeriodMilliseconds = 0) -> OpenTelemetry.Shared.IPersistentBlob -OpenTelemetry.Shared.IPersistentStorage -OpenTelemetry.Shared.IPersistentStorage.CreateBlob(byte[] buffer, int leasePeriodMilliseconds = 0) -> OpenTelemetry.Shared.IPersistentBlob -OpenTelemetry.Shared.IPersistentStorage.GetBlob() -> OpenTelemetry.Shared.IPersistentBlob -OpenTelemetry.Shared.IPersistentStorage.GetBlobs() -> System.Collections.Generic.IEnumerable \ No newline at end of file diff --git a/src/OpenTelemetry.Shared/.publicApi/netstandard2.0/PublicAPI.Shipped.txt b/src/OpenTelemetry.Shared/.publicApi/netstandard2.0/PublicAPI.Shipped.txt deleted file mode 100644 index 5f282702bb0..00000000000 --- a/src/OpenTelemetry.Shared/.publicApi/netstandard2.0/PublicAPI.Shipped.txt +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/OpenTelemetry.Shared/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Shared/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt deleted file mode 100644 index d5d40948982..00000000000 --- a/src/OpenTelemetry.Shared/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,9 +0,0 @@ -OpenTelemetry.Shared.IPersistentBlob -OpenTelemetry.Shared.IPersistentBlob.Delete() -> void -OpenTelemetry.Shared.IPersistentBlob.Lease(int leasePeriodMilliseconds) -> OpenTelemetry.Shared.IPersistentBlob -OpenTelemetry.Shared.IPersistentBlob.Read() -> byte[] -OpenTelemetry.Shared.IPersistentBlob.Write(byte[] buffer, int leasePeriodMilliseconds = 0) -> OpenTelemetry.Shared.IPersistentBlob -OpenTelemetry.Shared.IPersistentStorage -OpenTelemetry.Shared.IPersistentStorage.CreateBlob(byte[] buffer, int leasePeriodMilliseconds = 0) -> OpenTelemetry.Shared.IPersistentBlob -OpenTelemetry.Shared.IPersistentStorage.GetBlob() -> OpenTelemetry.Shared.IPersistentBlob -OpenTelemetry.Shared.IPersistentStorage.GetBlobs() -> System.Collections.Generic.IEnumerable \ No newline at end of file diff --git a/src/OpenTelemetry.Shared/IPersistentBlob.cs b/src/OpenTelemetry.Shared/IPersistentBlob.cs deleted file mode 100644 index 6e45ebeeb53..00000000000 --- a/src/OpenTelemetry.Shared/IPersistentBlob.cs +++ /dev/null @@ -1,74 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -namespace OpenTelemetry.Shared -{ - /// - /// Represents a persistent blob. - /// - public interface IPersistentBlob - { - /// - /// Reads the content from the blob. - /// - /// - /// The content of the blob if the operation succeeded, otherwise null. - /// - /// - /// This function should never throw exception. - /// - byte[] Read(); - - /// - /// Writes the given content to the blob. - /// - /// - /// The content to be written. - /// - /// - /// The number of milliseconds to lease after the write operation finished. - /// - /// - /// The same blob if the operation succeeded, otherwise null. - /// - /// - /// This function should never throw exception. - /// - IPersistentBlob Write(byte[] buffer, int leasePeriodMilliseconds = 0); - - /// - /// Creates a lease on the blob. - /// - /// - /// The number of milliseconds to lease. - /// - /// - /// The same blob if the lease operation succeeded, otherwise null. - /// - /// - /// This function should never throw exception. - /// - IPersistentBlob Lease(int leasePeriodMilliseconds); - - /// - /// Attempts to delete the blob. - /// - /// - /// This function should never throw exception. - /// - void Delete(); - } -} diff --git a/src/OpenTelemetry.Shared/IPersistentStorage.cs b/src/OpenTelemetry.Shared/IPersistentStorage.cs deleted file mode 100644 index 6823dcffcc6..00000000000 --- a/src/OpenTelemetry.Shared/IPersistentStorage.cs +++ /dev/null @@ -1,65 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -using System.Collections.Generic; - -namespace OpenTelemetry.Shared -{ - /// - /// Persistent storage API. - /// - public interface IPersistentStorage - { - /// - /// Reads a sequence of blobs from storage. - /// - /// - /// Sequence of blobs from storage. - /// - /// - /// This function should never throw exception. - /// - IEnumerable GetBlobs(); - - /// - /// Attempts to get a blob from storage. - /// - /// - /// A blob if there is an available one, or null if there is no blob available. - /// - /// - /// This function should never throw exception. - /// - IPersistentBlob GetBlob(); - - /// - /// Creates a new blob with the provided data. - /// - /// - /// The content to be written. - /// - /// - /// The number of milliseconds to lease after the blob is created. - /// - /// - /// The created blob. - /// - /// - /// This function should never throw exception. - /// - IPersistentBlob CreateBlob(byte[] buffer, int leasePeriodMilliseconds = 0); - } -} diff --git a/src/OpenTelemetry.Shared/OpenTelemetry.Shared.csproj b/src/OpenTelemetry.Shared/OpenTelemetry.Shared.csproj deleted file mode 100644 index 0a109388cbe..00000000000 --- a/src/OpenTelemetry.Shared/OpenTelemetry.Shared.csproj +++ /dev/null @@ -1,13 +0,0 @@ - - - - netstandard2.0;net461 - Shared project for OpenTelemetry .NET - $(PackageTags);Shared - - - - $(NoWarn),1591 - - - diff --git a/src/OpenTelemetry.Shims.OpenTracing/CHANGELOG.md b/src/OpenTelemetry.Shims.OpenTracing/CHANGELOG.md index 7d50303ee14..f1ec0e694e5 100644 --- a/src/OpenTelemetry.Shims.OpenTracing/CHANGELOG.md +++ b/src/OpenTelemetry.Shims.OpenTracing/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## 1.0.0-rc8 + +Released 2021-Oct-08 + * Removes .NET Framework 4.5.2 support. The minimum .NET Framework version supported is .NET 4.6.1. ([#2138](https://github.com/open-telemetry/opentelemetry-dotnet/issues/2138)) diff --git a/src/OpenTelemetry.Shims.OpenTracing/OpenTelemetry.Shims.OpenTracing.csproj b/src/OpenTelemetry.Shims.OpenTracing/OpenTelemetry.Shims.OpenTracing.csproj index 669d7c68aa1..6736743f281 100644 --- a/src/OpenTelemetry.Shims.OpenTracing/OpenTelemetry.Shims.OpenTracing.csproj +++ b/src/OpenTelemetry.Shims.OpenTracing/OpenTelemetry.Shims.OpenTracing.csproj @@ -18,4 +18,8 @@ + + + + diff --git a/src/OpenTelemetry.Shims.OpenTracing/ScopeManagerShim.cs b/src/OpenTelemetry.Shims.OpenTracing/ScopeManagerShim.cs index 507e89a6e29..e184fc69c66 100644 --- a/src/OpenTelemetry.Shims.OpenTracing/ScopeManagerShim.cs +++ b/src/OpenTelemetry.Shims.OpenTracing/ScopeManagerShim.cs @@ -16,15 +16,18 @@ using System; using System.Runtime.CompilerServices; +#if DEBUG using System.Threading; -using global::OpenTracing; +#endif +using OpenTelemetry.Internal; using OpenTelemetry.Trace; +using OpenTracing; namespace OpenTelemetry.Shims.OpenTracing { internal sealed class ScopeManagerShim : IScopeManager { - private static readonly ConditionalWeakTable SpanScopeTable = new ConditionalWeakTable(); + private static readonly ConditionalWeakTable SpanScopeTable = new ConditionalWeakTable(); private readonly Tracer tracer; @@ -32,9 +35,11 @@ internal sealed class ScopeManagerShim : IScopeManager private int spanScopeTableCount; #endif - public ScopeManagerShim(Trace.Tracer tracer) + public ScopeManagerShim(Tracer tracer) { - this.tracer = tracer ?? throw new ArgumentNullException(nameof(tracer)); + Guard.ThrowIfNull(tracer, nameof(tracer)); + + this.tracer = tracer; } #if DEBUG @@ -42,7 +47,7 @@ public ScopeManagerShim(Trace.Tracer tracer) #endif /// - public global::OpenTracing.IScope Active + public IScope Active { get { @@ -62,12 +67,9 @@ public ScopeManagerShim(Trace.Tracer tracer) } /// - public global::OpenTracing.IScope Activate(ISpan span, bool finishSpanOnDispose) + public IScope Activate(ISpan span, bool finishSpanOnDispose) { - if (!(span is SpanShim shim)) - { - throw new ArgumentException("span is not a valid SpanShim object"); - } + var shim = Guard.ThrowIfNotOfType(span, nameof(span)); var scope = Tracer.WithSpan(shim.Span); @@ -93,7 +95,7 @@ public ScopeManagerShim(Trace.Tracer tracer) return instrumentation; } - private class ScopeInstrumentation : global::OpenTracing.IScope + private class ScopeInstrumentation : IScope { private readonly Action disposeAction; diff --git a/src/OpenTelemetry.Shims.OpenTracing/SpanBuilderShim.cs b/src/OpenTelemetry.Shims.OpenTracing/SpanBuilderShim.cs index 82c8b20f70d..6a27eefe635 100644 --- a/src/OpenTelemetry.Shims.OpenTracing/SpanBuilderShim.cs +++ b/src/OpenTelemetry.Shims.OpenTracing/SpanBuilderShim.cs @@ -17,8 +17,9 @@ using System; using System.Collections.Generic; using System.Diagnostics; -using global::OpenTracing; +using OpenTelemetry.Internal; using OpenTelemetry.Trace; +using OpenTracing; namespace OpenTelemetry.Shims.OpenTracing { @@ -32,7 +33,7 @@ internal sealed class SpanBuilderShim : ISpanBuilder /// /// The tracer. /// - private readonly Trace.Tracer tracer; + private readonly Tracer tracer; /// /// The span name. @@ -65,7 +66,7 @@ internal sealed class SpanBuilderShim : ISpanBuilder /// /// The parent as an SpanContext, if any. /// - private Trace.SpanContext parentSpanContext; + private SpanContext parentSpanContext; /// /// The explicit start time, if any. @@ -74,19 +75,22 @@ internal sealed class SpanBuilderShim : ISpanBuilder private bool ignoreActiveSpan; - private Trace.SpanKind spanKind; + private SpanKind spanKind; private bool error; - public SpanBuilderShim(Trace.Tracer tracer, string spanName, IList rootOperationNamesForActivityBasedAutoInstrumentations = null) + public SpanBuilderShim(Tracer tracer, string spanName, IList rootOperationNamesForActivityBasedAutoInstrumentations = null) { - this.tracer = tracer ?? throw new ArgumentNullException(nameof(tracer)); - this.spanName = spanName ?? throw new ArgumentNullException(nameof(spanName)); + Guard.ThrowIfNull(tracer, nameof(tracer)); + Guard.ThrowIfNull(spanName, nameof(spanName)); + + this.tracer = tracer; + this.spanName = spanName; this.ScopeManager = new ScopeManagerShim(this.tracer); this.rootOperationNamesForActivityBasedAutoInstrumentations = rootOperationNamesForActivityBasedAutoInstrumentations ?? this.rootOperationNamesForActivityBasedAutoInstrumentations; } - private global::OpenTracing.IScopeManager ScopeManager { get; } + private IScopeManager ScopeManager { get; } private bool ParentSet => this.parentSpan != null || this.parentSpanContext.IsValid; @@ -98,7 +102,7 @@ public ISpanBuilder AsChildOf(ISpanContext parent) return this; } - return this.AddReference(global::OpenTracing.References.ChildOf, parent); + return this.AddReference(References.ChildOf, parent); } /// @@ -126,10 +130,7 @@ public ISpanBuilder AddReference(string referenceType, ISpanContext referencedCo return this; } - if (referenceType is null) - { - throw new ArgumentNullException(nameof(referenceType)); - } + Guard.ThrowIfNull(referenceType, nameof(referenceType)); // TODO There is no relation between OpenTracing.References (referenceType) and OpenTelemetry Link var actualContext = GetOpenTelemetrySpanContext(referencedContext); @@ -140,7 +141,7 @@ public ISpanBuilder AddReference(string referenceType, ISpanContext referencedCo } else { - this.links.Add(new Trace.Link(actualContext)); + this.links.Add(new Link(actualContext)); } return this; @@ -191,7 +192,7 @@ public ISpan Start() if (this.error) { - span.SetStatus(Trace.Status.Error); + span.SetStatus(Status.Error); } return new SpanShim(span); @@ -222,11 +223,11 @@ public ISpanBuilder WithTag(string key, string value) { this.spanKind = value switch { - global::OpenTracing.Tag.Tags.SpanKindClient => Trace.SpanKind.Client, - global::OpenTracing.Tag.Tags.SpanKindServer => Trace.SpanKind.Server, - global::OpenTracing.Tag.Tags.SpanKindProducer => Trace.SpanKind.Producer, - global::OpenTracing.Tag.Tags.SpanKindConsumer => Trace.SpanKind.Consumer, - _ => Trace.SpanKind.Internal, + global::OpenTracing.Tag.Tags.SpanKindClient => SpanKind.Client, + global::OpenTracing.Tag.Tags.SpanKindServer => SpanKind.Server, + global::OpenTracing.Tag.Tags.SpanKindProducer => SpanKind.Producer, + global::OpenTracing.Tag.Tags.SpanKindConsumer => SpanKind.Consumer, + _ => SpanKind.Internal, }; } else if (global::OpenTracing.Tag.Tags.Error.Key.Equals(key, StringComparison.Ordinal) && bool.TryParse(value, out var booleanValue)) @@ -278,10 +279,7 @@ public ISpanBuilder WithTag(string key, double value) /// public ISpanBuilder WithTag(global::OpenTracing.Tag.BooleanTag tag, bool value) { - if (tag == null || tag.Key == null) - { - throw new ArgumentNullException(nameof(tag)); - } + Guard.ThrowIfNull(tag?.Key, $"{nameof(tag)}?.{nameof(tag.Key)}"); return this.WithTag(tag.Key, value); } @@ -289,10 +287,7 @@ public ISpanBuilder WithTag(global::OpenTracing.Tag.BooleanTag tag, bool value) /// public ISpanBuilder WithTag(global::OpenTracing.Tag.IntOrStringTag tag, string value) { - if (tag == null || tag.Key == null) - { - throw new ArgumentNullException(nameof(tag)); - } + Guard.ThrowIfNull(tag?.Key, $"{nameof(tag)}?.{nameof(tag.Key)}"); if (int.TryParse(value, out var result)) { @@ -305,10 +300,7 @@ public ISpanBuilder WithTag(global::OpenTracing.Tag.IntOrStringTag tag, string v /// public ISpanBuilder WithTag(global::OpenTracing.Tag.IntTag tag, int value) { - if (tag == null || tag.Key == null) - { - throw new ArgumentNullException(nameof(tag)); - } + Guard.ThrowIfNull(tag?.Key, $"{nameof(tag)}?.{nameof(tag.Key)}"); return this.WithTag(tag.Key, value); } @@ -316,10 +308,7 @@ public ISpanBuilder WithTag(global::OpenTracing.Tag.IntTag tag, int value) /// public ISpanBuilder WithTag(global::OpenTracing.Tag.StringTag tag, string value) { - if (tag == null || tag.Key == null) - { - throw new ArgumentNullException(nameof(tag)); - } + Guard.ThrowIfNull(tag?.Key, $"{nameof(tag)}?.{nameof(tag.Key)}"); return this.WithTag(tag.Key, value); } @@ -332,10 +321,7 @@ public ISpanBuilder WithTag(global::OpenTracing.Tag.StringTag tag, string value) /// span is not a valid SpanShim object. private static TelemetrySpan GetOpenTelemetrySpan(ISpan span) { - if (!(span is SpanShim shim)) - { - throw new ArgumentException("span is not a valid SpanShim object"); - } + var shim = Guard.ThrowIfNotOfType(span, nameof(span)); return shim.Span; } @@ -346,12 +332,9 @@ private static TelemetrySpan GetOpenTelemetrySpan(ISpan span) /// The span context. /// the OpenTelemetry SpanContext. /// context is not a valid SpanContextShim object. - private static Trace.SpanContext GetOpenTelemetrySpanContext(ISpanContext spanContext) + private static SpanContext GetOpenTelemetrySpanContext(ISpanContext spanContext) { - if (!(spanContext is SpanContextShim shim)) - { - throw new ArgumentException("context is not a valid SpanContextShim object"); - } + var shim = Guard.ThrowIfNotOfType(spanContext, nameof(spanContext)); return shim.SpanContext; } diff --git a/src/OpenTelemetry.Shims.OpenTracing/SpanContextShim.cs b/src/OpenTelemetry.Shims.OpenTracing/SpanContextShim.cs index 4a91597b017..48e7c3dc28b 100644 --- a/src/OpenTelemetry.Shims.OpenTracing/SpanContextShim.cs +++ b/src/OpenTelemetry.Shims.OpenTracing/SpanContextShim.cs @@ -16,7 +16,7 @@ using System; using System.Collections.Generic; -using global::OpenTracing; +using OpenTracing; namespace OpenTelemetry.Shims.OpenTracing { @@ -26,7 +26,7 @@ public SpanContextShim(in Trace.SpanContext spanContext) { if (!spanContext.IsValid) { - throw new ArgumentException(nameof(spanContext)); + throw new ArgumentException($"Invalid '{nameof(Trace.SpanContext)}'", nameof(spanContext)); } this.SpanContext = spanContext; diff --git a/src/OpenTelemetry.Shims.OpenTracing/SpanShim.cs b/src/OpenTelemetry.Shims.OpenTracing/SpanShim.cs index f046fe22416..56aa2738d01 100644 --- a/src/OpenTelemetry.Shims.OpenTracing/SpanShim.cs +++ b/src/OpenTelemetry.Shims.OpenTracing/SpanShim.cs @@ -17,12 +17,13 @@ using System; using System.Collections.Generic; using System.Linq; -using global::OpenTracing; +using OpenTelemetry.Internal; using OpenTelemetry.Trace; +using OpenTracing; namespace OpenTelemetry.Shims.OpenTracing { - internal sealed class SpanShim : global::OpenTracing.ISpan + internal sealed class SpanShim : ISpan { /// /// The default event name if not specified. @@ -45,13 +46,14 @@ internal sealed class SpanShim : global::OpenTracing.ISpan public SpanShim(TelemetrySpan span) { - this.Span = span ?? throw new ArgumentNullException(nameof(span), "Parameter cannot be null"); + Guard.ThrowIfNull(span, nameof(span)); - if (!this.Span.Context.IsValid) + if (!span.Context.IsValid) { - throw new ArgumentException("Passed span's context is not valid", nameof(this.Span.Context)); + throw new ArgumentException($"Invalid '{nameof(SpanContext)}'", nameof(span.Context)); } + this.Span = span; this.spanContextShim = new SpanContextShim(this.Span.Context); } @@ -76,12 +78,9 @@ public string GetBaggageItem(string key) => Baggage.GetBaggage(key); /// - public global::OpenTracing.ISpan Log(DateTimeOffset timestamp, IEnumerable> fields) + public ISpan Log(DateTimeOffset timestamp, IEnumerable> fields) { - if (fields is null) - { - throw new ArgumentNullException(nameof(fields), "Parameter cannot be null"); - } + Guard.ThrowIfNull(fields, nameof(fields)); var payload = ConvertToEventPayload(fields); var eventName = payload.Item1; @@ -134,79 +133,64 @@ public string GetBaggageItem(string key) } /// - public global::OpenTracing.ISpan Log(IEnumerable> fields) + public ISpan Log(IEnumerable> fields) { return this.Log(DateTimeOffset.MinValue, fields); } /// - public global::OpenTracing.ISpan Log(string @event) + public ISpan Log(string @event) { - if (@event is null) - { - throw new ArgumentNullException(nameof(@event), "Parameter cannot be null"); - } + Guard.ThrowIfNull(@event, nameof(@event)); this.Span.AddEvent(@event); return this; } /// - public global::OpenTracing.ISpan Log(DateTimeOffset timestamp, string @event) + public ISpan Log(DateTimeOffset timestamp, string @event) { - if (@event is null) - { - throw new ArgumentNullException(nameof(@event), "Parameter cannot be null"); - } + Guard.ThrowIfNull(@event, nameof(@event)); this.Span.AddEvent(@event, timestamp); return this; } /// - public global::OpenTracing.ISpan SetBaggageItem(string key, string value) + public ISpan SetBaggageItem(string key, string value) { Baggage.SetBaggage(key, value); return this; } /// - public global::OpenTracing.ISpan SetOperationName(string operationName) + public ISpan SetOperationName(string operationName) { - if (operationName is null) - { - throw new ArgumentNullException(nameof(operationName), "Parameter cannot be null"); - } + Guard.ThrowIfNull(operationName, nameof(operationName)); this.Span.UpdateName(operationName); return this; } /// - public global::OpenTracing.ISpan SetTag(string key, string value) + public ISpan SetTag(string key, string value) { - if (key is null) - { - throw new ArgumentNullException(nameof(key), "Parameter cannot be null"); - } + Guard.ThrowIfNull(key, nameof(key)); this.Span.SetAttribute(key, value); return this; } /// - public global::OpenTracing.ISpan SetTag(string key, bool value) + public ISpan SetTag(string key, bool value) { - if (key is null) - { - throw new ArgumentNullException(nameof(key), "Parameter cannot be null"); - } + Guard.ThrowIfNull(key, nameof(key)); // Special case the OpenTracing Error Tag // see https://opentracing.io/specification/conventions/ if (global::OpenTracing.Tag.Tags.Error.Key.Equals(key, StringComparison.Ordinal)) { - this.Span.SetStatus(value ? Trace.Status.Error : Trace.Status.Ok); + this.Span.SetStatus(value ? Status.Error : Status.Ok); } else { @@ -217,37 +201,31 @@ public string GetBaggageItem(string key) } /// - public global::OpenTracing.ISpan SetTag(string key, int value) + public ISpan SetTag(string key, int value) { - if (key is null) - { - throw new ArgumentNullException(nameof(key), "Parameter cannot be null"); - } + Guard.ThrowIfNull(key, nameof(key)); this.Span.SetAttribute(key, value); return this; } /// - public global::OpenTracing.ISpan SetTag(string key, double value) + public ISpan SetTag(string key, double value) { - if (key is null) - { - throw new ArgumentNullException(nameof(key), "Parameter cannot be null"); - } + Guard.ThrowIfNull(key, nameof(key)); this.Span.SetAttribute(key, value); return this; } /// - public global::OpenTracing.ISpan SetTag(global::OpenTracing.Tag.BooleanTag tag, bool value) + public ISpan SetTag(global::OpenTracing.Tag.BooleanTag tag, bool value) { return this.SetTag(tag?.Key, value); } /// - public global::OpenTracing.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)) { @@ -258,13 +236,13 @@ public string GetBaggageItem(string key) } /// - public global::OpenTracing.ISpan SetTag(global::OpenTracing.Tag.IntTag tag, int value) + public ISpan SetTag(global::OpenTracing.Tag.IntTag tag, int value) { return this.SetTag(tag?.Key, value); } /// - public global::OpenTracing.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); } diff --git a/src/OpenTelemetry.Shims.OpenTracing/TracerShim.cs b/src/OpenTelemetry.Shims.OpenTracing/TracerShim.cs index f1540fcba8f..c702c1252d5 100644 --- a/src/OpenTelemetry.Shims.OpenTracing/TracerShim.cs +++ b/src/OpenTelemetry.Shims.OpenTracing/TracerShim.cs @@ -14,10 +14,10 @@ // limitations under the License. // -using System; using System.Collections.Generic; -using global::OpenTracing.Propagation; using OpenTelemetry.Context.Propagation; +using OpenTelemetry.Internal; +using OpenTracing.Propagation; namespace OpenTelemetry.Shims.OpenTracing { @@ -28,9 +28,11 @@ public class TracerShim : global::OpenTracing.ITracer public TracerShim(Trace.Tracer tracer, TextMapPropagator textFormat) { - this.tracer = tracer ?? throw new ArgumentNullException(nameof(tracer), "Parameter cannot be null"); - this.propagator = textFormat ?? throw new ArgumentNullException(nameof(textFormat), "Parameter cannot be null"); + Guard.ThrowIfNull(tracer, nameof(tracer)); + Guard.ThrowIfNull(textFormat, nameof(textFormat)); + this.tracer = tracer; + this.propagator = textFormat; this.ScopeManager = new ScopeManagerShim(this.tracer); } @@ -47,17 +49,10 @@ public TracerShim(Trace.Tracer tracer, TextMapPropagator textFormat) } /// - public global::OpenTracing.ISpanContext Extract(global::OpenTracing.Propagation.IFormat format, TCarrier carrier) + public global::OpenTracing.ISpanContext Extract(IFormat format, TCarrier carrier) { - if (format is null) - { - throw new ArgumentNullException(nameof(format), "Parameter cannot be null"); - } - - if (carrier == null) - { - throw new ArgumentNullException(nameof(carrier), "Parameter cannot be null"); - } + Guard.ThrowIfNull(format, nameof(format)); + Guard.ThrowIfNull(carrier, nameof(carrier)); PropagationContext propagationContext = default; @@ -94,28 +89,13 @@ static IEnumerable GetCarrierKeyValue(Dictionary public void Inject( global::OpenTracing.ISpanContext spanContext, - global::OpenTracing.Propagation.IFormat format, + IFormat format, TCarrier carrier) { - if (spanContext is null) - { - throw new ArgumentNullException(nameof(spanContext), "Parameter cannot be null"); - } - - if (!(spanContext is SpanContextShim shim)) - { - throw new ArgumentException("Context is not a valid SpanContextShim object", nameof(shim)); - } - - if (format is null) - { - throw new ArgumentNullException(nameof(format), "Parameter cannot be null"); - } - - if (carrier == null) - { - throw new ArgumentNullException(nameof(carrier), "Parameter cannot be null"); - } + Guard.ThrowIfNull(spanContext, nameof(spanContext)); + var shim = Guard.ThrowIfNotOfType(spanContext, nameof(spanContext)); + Guard.ThrowIfNull(format, nameof(format)); + Guard.ThrowIfNull(carrier, nameof(carrier)); if ((format == BuiltinFormats.TextMap || format == BuiltinFormats.HttpHeaders) && carrier is ITextMap textMapCarrier) { diff --git a/src/OpenTelemetry/.publicApi/net452/PublicAPI.Shipped.txt b/src/OpenTelemetry/.publicApi/net452/PublicAPI.Shipped.txt deleted file mode 100644 index e0e35c4592b..00000000000 --- a/src/OpenTelemetry/.publicApi/net452/PublicAPI.Shipped.txt +++ /dev/null @@ -1,160 +0,0 @@ -abstract OpenTelemetry.BaseExporter.Export(in OpenTelemetry.Batch batch) -> OpenTelemetry.ExportResult -abstract OpenTelemetry.BaseExportProcessor.OnExport(T data) -> void -abstract OpenTelemetry.Trace.Sampler.ShouldSample(in OpenTelemetry.Trace.SamplingParameters samplingParameters) -> OpenTelemetry.Trace.SamplingResult -OpenTelemetry.BaseExporter -OpenTelemetry.BaseExporter.BaseExporter() -> void -OpenTelemetry.BaseExporter.Dispose() -> void -OpenTelemetry.BaseExporter.ParentProvider.get -> OpenTelemetry.BaseProvider -OpenTelemetry.BaseExporter.Shutdown(int timeoutMilliseconds = -1) -> bool -OpenTelemetry.BaseExportProcessor -OpenTelemetry.BaseExportProcessor.BaseExportProcessor(OpenTelemetry.BaseExporter exporter) -> void -OpenTelemetry.BaseProcessor -OpenTelemetry.BaseProcessor.BaseProcessor() -> void -OpenTelemetry.BaseProcessor.Dispose() -> void -OpenTelemetry.BaseProcessor.ForceFlush(int timeoutMilliseconds = -1) -> bool -OpenTelemetry.BaseProcessor.ParentProvider.get -> OpenTelemetry.BaseProvider -OpenTelemetry.BaseProcessor.Shutdown(int timeoutMilliseconds = -1) -> bool -OpenTelemetry.Batch -OpenTelemetry.Batch.Batch() -> void -OpenTelemetry.Batch.Dispose() -> void -OpenTelemetry.Batch.Enumerator -OpenTelemetry.Batch.Enumerator.Current.get -> T -OpenTelemetry.Batch.Enumerator.Dispose() -> void -OpenTelemetry.Batch.Enumerator.Enumerator() -> void -OpenTelemetry.Batch.Enumerator.MoveNext() -> bool -OpenTelemetry.Batch.Enumerator.Reset() -> void -OpenTelemetry.Batch.GetEnumerator() -> OpenTelemetry.Batch.Enumerator -OpenTelemetry.BatchActivityExportProcessor -OpenTelemetry.BatchActivityExportProcessor.BatchActivityExportProcessor(OpenTelemetry.BaseExporter exporter, int maxQueueSize = 2048, int scheduledDelayMilliseconds = 5000, int exporterTimeoutMilliseconds = 30000, int maxExportBatchSize = 512) -> void -OpenTelemetry.BatchExportProcessor -OpenTelemetry.BatchExportProcessor.BatchExportProcessor(OpenTelemetry.BaseExporter exporter, int maxQueueSize = 2048, int scheduledDelayMilliseconds = 5000, int exporterTimeoutMilliseconds = 30000, int maxExportBatchSize = 512) -> void -OpenTelemetry.BatchExportProcessorOptions -OpenTelemetry.BatchExportProcessorOptions.BatchExportProcessorOptions() -> void -OpenTelemetry.BatchExportProcessorOptions.ExporterTimeoutMilliseconds.get -> int -OpenTelemetry.BatchExportProcessorOptions.ExporterTimeoutMilliseconds.set -> void -OpenTelemetry.BatchExportProcessorOptions.MaxExportBatchSize.get -> int -OpenTelemetry.BatchExportProcessorOptions.MaxExportBatchSize.set -> void -OpenTelemetry.BatchExportProcessorOptions.MaxQueueSize.get -> int -OpenTelemetry.BatchExportProcessorOptions.MaxQueueSize.set -> void -OpenTelemetry.BatchExportProcessorOptions.ScheduledDelayMilliseconds.get -> int -OpenTelemetry.BatchExportProcessorOptions.ScheduledDelayMilliseconds.set -> void -OpenTelemetry.CompositeProcessor -OpenTelemetry.CompositeProcessor.AddProcessor(OpenTelemetry.BaseProcessor processor) -> OpenTelemetry.CompositeProcessor -OpenTelemetry.CompositeProcessor.CompositeProcessor(System.Collections.Generic.IEnumerable> processors) -> void -OpenTelemetry.ExportProcessorType -OpenTelemetry.ExportProcessorType.Batch = 1 -> OpenTelemetry.ExportProcessorType -OpenTelemetry.ExportProcessorType.Simple = 0 -> OpenTelemetry.ExportProcessorType -OpenTelemetry.ExportResult -OpenTelemetry.ExportResult.Failure = 1 -> OpenTelemetry.ExportResult -OpenTelemetry.ExportResult.Success = 0 -> OpenTelemetry.ExportResult -OpenTelemetry.ProviderExtensions -OpenTelemetry.Resources.Resource -OpenTelemetry.Resources.Resource.Attributes.get -> System.Collections.Generic.IEnumerable> -OpenTelemetry.Resources.Resource.Merge(OpenTelemetry.Resources.Resource other) -> OpenTelemetry.Resources.Resource -OpenTelemetry.Resources.ResourceBuilder -OpenTelemetry.Resources.ResourceBuilder.Build() -> OpenTelemetry.Resources.Resource -OpenTelemetry.Resources.ResourceBuilder.Clear() -> OpenTelemetry.Resources.ResourceBuilder -OpenTelemetry.Resources.ResourceBuilderExtensions -OpenTelemetry.Sdk -OpenTelemetry.SimpleActivityExportProcessor -OpenTelemetry.SimpleActivityExportProcessor.SimpleActivityExportProcessor(OpenTelemetry.BaseExporter exporter) -> void -OpenTelemetry.SimpleExportProcessor -OpenTelemetry.SimpleExportProcessor.SimpleExportProcessor(OpenTelemetry.BaseExporter exporter) -> void -OpenTelemetry.SuppressInstrumentationScope -OpenTelemetry.SuppressInstrumentationScope.Dispose() -> void -OpenTelemetry.Trace.AlwaysOffSampler -OpenTelemetry.Trace.AlwaysOffSampler.AlwaysOffSampler() -> void -OpenTelemetry.Trace.AlwaysOnSampler -OpenTelemetry.Trace.AlwaysOnSampler.AlwaysOnSampler() -> void -OpenTelemetry.Trace.ParentBasedSampler -OpenTelemetry.Trace.ParentBasedSampler.ParentBasedSampler(OpenTelemetry.Trace.Sampler rootSampler) -> void -OpenTelemetry.Trace.ParentBasedSampler.ParentBasedSampler(OpenTelemetry.Trace.Sampler rootSampler, OpenTelemetry.Trace.Sampler remoteParentSampled = null, OpenTelemetry.Trace.Sampler remoteParentNotSampled = null, OpenTelemetry.Trace.Sampler localParentSampled = null, OpenTelemetry.Trace.Sampler localParentNotSampled = null) -> void -OpenTelemetry.Trace.Sampler -OpenTelemetry.Trace.Sampler.Description.get -> string -OpenTelemetry.Trace.Sampler.Description.set -> void -OpenTelemetry.Trace.Sampler.Sampler() -> void -OpenTelemetry.Trace.SamplingDecision -OpenTelemetry.Trace.SamplingDecision.Drop = 0 -> OpenTelemetry.Trace.SamplingDecision -OpenTelemetry.Trace.SamplingDecision.RecordAndSample = 2 -> OpenTelemetry.Trace.SamplingDecision -OpenTelemetry.Trace.SamplingDecision.RecordOnly = 1 -> OpenTelemetry.Trace.SamplingDecision -OpenTelemetry.Trace.SamplingParameters -OpenTelemetry.Trace.SamplingParameters.Kind.get -> System.Diagnostics.ActivityKind -OpenTelemetry.Trace.SamplingParameters.Links.get -> System.Collections.Generic.IEnumerable -OpenTelemetry.Trace.SamplingParameters.Name.get -> string -OpenTelemetry.Trace.SamplingParameters.ParentContext.get -> System.Diagnostics.ActivityContext -OpenTelemetry.Trace.SamplingParameters.SamplingParameters() -> void -OpenTelemetry.Trace.SamplingParameters.SamplingParameters(System.Diagnostics.ActivityContext parentContext, System.Diagnostics.ActivityTraceId traceId, string name, System.Diagnostics.ActivityKind kind, System.Collections.Generic.IEnumerable> tags = null, System.Collections.Generic.IEnumerable links = null) -> void -OpenTelemetry.Trace.SamplingParameters.Tags.get -> System.Collections.Generic.IEnumerable> -OpenTelemetry.Trace.SamplingParameters.TraceId.get -> System.Diagnostics.ActivityTraceId -OpenTelemetry.Trace.SamplingResult -OpenTelemetry.Trace.SamplingResult.Attributes.get -> System.Collections.Generic.IEnumerable> -OpenTelemetry.Trace.SamplingResult.Decision.get -> OpenTelemetry.Trace.SamplingDecision -OpenTelemetry.Trace.SamplingResult.Equals(OpenTelemetry.Trace.SamplingResult other) -> bool -OpenTelemetry.Trace.SamplingResult.SamplingResult() -> void -OpenTelemetry.Trace.SamplingResult.SamplingResult(bool isSampled) -> void -OpenTelemetry.Trace.SamplingResult.SamplingResult(OpenTelemetry.Trace.SamplingDecision decision) -> void -OpenTelemetry.Trace.SamplingResult.SamplingResult(OpenTelemetry.Trace.SamplingDecision decision, System.Collections.Generic.IEnumerable> attributes) -> void -OpenTelemetry.Trace.TraceIdRatioBasedSampler -OpenTelemetry.Trace.TraceIdRatioBasedSampler.TraceIdRatioBasedSampler(double probability) -> void -OpenTelemetry.Trace.TracerProviderBuilderBase -OpenTelemetry.Trace.TracerProviderBuilderBase.AddInstrumentation(string instrumentationName, string instrumentationVersion, System.Func instrumentationFactory) -> OpenTelemetry.Trace.TracerProviderBuilder -OpenTelemetry.Trace.TracerProviderBuilderBase.Build() -> OpenTelemetry.Trace.TracerProvider -OpenTelemetry.Trace.TracerProviderBuilderBase.TracerProviderBuilderBase() -> void -OpenTelemetry.Trace.TracerProviderBuilderExtensions -OpenTelemetry.Trace.TracerProviderExtensions -override OpenTelemetry.BaseExportProcessor.Dispose(bool disposing) -> void -override OpenTelemetry.BaseExportProcessor.OnEnd(T data) -> void -override OpenTelemetry.BaseExportProcessor.OnShutdown(int timeoutMilliseconds) -> bool -override OpenTelemetry.BatchActivityExportProcessor.OnEnd(System.Diagnostics.Activity data) -> void -override OpenTelemetry.BatchExportProcessor.OnExport(T data) -> void -override OpenTelemetry.BatchExportProcessor.OnForceFlush(int timeoutMilliseconds) -> bool -override OpenTelemetry.BatchExportProcessor.OnShutdown(int timeoutMilliseconds) -> bool -override OpenTelemetry.CompositeProcessor.Dispose(bool disposing) -> void -override OpenTelemetry.CompositeProcessor.OnEnd(T data) -> void -override OpenTelemetry.CompositeProcessor.OnForceFlush(int timeoutMilliseconds) -> bool -override OpenTelemetry.CompositeProcessor.OnShutdown(int timeoutMilliseconds) -> bool -override OpenTelemetry.CompositeProcessor.OnStart(T data) -> void -override OpenTelemetry.SimpleActivityExportProcessor.OnEnd(System.Diagnostics.Activity data) -> void -override OpenTelemetry.SimpleExportProcessor.OnExport(T data) -> void -override OpenTelemetry.Trace.AlwaysOffSampler.ShouldSample(in OpenTelemetry.Trace.SamplingParameters samplingParameters) -> OpenTelemetry.Trace.SamplingResult -override OpenTelemetry.Trace.AlwaysOnSampler.ShouldSample(in OpenTelemetry.Trace.SamplingParameters samplingParameters) -> OpenTelemetry.Trace.SamplingResult -override OpenTelemetry.Trace.ParentBasedSampler.ShouldSample(in OpenTelemetry.Trace.SamplingParameters samplingParameters) -> OpenTelemetry.Trace.SamplingResult -override OpenTelemetry.Trace.SamplingResult.Equals(object obj) -> bool -override OpenTelemetry.Trace.SamplingResult.GetHashCode() -> int -override OpenTelemetry.Trace.TraceIdRatioBasedSampler.ShouldSample(in OpenTelemetry.Trace.SamplingParameters samplingParameters) -> OpenTelemetry.Trace.SamplingResult -override OpenTelemetry.Trace.TracerProviderBuilderBase.AddInstrumentation(System.Func instrumentationFactory) -> OpenTelemetry.Trace.TracerProviderBuilder -override OpenTelemetry.Trace.TracerProviderBuilderBase.AddLegacySource(string operationName) -> OpenTelemetry.Trace.TracerProviderBuilder -override OpenTelemetry.Trace.TracerProviderBuilderBase.AddSource(params string[] names) -> OpenTelemetry.Trace.TracerProviderBuilder -override sealed OpenTelemetry.BaseExportProcessor.OnStart(T data) -> void -readonly OpenTelemetry.BaseExportProcessor.exporter -> OpenTelemetry.BaseExporter -static OpenTelemetry.ProviderExtensions.GetDefaultResource(this OpenTelemetry.BaseProvider baseProvider) -> OpenTelemetry.Resources.Resource -static OpenTelemetry.ProviderExtensions.GetResource(this OpenTelemetry.BaseProvider baseProvider) -> OpenTelemetry.Resources.Resource -static OpenTelemetry.Resources.Resource.Empty.get -> OpenTelemetry.Resources.Resource -static OpenTelemetry.Resources.ResourceBuilder.CreateDefault() -> OpenTelemetry.Resources.ResourceBuilder -static OpenTelemetry.Resources.ResourceBuilder.CreateEmpty() -> OpenTelemetry.Resources.ResourceBuilder -static OpenTelemetry.Resources.ResourceBuilderExtensions.AddAttributes(this OpenTelemetry.Resources.ResourceBuilder resourceBuilder, System.Collections.Generic.IEnumerable> attributes) -> OpenTelemetry.Resources.ResourceBuilder -static OpenTelemetry.Resources.ResourceBuilderExtensions.AddEnvironmentVariableDetector(this OpenTelemetry.Resources.ResourceBuilder resourceBuilder) -> OpenTelemetry.Resources.ResourceBuilder -static OpenTelemetry.Resources.ResourceBuilderExtensions.AddService(this OpenTelemetry.Resources.ResourceBuilder resourceBuilder, string serviceName, string serviceNamespace = null, string serviceVersion = null, bool autoGenerateServiceInstanceId = true, string serviceInstanceId = null) -> OpenTelemetry.Resources.ResourceBuilder -static OpenTelemetry.Resources.ResourceBuilderExtensions.AddTelemetrySdk(this OpenTelemetry.Resources.ResourceBuilder resourceBuilder) -> OpenTelemetry.Resources.ResourceBuilder -static OpenTelemetry.Sdk.CreateTracerProviderBuilder() -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Sdk.SetDefaultTextMapPropagator(OpenTelemetry.Context.Propagation.TextMapPropagator textMapPropagator) -> void -static OpenTelemetry.Sdk.SuppressInstrumentation.get -> bool -static OpenTelemetry.SuppressInstrumentationScope.Begin(bool value = true) -> System.IDisposable -static OpenTelemetry.SuppressInstrumentationScope.Enter() -> int -static OpenTelemetry.Trace.SamplingResult.operator !=(OpenTelemetry.Trace.SamplingResult decision1, OpenTelemetry.Trace.SamplingResult decision2) -> bool -static OpenTelemetry.Trace.SamplingResult.operator ==(OpenTelemetry.Trace.SamplingResult decision1, OpenTelemetry.Trace.SamplingResult decision2) -> bool -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddProcessor(this OpenTelemetry.Trace.TracerProviderBuilder tracerProviderBuilder, OpenTelemetry.BaseProcessor processor) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.Build(this OpenTelemetry.Trace.TracerProviderBuilder tracerProviderBuilder) -> OpenTelemetry.Trace.TracerProvider -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.SetErrorStatusOnException(this OpenTelemetry.Trace.TracerProviderBuilder tracerProviderBuilder, bool enabled = true) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.SetResourceBuilder(this OpenTelemetry.Trace.TracerProviderBuilder tracerProviderBuilder, OpenTelemetry.Resources.ResourceBuilder resourceBuilder) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.SetSampler(this OpenTelemetry.Trace.TracerProviderBuilder tracerProviderBuilder, OpenTelemetry.Trace.Sampler sampler) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.TracerProviderExtensions.AddProcessor(this OpenTelemetry.Trace.TracerProvider provider, OpenTelemetry.BaseProcessor processor) -> OpenTelemetry.Trace.TracerProvider -static OpenTelemetry.Trace.TracerProviderExtensions.ForceFlush(this OpenTelemetry.Trace.TracerProvider provider, int timeoutMilliseconds = -1) -> bool -static OpenTelemetry.Trace.TracerProviderExtensions.Shutdown(this OpenTelemetry.Trace.TracerProvider provider, int timeoutMilliseconds = -1) -> bool -virtual OpenTelemetry.BaseExporter.Dispose(bool disposing) -> void -virtual OpenTelemetry.BaseExporter.OnShutdown(int timeoutMilliseconds) -> bool -virtual OpenTelemetry.BaseProcessor.Dispose(bool disposing) -> void -virtual OpenTelemetry.BaseProcessor.OnEnd(T data) -> void -virtual OpenTelemetry.BaseProcessor.OnForceFlush(int timeoutMilliseconds) -> bool -virtual OpenTelemetry.BaseProcessor.OnShutdown(int timeoutMilliseconds) -> bool -virtual OpenTelemetry.BaseProcessor.OnStart(T data) -> void diff --git a/src/OpenTelemetry/.publicApi/net46/PublicAPI.Shipped.txt b/src/OpenTelemetry/.publicApi/net46/PublicAPI.Shipped.txt deleted file mode 100644 index e0e35c4592b..00000000000 --- a/src/OpenTelemetry/.publicApi/net46/PublicAPI.Shipped.txt +++ /dev/null @@ -1,160 +0,0 @@ -abstract OpenTelemetry.BaseExporter.Export(in OpenTelemetry.Batch batch) -> OpenTelemetry.ExportResult -abstract OpenTelemetry.BaseExportProcessor.OnExport(T data) -> void -abstract OpenTelemetry.Trace.Sampler.ShouldSample(in OpenTelemetry.Trace.SamplingParameters samplingParameters) -> OpenTelemetry.Trace.SamplingResult -OpenTelemetry.BaseExporter -OpenTelemetry.BaseExporter.BaseExporter() -> void -OpenTelemetry.BaseExporter.Dispose() -> void -OpenTelemetry.BaseExporter.ParentProvider.get -> OpenTelemetry.BaseProvider -OpenTelemetry.BaseExporter.Shutdown(int timeoutMilliseconds = -1) -> bool -OpenTelemetry.BaseExportProcessor -OpenTelemetry.BaseExportProcessor.BaseExportProcessor(OpenTelemetry.BaseExporter exporter) -> void -OpenTelemetry.BaseProcessor -OpenTelemetry.BaseProcessor.BaseProcessor() -> void -OpenTelemetry.BaseProcessor.Dispose() -> void -OpenTelemetry.BaseProcessor.ForceFlush(int timeoutMilliseconds = -1) -> bool -OpenTelemetry.BaseProcessor.ParentProvider.get -> OpenTelemetry.BaseProvider -OpenTelemetry.BaseProcessor.Shutdown(int timeoutMilliseconds = -1) -> bool -OpenTelemetry.Batch -OpenTelemetry.Batch.Batch() -> void -OpenTelemetry.Batch.Dispose() -> void -OpenTelemetry.Batch.Enumerator -OpenTelemetry.Batch.Enumerator.Current.get -> T -OpenTelemetry.Batch.Enumerator.Dispose() -> void -OpenTelemetry.Batch.Enumerator.Enumerator() -> void -OpenTelemetry.Batch.Enumerator.MoveNext() -> bool -OpenTelemetry.Batch.Enumerator.Reset() -> void -OpenTelemetry.Batch.GetEnumerator() -> OpenTelemetry.Batch.Enumerator -OpenTelemetry.BatchActivityExportProcessor -OpenTelemetry.BatchActivityExportProcessor.BatchActivityExportProcessor(OpenTelemetry.BaseExporter exporter, int maxQueueSize = 2048, int scheduledDelayMilliseconds = 5000, int exporterTimeoutMilliseconds = 30000, int maxExportBatchSize = 512) -> void -OpenTelemetry.BatchExportProcessor -OpenTelemetry.BatchExportProcessor.BatchExportProcessor(OpenTelemetry.BaseExporter exporter, int maxQueueSize = 2048, int scheduledDelayMilliseconds = 5000, int exporterTimeoutMilliseconds = 30000, int maxExportBatchSize = 512) -> void -OpenTelemetry.BatchExportProcessorOptions -OpenTelemetry.BatchExportProcessorOptions.BatchExportProcessorOptions() -> void -OpenTelemetry.BatchExportProcessorOptions.ExporterTimeoutMilliseconds.get -> int -OpenTelemetry.BatchExportProcessorOptions.ExporterTimeoutMilliseconds.set -> void -OpenTelemetry.BatchExportProcessorOptions.MaxExportBatchSize.get -> int -OpenTelemetry.BatchExportProcessorOptions.MaxExportBatchSize.set -> void -OpenTelemetry.BatchExportProcessorOptions.MaxQueueSize.get -> int -OpenTelemetry.BatchExportProcessorOptions.MaxQueueSize.set -> void -OpenTelemetry.BatchExportProcessorOptions.ScheduledDelayMilliseconds.get -> int -OpenTelemetry.BatchExportProcessorOptions.ScheduledDelayMilliseconds.set -> void -OpenTelemetry.CompositeProcessor -OpenTelemetry.CompositeProcessor.AddProcessor(OpenTelemetry.BaseProcessor processor) -> OpenTelemetry.CompositeProcessor -OpenTelemetry.CompositeProcessor.CompositeProcessor(System.Collections.Generic.IEnumerable> processors) -> void -OpenTelemetry.ExportProcessorType -OpenTelemetry.ExportProcessorType.Batch = 1 -> OpenTelemetry.ExportProcessorType -OpenTelemetry.ExportProcessorType.Simple = 0 -> OpenTelemetry.ExportProcessorType -OpenTelemetry.ExportResult -OpenTelemetry.ExportResult.Failure = 1 -> OpenTelemetry.ExportResult -OpenTelemetry.ExportResult.Success = 0 -> OpenTelemetry.ExportResult -OpenTelemetry.ProviderExtensions -OpenTelemetry.Resources.Resource -OpenTelemetry.Resources.Resource.Attributes.get -> System.Collections.Generic.IEnumerable> -OpenTelemetry.Resources.Resource.Merge(OpenTelemetry.Resources.Resource other) -> OpenTelemetry.Resources.Resource -OpenTelemetry.Resources.ResourceBuilder -OpenTelemetry.Resources.ResourceBuilder.Build() -> OpenTelemetry.Resources.Resource -OpenTelemetry.Resources.ResourceBuilder.Clear() -> OpenTelemetry.Resources.ResourceBuilder -OpenTelemetry.Resources.ResourceBuilderExtensions -OpenTelemetry.Sdk -OpenTelemetry.SimpleActivityExportProcessor -OpenTelemetry.SimpleActivityExportProcessor.SimpleActivityExportProcessor(OpenTelemetry.BaseExporter exporter) -> void -OpenTelemetry.SimpleExportProcessor -OpenTelemetry.SimpleExportProcessor.SimpleExportProcessor(OpenTelemetry.BaseExporter exporter) -> void -OpenTelemetry.SuppressInstrumentationScope -OpenTelemetry.SuppressInstrumentationScope.Dispose() -> void -OpenTelemetry.Trace.AlwaysOffSampler -OpenTelemetry.Trace.AlwaysOffSampler.AlwaysOffSampler() -> void -OpenTelemetry.Trace.AlwaysOnSampler -OpenTelemetry.Trace.AlwaysOnSampler.AlwaysOnSampler() -> void -OpenTelemetry.Trace.ParentBasedSampler -OpenTelemetry.Trace.ParentBasedSampler.ParentBasedSampler(OpenTelemetry.Trace.Sampler rootSampler) -> void -OpenTelemetry.Trace.ParentBasedSampler.ParentBasedSampler(OpenTelemetry.Trace.Sampler rootSampler, OpenTelemetry.Trace.Sampler remoteParentSampled = null, OpenTelemetry.Trace.Sampler remoteParentNotSampled = null, OpenTelemetry.Trace.Sampler localParentSampled = null, OpenTelemetry.Trace.Sampler localParentNotSampled = null) -> void -OpenTelemetry.Trace.Sampler -OpenTelemetry.Trace.Sampler.Description.get -> string -OpenTelemetry.Trace.Sampler.Description.set -> void -OpenTelemetry.Trace.Sampler.Sampler() -> void -OpenTelemetry.Trace.SamplingDecision -OpenTelemetry.Trace.SamplingDecision.Drop = 0 -> OpenTelemetry.Trace.SamplingDecision -OpenTelemetry.Trace.SamplingDecision.RecordAndSample = 2 -> OpenTelemetry.Trace.SamplingDecision -OpenTelemetry.Trace.SamplingDecision.RecordOnly = 1 -> OpenTelemetry.Trace.SamplingDecision -OpenTelemetry.Trace.SamplingParameters -OpenTelemetry.Trace.SamplingParameters.Kind.get -> System.Diagnostics.ActivityKind -OpenTelemetry.Trace.SamplingParameters.Links.get -> System.Collections.Generic.IEnumerable -OpenTelemetry.Trace.SamplingParameters.Name.get -> string -OpenTelemetry.Trace.SamplingParameters.ParentContext.get -> System.Diagnostics.ActivityContext -OpenTelemetry.Trace.SamplingParameters.SamplingParameters() -> void -OpenTelemetry.Trace.SamplingParameters.SamplingParameters(System.Diagnostics.ActivityContext parentContext, System.Diagnostics.ActivityTraceId traceId, string name, System.Diagnostics.ActivityKind kind, System.Collections.Generic.IEnumerable> tags = null, System.Collections.Generic.IEnumerable links = null) -> void -OpenTelemetry.Trace.SamplingParameters.Tags.get -> System.Collections.Generic.IEnumerable> -OpenTelemetry.Trace.SamplingParameters.TraceId.get -> System.Diagnostics.ActivityTraceId -OpenTelemetry.Trace.SamplingResult -OpenTelemetry.Trace.SamplingResult.Attributes.get -> System.Collections.Generic.IEnumerable> -OpenTelemetry.Trace.SamplingResult.Decision.get -> OpenTelemetry.Trace.SamplingDecision -OpenTelemetry.Trace.SamplingResult.Equals(OpenTelemetry.Trace.SamplingResult other) -> bool -OpenTelemetry.Trace.SamplingResult.SamplingResult() -> void -OpenTelemetry.Trace.SamplingResult.SamplingResult(bool isSampled) -> void -OpenTelemetry.Trace.SamplingResult.SamplingResult(OpenTelemetry.Trace.SamplingDecision decision) -> void -OpenTelemetry.Trace.SamplingResult.SamplingResult(OpenTelemetry.Trace.SamplingDecision decision, System.Collections.Generic.IEnumerable> attributes) -> void -OpenTelemetry.Trace.TraceIdRatioBasedSampler -OpenTelemetry.Trace.TraceIdRatioBasedSampler.TraceIdRatioBasedSampler(double probability) -> void -OpenTelemetry.Trace.TracerProviderBuilderBase -OpenTelemetry.Trace.TracerProviderBuilderBase.AddInstrumentation(string instrumentationName, string instrumentationVersion, System.Func instrumentationFactory) -> OpenTelemetry.Trace.TracerProviderBuilder -OpenTelemetry.Trace.TracerProviderBuilderBase.Build() -> OpenTelemetry.Trace.TracerProvider -OpenTelemetry.Trace.TracerProviderBuilderBase.TracerProviderBuilderBase() -> void -OpenTelemetry.Trace.TracerProviderBuilderExtensions -OpenTelemetry.Trace.TracerProviderExtensions -override OpenTelemetry.BaseExportProcessor.Dispose(bool disposing) -> void -override OpenTelemetry.BaseExportProcessor.OnEnd(T data) -> void -override OpenTelemetry.BaseExportProcessor.OnShutdown(int timeoutMilliseconds) -> bool -override OpenTelemetry.BatchActivityExportProcessor.OnEnd(System.Diagnostics.Activity data) -> void -override OpenTelemetry.BatchExportProcessor.OnExport(T data) -> void -override OpenTelemetry.BatchExportProcessor.OnForceFlush(int timeoutMilliseconds) -> bool -override OpenTelemetry.BatchExportProcessor.OnShutdown(int timeoutMilliseconds) -> bool -override OpenTelemetry.CompositeProcessor.Dispose(bool disposing) -> void -override OpenTelemetry.CompositeProcessor.OnEnd(T data) -> void -override OpenTelemetry.CompositeProcessor.OnForceFlush(int timeoutMilliseconds) -> bool -override OpenTelemetry.CompositeProcessor.OnShutdown(int timeoutMilliseconds) -> bool -override OpenTelemetry.CompositeProcessor.OnStart(T data) -> void -override OpenTelemetry.SimpleActivityExportProcessor.OnEnd(System.Diagnostics.Activity data) -> void -override OpenTelemetry.SimpleExportProcessor.OnExport(T data) -> void -override OpenTelemetry.Trace.AlwaysOffSampler.ShouldSample(in OpenTelemetry.Trace.SamplingParameters samplingParameters) -> OpenTelemetry.Trace.SamplingResult -override OpenTelemetry.Trace.AlwaysOnSampler.ShouldSample(in OpenTelemetry.Trace.SamplingParameters samplingParameters) -> OpenTelemetry.Trace.SamplingResult -override OpenTelemetry.Trace.ParentBasedSampler.ShouldSample(in OpenTelemetry.Trace.SamplingParameters samplingParameters) -> OpenTelemetry.Trace.SamplingResult -override OpenTelemetry.Trace.SamplingResult.Equals(object obj) -> bool -override OpenTelemetry.Trace.SamplingResult.GetHashCode() -> int -override OpenTelemetry.Trace.TraceIdRatioBasedSampler.ShouldSample(in OpenTelemetry.Trace.SamplingParameters samplingParameters) -> OpenTelemetry.Trace.SamplingResult -override OpenTelemetry.Trace.TracerProviderBuilderBase.AddInstrumentation(System.Func instrumentationFactory) -> OpenTelemetry.Trace.TracerProviderBuilder -override OpenTelemetry.Trace.TracerProviderBuilderBase.AddLegacySource(string operationName) -> OpenTelemetry.Trace.TracerProviderBuilder -override OpenTelemetry.Trace.TracerProviderBuilderBase.AddSource(params string[] names) -> OpenTelemetry.Trace.TracerProviderBuilder -override sealed OpenTelemetry.BaseExportProcessor.OnStart(T data) -> void -readonly OpenTelemetry.BaseExportProcessor.exporter -> OpenTelemetry.BaseExporter -static OpenTelemetry.ProviderExtensions.GetDefaultResource(this OpenTelemetry.BaseProvider baseProvider) -> OpenTelemetry.Resources.Resource -static OpenTelemetry.ProviderExtensions.GetResource(this OpenTelemetry.BaseProvider baseProvider) -> OpenTelemetry.Resources.Resource -static OpenTelemetry.Resources.Resource.Empty.get -> OpenTelemetry.Resources.Resource -static OpenTelemetry.Resources.ResourceBuilder.CreateDefault() -> OpenTelemetry.Resources.ResourceBuilder -static OpenTelemetry.Resources.ResourceBuilder.CreateEmpty() -> OpenTelemetry.Resources.ResourceBuilder -static OpenTelemetry.Resources.ResourceBuilderExtensions.AddAttributes(this OpenTelemetry.Resources.ResourceBuilder resourceBuilder, System.Collections.Generic.IEnumerable> attributes) -> OpenTelemetry.Resources.ResourceBuilder -static OpenTelemetry.Resources.ResourceBuilderExtensions.AddEnvironmentVariableDetector(this OpenTelemetry.Resources.ResourceBuilder resourceBuilder) -> OpenTelemetry.Resources.ResourceBuilder -static OpenTelemetry.Resources.ResourceBuilderExtensions.AddService(this OpenTelemetry.Resources.ResourceBuilder resourceBuilder, string serviceName, string serviceNamespace = null, string serviceVersion = null, bool autoGenerateServiceInstanceId = true, string serviceInstanceId = null) -> OpenTelemetry.Resources.ResourceBuilder -static OpenTelemetry.Resources.ResourceBuilderExtensions.AddTelemetrySdk(this OpenTelemetry.Resources.ResourceBuilder resourceBuilder) -> OpenTelemetry.Resources.ResourceBuilder -static OpenTelemetry.Sdk.CreateTracerProviderBuilder() -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Sdk.SetDefaultTextMapPropagator(OpenTelemetry.Context.Propagation.TextMapPropagator textMapPropagator) -> void -static OpenTelemetry.Sdk.SuppressInstrumentation.get -> bool -static OpenTelemetry.SuppressInstrumentationScope.Begin(bool value = true) -> System.IDisposable -static OpenTelemetry.SuppressInstrumentationScope.Enter() -> int -static OpenTelemetry.Trace.SamplingResult.operator !=(OpenTelemetry.Trace.SamplingResult decision1, OpenTelemetry.Trace.SamplingResult decision2) -> bool -static OpenTelemetry.Trace.SamplingResult.operator ==(OpenTelemetry.Trace.SamplingResult decision1, OpenTelemetry.Trace.SamplingResult decision2) -> bool -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.AddProcessor(this OpenTelemetry.Trace.TracerProviderBuilder tracerProviderBuilder, OpenTelemetry.BaseProcessor processor) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.Build(this OpenTelemetry.Trace.TracerProviderBuilder tracerProviderBuilder) -> OpenTelemetry.Trace.TracerProvider -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.SetErrorStatusOnException(this OpenTelemetry.Trace.TracerProviderBuilder tracerProviderBuilder, bool enabled = true) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.SetResourceBuilder(this OpenTelemetry.Trace.TracerProviderBuilder tracerProviderBuilder, OpenTelemetry.Resources.ResourceBuilder resourceBuilder) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.TracerProviderBuilderExtensions.SetSampler(this OpenTelemetry.Trace.TracerProviderBuilder tracerProviderBuilder, OpenTelemetry.Trace.Sampler sampler) -> OpenTelemetry.Trace.TracerProviderBuilder -static OpenTelemetry.Trace.TracerProviderExtensions.AddProcessor(this OpenTelemetry.Trace.TracerProvider provider, OpenTelemetry.BaseProcessor processor) -> OpenTelemetry.Trace.TracerProvider -static OpenTelemetry.Trace.TracerProviderExtensions.ForceFlush(this OpenTelemetry.Trace.TracerProvider provider, int timeoutMilliseconds = -1) -> bool -static OpenTelemetry.Trace.TracerProviderExtensions.Shutdown(this OpenTelemetry.Trace.TracerProvider provider, int timeoutMilliseconds = -1) -> bool -virtual OpenTelemetry.BaseExporter.Dispose(bool disposing) -> void -virtual OpenTelemetry.BaseExporter.OnShutdown(int timeoutMilliseconds) -> bool -virtual OpenTelemetry.BaseProcessor.Dispose(bool disposing) -> void -virtual OpenTelemetry.BaseProcessor.OnEnd(T data) -> void -virtual OpenTelemetry.BaseProcessor.OnForceFlush(int timeoutMilliseconds) -> bool -virtual OpenTelemetry.BaseProcessor.OnShutdown(int timeoutMilliseconds) -> bool -virtual OpenTelemetry.BaseProcessor.OnStart(T data) -> void diff --git a/src/OpenTelemetry/.publicApi/net461/PublicAPI.Unshipped.txt b/src/OpenTelemetry/.publicApi/net461/PublicAPI.Unshipped.txt index e69de29bb2d..6d964985981 100644 --- a/src/OpenTelemetry/.publicApi/net461/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry/.publicApi/net461/PublicAPI.Unshipped.txt @@ -0,0 +1,141 @@ +OpenTelemetry.BaseExporter.ForceFlush(int timeoutMilliseconds = -1) -> bool +OpenTelemetry.Batch.Batch(T[] items, int count) -> void +OpenTelemetry.Batch.Count.get -> long +OpenTelemetry.Metrics.AggregationTemporality +OpenTelemetry.Metrics.AggregationTemporality.Cumulative = 1 -> OpenTelemetry.Metrics.AggregationTemporality +OpenTelemetry.Metrics.AggregationTemporality.Delta = 2 -> OpenTelemetry.Metrics.AggregationTemporality +OpenTelemetry.Metrics.AggregationTemporalityAttribute +OpenTelemetry.Metrics.AggregationTemporalityAttribute.AggregationTemporalityAttribute(OpenTelemetry.Metrics.AggregationTemporality temporality) -> void +OpenTelemetry.Metrics.AggregationTemporalityAttribute.Temporality.get -> OpenTelemetry.Metrics.AggregationTemporality +OpenTelemetry.Metrics.BaseExportingMetricReader +OpenTelemetry.Metrics.BaseExportingMetricReader.BaseExportingMetricReader(OpenTelemetry.BaseExporter exporter) -> void +OpenTelemetry.Metrics.BaseExportingMetricReader.SupportedExportModes.get -> OpenTelemetry.Metrics.ExportModes +OpenTelemetry.Metrics.HistogramBucket +OpenTelemetry.Metrics.HistogramBucket.BucketCount.get -> long +OpenTelemetry.Metrics.HistogramBucket.ExplicitBound.get -> double +OpenTelemetry.Metrics.HistogramBucket.HistogramBucket() -> void +OpenTelemetry.Metrics.HistogramBuckets +OpenTelemetry.Metrics.HistogramBuckets.Enumerator +OpenTelemetry.Metrics.HistogramBuckets.Enumerator.Current.get -> OpenTelemetry.Metrics.HistogramBucket +OpenTelemetry.Metrics.HistogramBuckets.Enumerator.Enumerator() -> void +OpenTelemetry.Metrics.HistogramBuckets.Enumerator.MoveNext() -> bool +OpenTelemetry.Metrics.HistogramBuckets.GetEnumerator() -> OpenTelemetry.Metrics.HistogramBuckets.Enumerator +OpenTelemetry.Metrics.MetricPoint.EndTime.get -> System.DateTimeOffset +OpenTelemetry.Metrics.MetricPoint.GetSumDouble() -> double +OpenTelemetry.Metrics.MetricPoint.GetSumLong() -> long +OpenTelemetry.Metrics.MetricPoint.GetGaugeLastValueDouble() -> double +OpenTelemetry.Metrics.MetricPoint.GetGaugeLastValueLong() -> long +OpenTelemetry.Metrics.MetricPoint.GetHistogramBuckets() -> OpenTelemetry.Metrics.HistogramBuckets +OpenTelemetry.Metrics.MetricPoint.StartTime.get -> System.DateTimeOffset +OpenTelemetry.Metrics.MetricPointsAccessor +OpenTelemetry.Metrics.MetricPointsAccessor.MetricPointsAccessor() -> void +OpenTelemetry.Metrics.MetricPointsAccessor.Enumerator +OpenTelemetry.Metrics.MetricPointsAccessor.Enumerator.Current.get -> OpenTelemetry.Metrics.MetricPoint +OpenTelemetry.Metrics.MetricPointsAccessor.Enumerator.Enumerator() -> void +OpenTelemetry.Metrics.MetricPointsAccessor.Enumerator.MoveNext() -> bool +OpenTelemetry.Metrics.MetricPointsAccessor.GetEnumerator() -> OpenTelemetry.Metrics.MetricPointsAccessor.Enumerator +OpenTelemetry.Metrics.ExportModes +OpenTelemetry.Metrics.ExportModes.Pull = 2 -> OpenTelemetry.Metrics.ExportModes +OpenTelemetry.Metrics.ExportModes.Push = 1 -> OpenTelemetry.Metrics.ExportModes +OpenTelemetry.Metrics.ExportModesAttribute +OpenTelemetry.Metrics.ExportModesAttribute.ExportModesAttribute(OpenTelemetry.Metrics.ExportModes supported) -> void +OpenTelemetry.Metrics.ExportModesAttribute.Supported.get -> OpenTelemetry.Metrics.ExportModes +OpenTelemetry.Metrics.ExplicitBucketHistogramConfiguration +OpenTelemetry.Metrics.ExplicitBucketHistogramConfiguration.Boundaries.get -> double[] +OpenTelemetry.Metrics.ExplicitBucketHistogramConfiguration.Boundaries.set -> void +OpenTelemetry.Metrics.ExplicitBucketHistogramConfiguration.ExplicitBucketHistogramConfiguration() -> void +OpenTelemetry.Metrics.IPullMetricExporter +OpenTelemetry.Metrics.IPullMetricExporter.Collect.get -> System.Func +OpenTelemetry.Metrics.IPullMetricExporter.Collect.set -> void +OpenTelemetry.Metrics.MeterProviderBuilderBase +OpenTelemetry.Metrics.MeterProviderBuilderBase.Build() -> OpenTelemetry.Metrics.MeterProvider +OpenTelemetry.Metrics.MeterProviderBuilderBase.MeterProviderBuilderBase() -> void +OpenTelemetry.Metrics.MeterProviderBuilderExtensions +OpenTelemetry.Metrics.MeterProviderExtensions +OpenTelemetry.Metrics.Metric +OpenTelemetry.Metrics.Metric.Description.get -> string +OpenTelemetry.Metrics.Metric.GetMetricPoints() -> OpenTelemetry.Metrics.MetricPointsAccessor +OpenTelemetry.Metrics.Metric.Meter.get -> System.Diagnostics.Metrics.Meter +OpenTelemetry.Metrics.Metric.MetricType.get -> OpenTelemetry.Metrics.MetricType +OpenTelemetry.Metrics.Metric.Name.get -> string +OpenTelemetry.Metrics.Metric.Temporality.get -> OpenTelemetry.Metrics.AggregationTemporality +OpenTelemetry.Metrics.Metric.Unit.get -> string +OpenTelemetry.Metrics.MetricPoint +OpenTelemetry.Metrics.MetricPoint.GetHistogramCount() -> long +OpenTelemetry.Metrics.MetricPoint.GetHistogramSum() -> double +OpenTelemetry.Metrics.MetricPoint.MetricPoint() -> void +OpenTelemetry.Metrics.MetricPoint.Tags.get -> OpenTelemetry.ReadOnlyTagCollection +OpenTelemetry.Metrics.MetricReader +OpenTelemetry.Metrics.MetricReader.Collect(int timeoutMilliseconds = -1) -> bool +OpenTelemetry.Metrics.MetricReader.Dispose() -> void +OpenTelemetry.Metrics.MetricReader.MetricReader() -> void +OpenTelemetry.Metrics.MetricReader.ParentProvider.get -> OpenTelemetry.BaseProvider +OpenTelemetry.Metrics.MetricReader.Shutdown(int timeoutMilliseconds = -1) -> bool +OpenTelemetry.Metrics.MetricReader.Temporality.get -> OpenTelemetry.Metrics.AggregationTemporality +OpenTelemetry.Metrics.MetricReader.Temporality.set -> void +OpenTelemetry.Metrics.MetricReaderType +OpenTelemetry.Metrics.MetricReaderType.Manual = 0 -> OpenTelemetry.Metrics.MetricReaderType +OpenTelemetry.Metrics.MetricReaderType.Periodic = 1 -> OpenTelemetry.Metrics.MetricReaderType +OpenTelemetry.Metrics.MetricStreamConfiguration +OpenTelemetry.Metrics.MetricStreamConfiguration.Description.get -> string +OpenTelemetry.Metrics.MetricStreamConfiguration.Description.set -> void +OpenTelemetry.Metrics.MetricStreamConfiguration.MetricStreamConfiguration() -> void +OpenTelemetry.Metrics.MetricStreamConfiguration.Name.get -> string +OpenTelemetry.Metrics.MetricStreamConfiguration.Name.set -> void +OpenTelemetry.Metrics.MetricStreamConfiguration.TagKeys.get -> string[] +OpenTelemetry.Metrics.MetricStreamConfiguration.TagKeys.set -> void +OpenTelemetry.Metrics.MetricType +OpenTelemetry.Metrics.MetricType.DoubleGauge = 45 -> OpenTelemetry.Metrics.MetricType +OpenTelemetry.Metrics.MetricType.DoubleSum = 29 -> OpenTelemetry.Metrics.MetricType +OpenTelemetry.Metrics.MetricType.Histogram = 64 -> OpenTelemetry.Metrics.MetricType +OpenTelemetry.Metrics.MetricType.LongGauge = 42 -> OpenTelemetry.Metrics.MetricType +OpenTelemetry.Metrics.MetricType.LongSum = 26 -> OpenTelemetry.Metrics.MetricType +OpenTelemetry.Metrics.MetricTypeExtensions +OpenTelemetry.Metrics.PeriodicExportingMetricReader +OpenTelemetry.Metrics.PeriodicExportingMetricReader.PeriodicExportingMetricReader(OpenTelemetry.BaseExporter exporter, int exportIntervalMilliseconds = 60000, int exportTimeoutMilliseconds = 30000) -> void +OpenTelemetry.Metrics.PeriodicExportingMetricReaderOptions +OpenTelemetry.Metrics.PeriodicExportingMetricReaderOptions.PeriodicExportingMetricReaderOptions() -> void +OpenTelemetry.Metrics.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds.get -> int +OpenTelemetry.Metrics.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds.set -> void +OpenTelemetry.ReadOnlyTagCollection +OpenTelemetry.ReadOnlyTagCollection.Count.get -> int +OpenTelemetry.ReadOnlyTagCollection.Enumerator +OpenTelemetry.ReadOnlyTagCollection.Enumerator.Current.get -> System.Collections.Generic.KeyValuePair +OpenTelemetry.ReadOnlyTagCollection.Enumerator.Enumerator() -> void +OpenTelemetry.ReadOnlyTagCollection.Enumerator.MoveNext() -> bool +OpenTelemetry.ReadOnlyTagCollection.GetEnumerator() -> OpenTelemetry.ReadOnlyTagCollection.Enumerator +OpenTelemetry.ReadOnlyTagCollection.ReadOnlyTagCollection() -> void +OpenTelemetry.Trace.BatchExportActivityProcessorOptions +OpenTelemetry.Trace.BatchExportActivityProcessorOptions.BatchExportActivityProcessorOptions() -> void +override OpenTelemetry.BaseExportProcessor.OnForceFlush(int timeoutMilliseconds) -> bool +override OpenTelemetry.BatchExportProcessor.Dispose(bool disposing) -> void +override OpenTelemetry.Metrics.BaseExportingMetricReader.Dispose(bool disposing) -> void +override OpenTelemetry.Metrics.BaseExportingMetricReader.OnCollect(int timeoutMilliseconds) -> bool +override OpenTelemetry.Metrics.BaseExportingMetricReader.OnShutdown(int timeoutMilliseconds) -> bool +override OpenTelemetry.Metrics.MeterProviderBuilderBase.AddInstrumentation(System.Func instrumentationFactory) -> OpenTelemetry.Metrics.MeterProviderBuilder +override OpenTelemetry.Metrics.MeterProviderBuilderBase.AddMeter(params string[] names) -> OpenTelemetry.Metrics.MeterProviderBuilder +override OpenTelemetry.Metrics.PeriodicExportingMetricReader.Dispose(bool disposing) -> void +override OpenTelemetry.Metrics.PeriodicExportingMetricReader.OnShutdown(int timeoutMilliseconds) -> bool +readonly OpenTelemetry.Metrics.BaseExportingMetricReader.exporter -> OpenTelemetry.BaseExporter +static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddReader(this OpenTelemetry.Metrics.MeterProviderBuilder meterProviderBuilder, OpenTelemetry.Metrics.MetricReader reader) -> OpenTelemetry.Metrics.MeterProviderBuilder +static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddView(this OpenTelemetry.Metrics.MeterProviderBuilder meterProviderBuilder, string instrumentName, OpenTelemetry.Metrics.MetricStreamConfiguration metricStreamConfiguration) -> OpenTelemetry.Metrics.MeterProviderBuilder +static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddView(this OpenTelemetry.Metrics.MeterProviderBuilder meterProviderBuilder, string instrumentName, string name) -> OpenTelemetry.Metrics.MeterProviderBuilder +static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddView(this OpenTelemetry.Metrics.MeterProviderBuilder meterProviderBuilder, System.Func viewConfig) -> OpenTelemetry.Metrics.MeterProviderBuilder +static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.Build(this OpenTelemetry.Metrics.MeterProviderBuilder meterProviderBuilder) -> OpenTelemetry.Metrics.MeterProvider +static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.SetMaxMetricPointsPerMetricStream(this OpenTelemetry.Metrics.MeterProviderBuilder meterProviderBuilder, int maxMetricPointsPerMetricStream) -> OpenTelemetry.Metrics.MeterProviderBuilder +static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.SetMaxMetricStreams(this OpenTelemetry.Metrics.MeterProviderBuilder meterProviderBuilder, int maxMetricStreams) -> OpenTelemetry.Metrics.MeterProviderBuilder +static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.SetResourceBuilder(this OpenTelemetry.Metrics.MeterProviderBuilder meterProviderBuilder, OpenTelemetry.Resources.ResourceBuilder resourceBuilder) -> OpenTelemetry.Metrics.MeterProviderBuilder +static OpenTelemetry.Metrics.MeterProviderExtensions.ForceFlush(this OpenTelemetry.Metrics.MeterProvider provider, int timeoutMilliseconds = -1) -> bool +static OpenTelemetry.Metrics.MeterProviderExtensions.Shutdown(this OpenTelemetry.Metrics.MeterProvider provider, int timeoutMilliseconds = -1) -> bool +static OpenTelemetry.Metrics.MeterProviderExtensions.TryFindExporter(this OpenTelemetry.Metrics.MeterProvider provider, out T exporter) -> bool +static OpenTelemetry.Metrics.MetricTypeExtensions.IsDouble(this OpenTelemetry.Metrics.MetricType self) -> bool +static OpenTelemetry.Metrics.MetricTypeExtensions.IsGauge(this OpenTelemetry.Metrics.MetricType self) -> bool +static OpenTelemetry.Metrics.MetricTypeExtensions.IsHistogram(this OpenTelemetry.Metrics.MetricType self) -> bool +static OpenTelemetry.Metrics.MetricTypeExtensions.IsLong(this OpenTelemetry.Metrics.MetricType self) -> bool +static OpenTelemetry.Metrics.MetricTypeExtensions.IsSum(this OpenTelemetry.Metrics.MetricType self) -> bool +static OpenTelemetry.Sdk.CreateMeterProviderBuilder() -> OpenTelemetry.Metrics.MeterProviderBuilder +static readonly OpenTelemetry.Metrics.MetricStreamConfiguration.Drop -> OpenTelemetry.Metrics.MetricStreamConfiguration +virtual OpenTelemetry.BaseExporter.OnForceFlush(int timeoutMilliseconds) -> bool +virtual OpenTelemetry.Metrics.MetricReader.Dispose(bool disposing) -> void +virtual OpenTelemetry.Metrics.MetricReader.OnCollect(int timeoutMilliseconds) -> bool +virtual OpenTelemetry.Metrics.MetricReader.OnShutdown(int timeoutMilliseconds) -> bool diff --git a/src/OpenTelemetry/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt index e69de29bb2d..6d964985981 100644 --- a/src/OpenTelemetry/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/OpenTelemetry/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt @@ -0,0 +1,141 @@ +OpenTelemetry.BaseExporter.ForceFlush(int timeoutMilliseconds = -1) -> bool +OpenTelemetry.Batch.Batch(T[] items, int count) -> void +OpenTelemetry.Batch.Count.get -> long +OpenTelemetry.Metrics.AggregationTemporality +OpenTelemetry.Metrics.AggregationTemporality.Cumulative = 1 -> OpenTelemetry.Metrics.AggregationTemporality +OpenTelemetry.Metrics.AggregationTemporality.Delta = 2 -> OpenTelemetry.Metrics.AggregationTemporality +OpenTelemetry.Metrics.AggregationTemporalityAttribute +OpenTelemetry.Metrics.AggregationTemporalityAttribute.AggregationTemporalityAttribute(OpenTelemetry.Metrics.AggregationTemporality temporality) -> void +OpenTelemetry.Metrics.AggregationTemporalityAttribute.Temporality.get -> OpenTelemetry.Metrics.AggregationTemporality +OpenTelemetry.Metrics.BaseExportingMetricReader +OpenTelemetry.Metrics.BaseExportingMetricReader.BaseExportingMetricReader(OpenTelemetry.BaseExporter exporter) -> void +OpenTelemetry.Metrics.BaseExportingMetricReader.SupportedExportModes.get -> OpenTelemetry.Metrics.ExportModes +OpenTelemetry.Metrics.HistogramBucket +OpenTelemetry.Metrics.HistogramBucket.BucketCount.get -> long +OpenTelemetry.Metrics.HistogramBucket.ExplicitBound.get -> double +OpenTelemetry.Metrics.HistogramBucket.HistogramBucket() -> void +OpenTelemetry.Metrics.HistogramBuckets +OpenTelemetry.Metrics.HistogramBuckets.Enumerator +OpenTelemetry.Metrics.HistogramBuckets.Enumerator.Current.get -> OpenTelemetry.Metrics.HistogramBucket +OpenTelemetry.Metrics.HistogramBuckets.Enumerator.Enumerator() -> void +OpenTelemetry.Metrics.HistogramBuckets.Enumerator.MoveNext() -> bool +OpenTelemetry.Metrics.HistogramBuckets.GetEnumerator() -> OpenTelemetry.Metrics.HistogramBuckets.Enumerator +OpenTelemetry.Metrics.MetricPoint.EndTime.get -> System.DateTimeOffset +OpenTelemetry.Metrics.MetricPoint.GetSumDouble() -> double +OpenTelemetry.Metrics.MetricPoint.GetSumLong() -> long +OpenTelemetry.Metrics.MetricPoint.GetGaugeLastValueDouble() -> double +OpenTelemetry.Metrics.MetricPoint.GetGaugeLastValueLong() -> long +OpenTelemetry.Metrics.MetricPoint.GetHistogramBuckets() -> OpenTelemetry.Metrics.HistogramBuckets +OpenTelemetry.Metrics.MetricPoint.StartTime.get -> System.DateTimeOffset +OpenTelemetry.Metrics.MetricPointsAccessor +OpenTelemetry.Metrics.MetricPointsAccessor.MetricPointsAccessor() -> void +OpenTelemetry.Metrics.MetricPointsAccessor.Enumerator +OpenTelemetry.Metrics.MetricPointsAccessor.Enumerator.Current.get -> OpenTelemetry.Metrics.MetricPoint +OpenTelemetry.Metrics.MetricPointsAccessor.Enumerator.Enumerator() -> void +OpenTelemetry.Metrics.MetricPointsAccessor.Enumerator.MoveNext() -> bool +OpenTelemetry.Metrics.MetricPointsAccessor.GetEnumerator() -> OpenTelemetry.Metrics.MetricPointsAccessor.Enumerator +OpenTelemetry.Metrics.ExportModes +OpenTelemetry.Metrics.ExportModes.Pull = 2 -> OpenTelemetry.Metrics.ExportModes +OpenTelemetry.Metrics.ExportModes.Push = 1 -> OpenTelemetry.Metrics.ExportModes +OpenTelemetry.Metrics.ExportModesAttribute +OpenTelemetry.Metrics.ExportModesAttribute.ExportModesAttribute(OpenTelemetry.Metrics.ExportModes supported) -> void +OpenTelemetry.Metrics.ExportModesAttribute.Supported.get -> OpenTelemetry.Metrics.ExportModes +OpenTelemetry.Metrics.ExplicitBucketHistogramConfiguration +OpenTelemetry.Metrics.ExplicitBucketHistogramConfiguration.Boundaries.get -> double[] +OpenTelemetry.Metrics.ExplicitBucketHistogramConfiguration.Boundaries.set -> void +OpenTelemetry.Metrics.ExplicitBucketHistogramConfiguration.ExplicitBucketHistogramConfiguration() -> void +OpenTelemetry.Metrics.IPullMetricExporter +OpenTelemetry.Metrics.IPullMetricExporter.Collect.get -> System.Func +OpenTelemetry.Metrics.IPullMetricExporter.Collect.set -> void +OpenTelemetry.Metrics.MeterProviderBuilderBase +OpenTelemetry.Metrics.MeterProviderBuilderBase.Build() -> OpenTelemetry.Metrics.MeterProvider +OpenTelemetry.Metrics.MeterProviderBuilderBase.MeterProviderBuilderBase() -> void +OpenTelemetry.Metrics.MeterProviderBuilderExtensions +OpenTelemetry.Metrics.MeterProviderExtensions +OpenTelemetry.Metrics.Metric +OpenTelemetry.Metrics.Metric.Description.get -> string +OpenTelemetry.Metrics.Metric.GetMetricPoints() -> OpenTelemetry.Metrics.MetricPointsAccessor +OpenTelemetry.Metrics.Metric.Meter.get -> System.Diagnostics.Metrics.Meter +OpenTelemetry.Metrics.Metric.MetricType.get -> OpenTelemetry.Metrics.MetricType +OpenTelemetry.Metrics.Metric.Name.get -> string +OpenTelemetry.Metrics.Metric.Temporality.get -> OpenTelemetry.Metrics.AggregationTemporality +OpenTelemetry.Metrics.Metric.Unit.get -> string +OpenTelemetry.Metrics.MetricPoint +OpenTelemetry.Metrics.MetricPoint.GetHistogramCount() -> long +OpenTelemetry.Metrics.MetricPoint.GetHistogramSum() -> double +OpenTelemetry.Metrics.MetricPoint.MetricPoint() -> void +OpenTelemetry.Metrics.MetricPoint.Tags.get -> OpenTelemetry.ReadOnlyTagCollection +OpenTelemetry.Metrics.MetricReader +OpenTelemetry.Metrics.MetricReader.Collect(int timeoutMilliseconds = -1) -> bool +OpenTelemetry.Metrics.MetricReader.Dispose() -> void +OpenTelemetry.Metrics.MetricReader.MetricReader() -> void +OpenTelemetry.Metrics.MetricReader.ParentProvider.get -> OpenTelemetry.BaseProvider +OpenTelemetry.Metrics.MetricReader.Shutdown(int timeoutMilliseconds = -1) -> bool +OpenTelemetry.Metrics.MetricReader.Temporality.get -> OpenTelemetry.Metrics.AggregationTemporality +OpenTelemetry.Metrics.MetricReader.Temporality.set -> void +OpenTelemetry.Metrics.MetricReaderType +OpenTelemetry.Metrics.MetricReaderType.Manual = 0 -> OpenTelemetry.Metrics.MetricReaderType +OpenTelemetry.Metrics.MetricReaderType.Periodic = 1 -> OpenTelemetry.Metrics.MetricReaderType +OpenTelemetry.Metrics.MetricStreamConfiguration +OpenTelemetry.Metrics.MetricStreamConfiguration.Description.get -> string +OpenTelemetry.Metrics.MetricStreamConfiguration.Description.set -> void +OpenTelemetry.Metrics.MetricStreamConfiguration.MetricStreamConfiguration() -> void +OpenTelemetry.Metrics.MetricStreamConfiguration.Name.get -> string +OpenTelemetry.Metrics.MetricStreamConfiguration.Name.set -> void +OpenTelemetry.Metrics.MetricStreamConfiguration.TagKeys.get -> string[] +OpenTelemetry.Metrics.MetricStreamConfiguration.TagKeys.set -> void +OpenTelemetry.Metrics.MetricType +OpenTelemetry.Metrics.MetricType.DoubleGauge = 45 -> OpenTelemetry.Metrics.MetricType +OpenTelemetry.Metrics.MetricType.DoubleSum = 29 -> OpenTelemetry.Metrics.MetricType +OpenTelemetry.Metrics.MetricType.Histogram = 64 -> OpenTelemetry.Metrics.MetricType +OpenTelemetry.Metrics.MetricType.LongGauge = 42 -> OpenTelemetry.Metrics.MetricType +OpenTelemetry.Metrics.MetricType.LongSum = 26 -> OpenTelemetry.Metrics.MetricType +OpenTelemetry.Metrics.MetricTypeExtensions +OpenTelemetry.Metrics.PeriodicExportingMetricReader +OpenTelemetry.Metrics.PeriodicExportingMetricReader.PeriodicExportingMetricReader(OpenTelemetry.BaseExporter exporter, int exportIntervalMilliseconds = 60000, int exportTimeoutMilliseconds = 30000) -> void +OpenTelemetry.Metrics.PeriodicExportingMetricReaderOptions +OpenTelemetry.Metrics.PeriodicExportingMetricReaderOptions.PeriodicExportingMetricReaderOptions() -> void +OpenTelemetry.Metrics.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds.get -> int +OpenTelemetry.Metrics.PeriodicExportingMetricReaderOptions.ExportIntervalMilliseconds.set -> void +OpenTelemetry.ReadOnlyTagCollection +OpenTelemetry.ReadOnlyTagCollection.Count.get -> int +OpenTelemetry.ReadOnlyTagCollection.Enumerator +OpenTelemetry.ReadOnlyTagCollection.Enumerator.Current.get -> System.Collections.Generic.KeyValuePair +OpenTelemetry.ReadOnlyTagCollection.Enumerator.Enumerator() -> void +OpenTelemetry.ReadOnlyTagCollection.Enumerator.MoveNext() -> bool +OpenTelemetry.ReadOnlyTagCollection.GetEnumerator() -> OpenTelemetry.ReadOnlyTagCollection.Enumerator +OpenTelemetry.ReadOnlyTagCollection.ReadOnlyTagCollection() -> void +OpenTelemetry.Trace.BatchExportActivityProcessorOptions +OpenTelemetry.Trace.BatchExportActivityProcessorOptions.BatchExportActivityProcessorOptions() -> void +override OpenTelemetry.BaseExportProcessor.OnForceFlush(int timeoutMilliseconds) -> bool +override OpenTelemetry.BatchExportProcessor.Dispose(bool disposing) -> void +override OpenTelemetry.Metrics.BaseExportingMetricReader.Dispose(bool disposing) -> void +override OpenTelemetry.Metrics.BaseExportingMetricReader.OnCollect(int timeoutMilliseconds) -> bool +override OpenTelemetry.Metrics.BaseExportingMetricReader.OnShutdown(int timeoutMilliseconds) -> bool +override OpenTelemetry.Metrics.MeterProviderBuilderBase.AddInstrumentation(System.Func instrumentationFactory) -> OpenTelemetry.Metrics.MeterProviderBuilder +override OpenTelemetry.Metrics.MeterProviderBuilderBase.AddMeter(params string[] names) -> OpenTelemetry.Metrics.MeterProviderBuilder +override OpenTelemetry.Metrics.PeriodicExportingMetricReader.Dispose(bool disposing) -> void +override OpenTelemetry.Metrics.PeriodicExportingMetricReader.OnShutdown(int timeoutMilliseconds) -> bool +readonly OpenTelemetry.Metrics.BaseExportingMetricReader.exporter -> OpenTelemetry.BaseExporter +static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddReader(this OpenTelemetry.Metrics.MeterProviderBuilder meterProviderBuilder, OpenTelemetry.Metrics.MetricReader reader) -> OpenTelemetry.Metrics.MeterProviderBuilder +static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddView(this OpenTelemetry.Metrics.MeterProviderBuilder meterProviderBuilder, string instrumentName, OpenTelemetry.Metrics.MetricStreamConfiguration metricStreamConfiguration) -> OpenTelemetry.Metrics.MeterProviderBuilder +static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddView(this OpenTelemetry.Metrics.MeterProviderBuilder meterProviderBuilder, string instrumentName, string name) -> OpenTelemetry.Metrics.MeterProviderBuilder +static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.AddView(this OpenTelemetry.Metrics.MeterProviderBuilder meterProviderBuilder, System.Func viewConfig) -> OpenTelemetry.Metrics.MeterProviderBuilder +static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.Build(this OpenTelemetry.Metrics.MeterProviderBuilder meterProviderBuilder) -> OpenTelemetry.Metrics.MeterProvider +static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.SetMaxMetricPointsPerMetricStream(this OpenTelemetry.Metrics.MeterProviderBuilder meterProviderBuilder, int maxMetricPointsPerMetricStream) -> OpenTelemetry.Metrics.MeterProviderBuilder +static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.SetMaxMetricStreams(this OpenTelemetry.Metrics.MeterProviderBuilder meterProviderBuilder, int maxMetricStreams) -> OpenTelemetry.Metrics.MeterProviderBuilder +static OpenTelemetry.Metrics.MeterProviderBuilderExtensions.SetResourceBuilder(this OpenTelemetry.Metrics.MeterProviderBuilder meterProviderBuilder, OpenTelemetry.Resources.ResourceBuilder resourceBuilder) -> OpenTelemetry.Metrics.MeterProviderBuilder +static OpenTelemetry.Metrics.MeterProviderExtensions.ForceFlush(this OpenTelemetry.Metrics.MeterProvider provider, int timeoutMilliseconds = -1) -> bool +static OpenTelemetry.Metrics.MeterProviderExtensions.Shutdown(this OpenTelemetry.Metrics.MeterProvider provider, int timeoutMilliseconds = -1) -> bool +static OpenTelemetry.Metrics.MeterProviderExtensions.TryFindExporter(this OpenTelemetry.Metrics.MeterProvider provider, out T exporter) -> bool +static OpenTelemetry.Metrics.MetricTypeExtensions.IsDouble(this OpenTelemetry.Metrics.MetricType self) -> bool +static OpenTelemetry.Metrics.MetricTypeExtensions.IsGauge(this OpenTelemetry.Metrics.MetricType self) -> bool +static OpenTelemetry.Metrics.MetricTypeExtensions.IsHistogram(this OpenTelemetry.Metrics.MetricType self) -> bool +static OpenTelemetry.Metrics.MetricTypeExtensions.IsLong(this OpenTelemetry.Metrics.MetricType self) -> bool +static OpenTelemetry.Metrics.MetricTypeExtensions.IsSum(this OpenTelemetry.Metrics.MetricType self) -> bool +static OpenTelemetry.Sdk.CreateMeterProviderBuilder() -> OpenTelemetry.Metrics.MeterProviderBuilder +static readonly OpenTelemetry.Metrics.MetricStreamConfiguration.Drop -> OpenTelemetry.Metrics.MetricStreamConfiguration +virtual OpenTelemetry.BaseExporter.OnForceFlush(int timeoutMilliseconds) -> bool +virtual OpenTelemetry.Metrics.MetricReader.Dispose(bool disposing) -> void +virtual OpenTelemetry.Metrics.MetricReader.OnCollect(int timeoutMilliseconds) -> bool +virtual OpenTelemetry.Metrics.MetricReader.OnShutdown(int timeoutMilliseconds) -> bool diff --git a/src/OpenTelemetry/AssemblyInfo.cs b/src/OpenTelemetry/AssemblyInfo.cs index d6dd47bc2c9..8d6f5f821b6 100644 --- a/src/OpenTelemetry/AssemblyInfo.cs +++ b/src/OpenTelemetry/AssemblyInfo.cs @@ -20,7 +20,3 @@ [assembly: InternalsVisibleTo("OpenTelemetry.Extensions.Hosting.Tests" + AssemblyInfo.PublicKey)] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2" + AssemblyInfo.MoqPublicKey)] [assembly: InternalsVisibleTo("Benchmarks" + AssemblyInfo.PublicKey)] - -// TODO: Much of the metrics SDK is currently internal. These should be removed once the public API surface area for metrics is defined. -[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.OpenTelemetryProtocol" + AssemblyInfo.PublicKey)] -[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests" + AssemblyInfo.PublicKey)] diff --git a/src/OpenTelemetry/BaseExportProcessor.cs b/src/OpenTelemetry/BaseExportProcessor.cs index 2d0a78ff467..de8678524f0 100644 --- a/src/OpenTelemetry/BaseExportProcessor.cs +++ b/src/OpenTelemetry/BaseExportProcessor.cs @@ -35,7 +35,9 @@ public abstract class BaseExportProcessor : BaseProcessor /// Exporter instance. protected BaseExportProcessor(BaseExporter exporter) { - this.exporter = exporter ?? throw new ArgumentNullException(nameof(exporter)); + Guard.ThrowIfNull(exporter, nameof(exporter)); + + this.exporter = exporter; } /// @@ -57,6 +59,12 @@ internal override void SetParentProvider(BaseProvider parentProvider) protected abstract void OnExport(T data); + /// + protected override bool OnForceFlush(int timeoutMilliseconds) + { + return this.exporter.ForceFlush(timeoutMilliseconds); + } + /// protected override bool OnShutdown(int timeoutMilliseconds) { @@ -66,21 +74,24 @@ protected override bool OnShutdown(int timeoutMilliseconds) /// protected override void Dispose(bool disposing) { - base.Dispose(disposing); - - if (disposing && !this.disposed) + if (!this.disposed) { - try - { - this.exporter.Dispose(); - } - catch (Exception ex) + if (disposing) { - OpenTelemetrySdkEventSource.Log.SpanProcessorException(nameof(this.Dispose), ex); + try + { + this.exporter.Dispose(); + } + catch (Exception ex) + { + OpenTelemetrySdkEventSource.Log.SpanProcessorException(nameof(this.Dispose), ex); + } } this.disposed = true; } + + base.Dispose(disposing); } } } diff --git a/src/OpenTelemetry/BaseExporter.cs b/src/OpenTelemetry/BaseExporter.cs index 9a281063225..cb83c28ac93 100644 --- a/src/OpenTelemetry/BaseExporter.cs +++ b/src/OpenTelemetry/BaseExporter.cs @@ -57,18 +57,50 @@ public abstract class BaseExporter : IDisposable /// Result of the export operation. public abstract ExportResult Export(in Batch batch); + /// + /// Flushes the exporter, blocks the current thread until flush + /// completed, shutdown signaled or timed out. + /// + /// + /// The number (non-negative) of milliseconds to wait, or + /// Timeout.Infinite to wait indefinitely. + /// + /// + /// Returns true when flush succeeded; otherwise, false. + /// + /// + /// Thrown when the timeoutMilliseconds is smaller than -1. + /// + /// + /// This function guarantees thread-safety. + /// + public bool ForceFlush(int timeoutMilliseconds = Timeout.Infinite) + { + Guard.ThrowIfInvalidTimeout(timeoutMilliseconds, nameof(timeoutMilliseconds)); + + try + { + return this.OnForceFlush(timeoutMilliseconds); + } + catch (Exception ex) + { + OpenTelemetrySdkEventSource.Log.SpanProcessorException(nameof(this.ForceFlush), ex); + return false; + } + } + /// /// Attempts to shutdown the exporter, blocks the current thread until /// shutdown completed or timed out. /// /// - /// The number of milliseconds to wait, or Timeout.Infinite to - /// wait indefinitely. + /// The number (non-negative) of milliseconds to wait, or + /// Timeout.Infinite to wait indefinitely. /// /// /// Returns true when shutdown succeeded; otherwise, false. /// - /// + /// /// Thrown when the timeoutMilliseconds is smaller than -1. /// /// @@ -77,10 +109,7 @@ public abstract class BaseExporter : IDisposable /// public bool Shutdown(int timeoutMilliseconds = Timeout.Infinite) { - if (timeoutMilliseconds < 0 && timeoutMilliseconds != Timeout.Infinite) - { - throw new ArgumentOutOfRangeException(nameof(timeoutMilliseconds), timeoutMilliseconds, "timeoutMilliseconds should be non-negative."); - } + Guard.ThrowIfInvalidTimeout(timeoutMilliseconds, nameof(timeoutMilliseconds)); if (Interlocked.Increment(ref this.shutdownCount) > 1) { @@ -105,13 +134,34 @@ public void Dispose() GC.SuppressFinalize(this); } + /// + /// Called by ForceFlush. This function should block the current + /// thread until flush completed, shutdown signaled or timed out. + /// + /// + /// The number (non-negative) of milliseconds to wait, or + /// Timeout.Infinite to wait indefinitely. + /// + /// + /// Returns true when flush succeeded; otherwise, false. + /// + /// + /// This function is called synchronously on the thread which called + /// ForceFlush. This function should be thread-safe, and should + /// not throw exceptions. + /// + protected virtual bool OnForceFlush(int timeoutMilliseconds) + { + return true; + } + /// /// Called by Shutdown. This function should block the current /// thread until shutdown completed or timed out. /// /// - /// The number of milliseconds to wait, or Timeout.Infinite to - /// wait indefinitely. + /// The number (non-negative) of milliseconds to wait, or + /// Timeout.Infinite to wait indefinitely. /// /// /// Returns true when shutdown succeeded; otherwise, false. diff --git a/src/OpenTelemetry/BaseProcessor.cs b/src/OpenTelemetry/BaseProcessor.cs index e8cf98ae0e3..bf7ba828acf 100644 --- a/src/OpenTelemetry/BaseProcessor.cs +++ b/src/OpenTelemetry/BaseProcessor.cs @@ -68,13 +68,13 @@ public virtual void OnEnd(T data) /// completed, shutdown signaled or timed out. /// /// - /// The number of milliseconds to wait, or Timeout.Infinite to - /// wait indefinitely. + /// The number (non-negative) of milliseconds to wait, or + /// Timeout.Infinite to wait indefinitely. /// /// /// Returns true when flush succeeded; otherwise, false. /// - /// + /// /// Thrown when the timeoutMilliseconds is smaller than -1. /// /// @@ -82,10 +82,7 @@ public virtual void OnEnd(T data) /// public bool ForceFlush(int timeoutMilliseconds = Timeout.Infinite) { - if (timeoutMilliseconds < 0 && timeoutMilliseconds != Timeout.Infinite) - { - throw new ArgumentOutOfRangeException(nameof(timeoutMilliseconds), timeoutMilliseconds, "timeoutMilliseconds should be non-negative."); - } + Guard.ThrowIfInvalidTimeout(timeoutMilliseconds, nameof(timeoutMilliseconds)); try { @@ -103,13 +100,13 @@ public bool ForceFlush(int timeoutMilliseconds = Timeout.Infinite) /// shutdown completed or timed out. /// /// - /// The number of milliseconds to wait, or Timeout.Infinite to - /// wait indefinitely. + /// The number (non-negative) of milliseconds to wait, or + /// Timeout.Infinite to wait indefinitely. /// /// /// Returns true when shutdown succeeded; otherwise, false. /// - /// + /// /// Thrown when the timeoutMilliseconds is smaller than -1. /// /// @@ -118,12 +115,9 @@ public bool ForceFlush(int timeoutMilliseconds = Timeout.Infinite) /// public bool Shutdown(int timeoutMilliseconds = Timeout.Infinite) { - if (timeoutMilliseconds < 0 && timeoutMilliseconds != Timeout.Infinite) - { - throw new ArgumentOutOfRangeException(nameof(timeoutMilliseconds), timeoutMilliseconds, "timeoutMilliseconds should be non-negative."); - } + Guard.ThrowIfInvalidTimeout(timeoutMilliseconds, nameof(timeoutMilliseconds)); - if (Interlocked.Increment(ref this.shutdownCount) > 1) + if (Interlocked.CompareExchange(ref this.shutdownCount, 1, 0) != 0) { return false; // shutdown already called } @@ -156,8 +150,8 @@ internal virtual void SetParentProvider(BaseProvider parentProvider) /// thread until flush completed, shutdown signaled or timed out. /// /// - /// The number of milliseconds to wait, or Timeout.Infinite to - /// wait indefinitely. + /// The number (non-negative) of milliseconds to wait, or + /// Timeout.Infinite to wait indefinitely. /// /// /// Returns true when flush succeeded; otherwise, false. @@ -177,8 +171,8 @@ protected virtual bool OnForceFlush(int timeoutMilliseconds) /// thread until shutdown completed or timed out. /// /// - /// The number of milliseconds to wait, or Timeout.Infinite to - /// wait indefinitely. + /// The number (non-negative) of milliseconds to wait, or + /// Timeout.Infinite to wait indefinitely. /// /// /// Returns true when shutdown succeeded; otherwise, false. diff --git a/src/OpenTelemetry/Batch.cs b/src/OpenTelemetry/Batch.cs index 6f81c75e62c..a77e5c973c5 100644 --- a/src/OpenTelemetry/Batch.cs +++ b/src/OpenTelemetry/Batch.cs @@ -31,24 +31,54 @@ namespace OpenTelemetry { private readonly T item; private readonly CircularBuffer circularBuffer; + private readonly T[] items; private readonly long targetCount; + /// + /// Initializes a new instance of the struct. + /// + /// The items to store in the batch. + /// The number of items in the batch. + public Batch(T[] items, int count) + { + Guard.ThrowIfNull(items, nameof(items)); + Guard.ThrowIfOutOfRange(count, nameof(count), 0, items.Length); + + this.item = null; + this.circularBuffer = null; + this.items = items; + this.Count = this.targetCount = count; + } + internal Batch(T item) { - this.item = item ?? throw new ArgumentNullException(nameof(item)); + Debug.Assert(item != null, $"{nameof(item)} was null."); + + this.item = item; this.circularBuffer = null; - this.targetCount = 1; + this.items = null; + this.Count = this.targetCount = 1; } internal Batch(CircularBuffer circularBuffer, int maxSize) { Debug.Assert(maxSize > 0, $"{nameof(maxSize)} should be a positive number."); + Debug.Assert(circularBuffer != null, $"{nameof(circularBuffer)} was null."); this.item = null; - this.circularBuffer = circularBuffer ?? throw new ArgumentNullException(nameof(circularBuffer)); - this.targetCount = circularBuffer.RemovedCount + Math.Min(maxSize, circularBuffer.Count); + this.items = null; + this.circularBuffer = circularBuffer; + this.Count = Math.Min(maxSize, circularBuffer.Count); + this.targetCount = circularBuffer.RemovedCount + this.Count; } + private delegate bool BatchEnumeratorMoveNextFunc(ref Enumerator enumerator); + + /// + /// Gets the count of items in the batch. + /// + public long Count { get; } + /// public void Dispose() { @@ -70,7 +100,10 @@ public Enumerator GetEnumerator() { return this.circularBuffer != null ? new Enumerator(this.circularBuffer, this.targetCount) - : new Enumerator(this.item); + : this.item != null + ? new Enumerator(this.item) + /* In the event someone uses default/new Batch() to create Batch we fallback to empty items mode. */ + : new Enumerator(this.items ?? Array.Empty(), this.targetCount); } /// @@ -78,21 +111,80 @@ public Enumerator GetEnumerator() /// public struct Enumerator : IEnumerator { + private static readonly BatchEnumeratorMoveNextFunc MoveNextSingleItem = (ref Enumerator enumerator) => + { + if (enumerator.targetCount >= 0) + { + enumerator.Current = null; + return false; + } + + enumerator.targetCount++; + return true; + }; + + private static readonly BatchEnumeratorMoveNextFunc MoveNextCircularBuffer = (ref Enumerator enumerator) => + { + var circularBuffer = enumerator.circularBuffer; + + if (circularBuffer.RemovedCount < enumerator.targetCount) + { + enumerator.Current = circularBuffer.Read(); + return true; + } + + enumerator.Current = null; + return false; + }; + + private static readonly BatchEnumeratorMoveNextFunc MoveNextArray = (ref Enumerator enumerator) => + { + var items = enumerator.items; + + if (enumerator.itemIndex < enumerator.targetCount) + { + enumerator.Current = items[enumerator.itemIndex++]; + return true; + } + + enumerator.Current = null; + return false; + }; + private readonly CircularBuffer circularBuffer; + private readonly T[] items; + private readonly BatchEnumeratorMoveNextFunc moveNextFunc; private long targetCount; + private int itemIndex; internal Enumerator(T item) { this.Current = item; this.circularBuffer = null; + this.items = null; this.targetCount = -1; + this.itemIndex = 0; + this.moveNextFunc = MoveNextSingleItem; } internal Enumerator(CircularBuffer circularBuffer, long targetCount) { this.Current = null; + this.items = null; this.circularBuffer = circularBuffer; this.targetCount = targetCount; + this.itemIndex = 0; + this.moveNextFunc = MoveNextCircularBuffer; + } + + internal Enumerator(T[] items, long targetCount) + { + this.Current = null; + this.circularBuffer = null; + this.items = items; + this.targetCount = targetCount; + this.itemIndex = 0; + this.moveNextFunc = MoveNextArray; } /// @@ -109,28 +201,7 @@ public void Dispose() /// public bool MoveNext() { - var circularBuffer = this.circularBuffer; - - if (circularBuffer == null) - { - if (this.targetCount >= 0) - { - this.Current = null; - return false; - } - - this.targetCount++; - return true; - } - - if (circularBuffer.RemovedCount < this.targetCount) - { - this.Current = circularBuffer.Read(); - return true; - } - - this.Current = null; - return false; + return this.moveNextFunc(ref this); } /// diff --git a/src/OpenTelemetry/BatchExportProcessor.cs b/src/OpenTelemetry/BatchExportProcessor.cs index 94a529352ad..0e6a532e2f2 100644 --- a/src/OpenTelemetry/BatchExportProcessor.cs +++ b/src/OpenTelemetry/BatchExportProcessor.cs @@ -43,6 +43,7 @@ public abstract class BatchExportProcessor : BaseExportProcessor private readonly ManualResetEvent shutdownTrigger = new ManualResetEvent(false); private long shutdownDrainTarget = long.MaxValue; private long droppedCount; + private bool disposed; /// /// Initializes a new instance of the class. @@ -60,25 +61,10 @@ protected BatchExportProcessor( int maxExportBatchSize = DefaultMaxExportBatchSize) : base(exporter) { - if (maxQueueSize <= 0) - { - throw new ArgumentOutOfRangeException(nameof(maxQueueSize), maxQueueSize, "maxQueueSize should be greater than zero."); - } - - if (maxExportBatchSize <= 0 || maxExportBatchSize > maxQueueSize) - { - throw new ArgumentOutOfRangeException(nameof(maxExportBatchSize), maxExportBatchSize, "maxExportBatchSize should be greater than zero and less than maxQueueSize."); - } - - if (scheduledDelayMilliseconds <= 0) - { - throw new ArgumentOutOfRangeException(nameof(scheduledDelayMilliseconds), scheduledDelayMilliseconds, "scheduledDelayMilliseconds should be greater than zero."); - } - - if (exporterTimeoutMilliseconds < 0) - { - throw new ArgumentOutOfRangeException(nameof(exporterTimeoutMilliseconds), exporterTimeoutMilliseconds, "exporterTimeoutMilliseconds should be non-negative."); - } + Guard.ThrowIfOutOfRange(maxQueueSize, nameof(maxQueueSize), min: 1); + Guard.ThrowIfOutOfRange(maxExportBatchSize, nameof(maxExportBatchSize), min: 1, max: maxQueueSize, maxName: nameof(maxQueueSize)); + Guard.ThrowIfOutOfRange(scheduledDelayMilliseconds, nameof(scheduledDelayMilliseconds), min: 1); + Guard.ThrowIfOutOfRange(exporterTimeoutMilliseconds, nameof(exporterTimeoutMilliseconds), min: 0); this.circularBuffer = new CircularBuffer(maxQueueSize); this.scheduledDelayMilliseconds = scheduledDelayMilliseconds; @@ -144,7 +130,9 @@ protected override bool OnForceFlush(int timeoutMilliseconds) var triggers = new WaitHandle[] { this.dataExportedNotification, this.shutdownTrigger }; - var sw = Stopwatch.StartNew(); + var sw = timeoutMilliseconds == Timeout.Infinite + ? null + : Stopwatch.StartNew(); // There is a chance that the export thread finished processing all the data from the queue, // and signaled before we enter wait here, use polling to prevent being blocked indefinitely. @@ -152,9 +140,16 @@ protected override bool OnForceFlush(int timeoutMilliseconds) while (true) { - if (timeoutMilliseconds == Timeout.Infinite) + if (sw == null) { - WaitHandle.WaitAny(triggers, pollingMilliseconds); + try + { + WaitHandle.WaitAny(triggers, pollingMilliseconds); + } + catch (ObjectDisposedException) + { + return false; + } } else { @@ -165,7 +160,14 @@ protected override bool OnForceFlush(int timeoutMilliseconds) return this.circularBuffer.RemovedCount >= head; } - WaitHandle.WaitAny(triggers, Math.Min((int)timeout, pollingMilliseconds)); + try + { + WaitHandle.WaitAny(triggers, Math.Min((int)timeout, pollingMilliseconds)); + } + catch (ObjectDisposedException) + { + return false; + } } if (this.circularBuffer.RemovedCount >= head) @@ -186,6 +188,8 @@ protected override bool OnShutdown(int timeoutMilliseconds) this.shutdownDrainTarget = this.circularBuffer.AddedCount; this.shutdownTrigger.Set(); + OpenTelemetrySdkEventSource.Log.DroppedExportProcessorItems(this.GetType().Name, this.exporter.GetType().Name, this.droppedCount); + if (timeoutMilliseconds == Timeout.Infinite) { this.exporterThread.Join(); @@ -203,6 +207,24 @@ protected override bool OnShutdown(int timeoutMilliseconds) return this.exporter.Shutdown((int)Math.Max(timeout, 0)); } + /// + protected override void Dispose(bool disposing) + { + if (!this.disposed) + { + if (disposing) + { + this.exportTrigger.Dispose(); + this.dataExportedNotification.Dispose(); + this.shutdownTrigger.Dispose(); + } + + this.disposed = true; + } + + base.Dispose(disposing); + } + private void ExporterProc() { var triggers = new WaitHandle[] { this.exportTrigger, this.shutdownTrigger }; @@ -212,7 +234,14 @@ private void ExporterProc() // only wait when the queue doesn't have enough items, otherwise keep busy and send data continuously if (this.circularBuffer.Count < this.maxExportBatchSize) { - WaitHandle.WaitAny(triggers, this.scheduledDelayMilliseconds); + try + { + WaitHandle.WaitAny(triggers, this.scheduledDelayMilliseconds); + } + catch (ObjectDisposedException) + { + return; + } } if (this.circularBuffer.Count > 0) @@ -228,7 +257,7 @@ private void ExporterProc() if (this.circularBuffer.RemovedCount >= this.shutdownDrainTarget) { - break; + return; } } } diff --git a/src/OpenTelemetry/CHANGELOG.md b/src/OpenTelemetry/CHANGELOG.md index f3d9fd1eb9f..db0d25f5b90 100644 --- a/src/OpenTelemetry/CHANGELOG.md +++ b/src/OpenTelemetry/CHANGELOG.md @@ -2,18 +2,151 @@ ## Unreleased -* `ResourceBuilder.CreateDefault` has detectors for - `OTEL_RESOURCE_ATTRIBUTES`, `OTEL_SERVICE_NAME` environment variables - so that explicit `AddEnvironmentVariableDetector` call is not needed. ([#2247](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2247)) +* Make `MetricPoint` of `MetricPointAccessor` readonly. + ([2736](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2736)) + +* Fail-fast when using AddView with guaranteed conflict. + ([2751](https://github.com/open-telemetry/opentelemetry-dotnet/issues/2751)) + +## 1.2.0-rc1 + +Released 2021-Nov-29 + +* Prevent accessing activity Id before sampler runs in case of legacy + activities. + ([2659](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2659)) + +* Added `ReadOnlyTagCollection` and expose `Tags` on `MetricPoint` instead of + `Keys`+`Values` + ([#2642](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2642)) + +* Refactored `MetricPoint` and added public methods: `GetBucketCounts`, + `GetExplicitBounds`, `GetHistogramCount`, and `GetHistogramSum` + ([#2657](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2657)) + +* Remove MetricStreamConfiguration.Aggregation, as the feature to customize + aggregation is not implemented yet. + ([#2660](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2660)) + +* Removed the public property `HistogramMeasurements` and added a public method + `GetHistogramBuckets` instead. Renamed the class `HistogramMeasurements` to + `HistogramBuckets` and added an enumerator of type `HistogramBucket` for + enumerating `BucketCounts` and `ExplicitBounds`. Removed `GetBucketCounts` and + `GetExplicitBounds` methods from `MetricPoint`. + ([#2664](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2664)) + +* Refactored temporality setting to align with the latest spec. + ([#2666](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2666)) + +* Removed the public properties `LongValue`, `DoubleValue`, in favor of their + counterpart public methods `GetSumLong`, `GetSumDouble`, + `GetGaugeLastValueLong`, `GetGaugeLastValueDouble`. + ([#2667](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2667)) + +* MetricType modified to reserve bits for future types. + ([#2693](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2693)) + +## 1.2.0-beta2 + +Released 2021-Nov-19 + +* Renamed `HistogramConfiguration` to `ExplicitBucketHistogramConfiguration` and + changed its member `BucketBounds` to `Boundaries`. + ([#2638](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2638)) + +* Metrics with the same name but from different meters are allowed. + ([#2634](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2634)) + +* Metrics SDK will not provide inactive Metrics to delta exporter. + ([#2629](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2629)) + +* Histogram bounds are validated when added to a View. + ([#2573](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2573)) + +* Changed `BatchExportActivityProcessorOptions` constructor to throw + `FormatException` if it fails to parse any of the supported environment + variables. + +* Added `BaseExporter.ForceFlush`. + ([#2525](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2525)) + +* Exposed public `Batch(T[] items, int count)` constructor on `Batch` struct + ([#2542](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2542)) + +* Added wildcard support for AddMeter. + ([#2459](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2459)) + +* Add support for multiple Metric readers + ([#2596](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2596)) + +* Add ability to configure MaxMetricStreams, MaxMetricPointsPerMetricStream + ([#2635](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2635)) + +## 1.2.0-beta1 + +Released 2021-Oct-08 + +* Exception from Observable instrument callbacks does not result in entire + metrics being lost. + +* SDK is allocation-free on recording of measurements with upto 8 tags. + +* TracerProviderBuilder.AddLegacySource now supports wildcard activity names. + ([#2183](https://github.com/open-telemetry/opentelemetry-dotnet/issues/2183)) + +* Instrument and View names are validated [according with the + spec](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/api.md#instrument). + ([#2470](https://github.com/open-telemetry/opentelemetry-dotnet/issues/2470)) + +## 1.2.0-alpha4 + +Released 2021-Sep-23 + +* `BatchExportProcessor.OnShutdown` will now log the count of dropped telemetry + items. + ([#2331](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2331)) +* Changed `CompositeProcessor.OnForceFlush` to meet with the spec + requirement. Now the SDK will invoke `ForceFlush` on all registered + processors, even if there is a timeout. + ([#2388](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2388)) + +## 1.2.0-alpha3 + +Released 2021-Sep-13 + +* Metrics perf improvements, bug fixes. Replace MetricProcessor with + MetricReader. + ([#2306](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2306)) + +* Add `BatchExportActivityProcessorOptions` which supports field value + overriding using `OTEL_BSP_SCHEDULE_DELAY`, `OTEL_BSP_EXPORT_TIMEOUT`, + `OTEL_BSP_MAX_QUEUE_SIZE`, `OTEL_BSP_MAX_EXPORT_BATCH_SIZE` envionmental + variables as defined in the + [specification](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.5.0/specification/sdk-environment-variables.md#batch-span-processor). + ([#2219](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2219)) + +## 1.2.0-alpha2 + +Released 2021-Aug-24 + +* More Metrics features. All instrument types, push/pull exporters, + Delta/Cumulative temporality supported. + +* `ResourceBuilder.CreateDefault` has detectors for `OTEL_RESOURCE_ATTRIBUTES`, + `OTEL_SERVICE_NAME` environment variables so that explicit + `AddEnvironmentVariableDetector` call is not needed. + ([#2247](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2247)) * `ResourceBuilder.AddEnvironmentVariableDetector` handles `OTEL_SERVICE_NAME` - environmental variable. ([#2209](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2209)) + environmental variable. + ([#2209](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2209)) -* Removes upper constraint for Microsoft.Extensions.Logging - dependencies. ([#2179](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2179)) +* Removes upper constraint for Microsoft.Extensions.Logging dependencies. + ([#2179](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2179)) -* OpenTelemetryLogger modified to not throw, when the - formatter supplied in ILogger.Log call is null. ([#2200](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2200)) +* OpenTelemetryLogger modified to not throw, when the formatter supplied in + ILogger.Log call is null. + ([#2200](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2200)) ## 1.2.0-alpha1 @@ -24,7 +157,8 @@ Released 2021-Jul-23 ([#2174](https://github.com/open-telemetry/opentelemetry-dotnet/pull/2174)) * Removes .NET Framework 4.5.2, .NET 4.6 support. The minimum .NET Framework - version supported is .NET 4.6.1. ([#2138](https://github.com/open-telemetry/opentelemetry-dotnet/issues/2138)) + version supported is .NET 4.6.1. + ([#2138](https://github.com/open-telemetry/opentelemetry-dotnet/issues/2138)) ## 1.1.0 @@ -61,8 +195,8 @@ Released 2021-May-11 Released 2021-Apr-23 * Use `AssemblyFileVersionAttribute` instead of `FileVersionInfo.GetVersionInfo` - to get the SDK version attribute to ensure that it works when the assembly - is not loaded directly from a file on disk + to get the SDK version attribute to ensure that it works when the assembly is + not loaded directly from a file on disk ([#1908](https://github.com/open-telemetry/opentelemetry-dotnet/issues/1908)) ## 1.1.0-beta1 diff --git a/src/OpenTelemetry/CompositeProcessor.cs b/src/OpenTelemetry/CompositeProcessor.cs index 2b504e6432a..19260b1dd6c 100644 --- a/src/OpenTelemetry/CompositeProcessor.cs +++ b/src/OpenTelemetry/CompositeProcessor.cs @@ -24,22 +24,18 @@ namespace OpenTelemetry { public class CompositeProcessor : BaseProcessor { - private DoublyLinkedListNode head; + private readonly DoublyLinkedListNode head; private DoublyLinkedListNode tail; private bool disposed; public CompositeProcessor(IEnumerable> processors) { - if (processors == null) - { - throw new ArgumentNullException(nameof(processors)); - } + Guard.ThrowIfNull(processors, nameof(processors)); using var iter = processors.GetEnumerator(); - if (!iter.MoveNext()) { - throw new ArgumentException($"{nameof(processors)} collection is empty"); + throw new ArgumentException($"'{iter}' is null or empty", nameof(iter)); } this.head = new DoublyLinkedListNode(iter.Current); @@ -53,10 +49,7 @@ public CompositeProcessor(IEnumerable> processors) public CompositeProcessor AddProcessor(BaseProcessor processor) { - if (processor == null) - { - throw new ArgumentNullException(nameof(processor)); - } + Guard.ThrowIfNull(processor, nameof(processor)); var node = new DoublyLinkedListNode(processor) { @@ -71,73 +64,58 @@ public CompositeProcessor AddProcessor(BaseProcessor processor) /// public override void OnEnd(T data) { - var cur = this.head; - - while (cur != null) + for (var cur = this.head; cur != null; cur = cur.Next) { cur.Value.OnEnd(data); - cur = cur.Next; } } /// public override void OnStart(T data) { - var cur = this.head; - - while (cur != null) + for (var cur = this.head; cur != null; cur = cur.Next) { cur.Value.OnStart(data); - cur = cur.Next; } } /// protected override bool OnForceFlush(int timeoutMilliseconds) { - var cur = this.head; - - var sw = Stopwatch.StartNew(); + var result = true; + var sw = timeoutMilliseconds == Timeout.Infinite + ? null + : Stopwatch.StartNew(); - while (cur != null) + for (var cur = this.head; cur != null; cur = cur.Next) { - if (timeoutMilliseconds == Timeout.Infinite) + if (sw == null) { - _ = cur.Value.ForceFlush(Timeout.Infinite); + result = cur.Value.ForceFlush(Timeout.Infinite) && result; } else { var timeout = timeoutMilliseconds - sw.ElapsedMilliseconds; - if (timeout <= 0) - { - return false; - } - - var succeeded = cur.Value.ForceFlush((int)timeout); - - if (!succeeded) - { - return false; - } + // notify all the processors, even if we run overtime + result = cur.Value.ForceFlush((int)Math.Max(timeout, 0)) && result; } - - cur = cur.Next; } - return true; + return result; } /// protected override bool OnShutdown(int timeoutMilliseconds) { - var cur = this.head; var result = true; - var sw = Stopwatch.StartNew(); + var sw = timeoutMilliseconds == Timeout.Infinite + ? null + : Stopwatch.StartNew(); - while (cur != null) + for (var cur = this.head; cur != null; cur = cur.Next) { - if (timeoutMilliseconds == Timeout.Infinite) + if (sw == null) { result = cur.Value.Shutdown(Timeout.Infinite) && result; } @@ -148,40 +126,35 @@ protected override bool OnShutdown(int timeoutMilliseconds) // notify all the processors, even if we run overtime result = cur.Value.Shutdown((int)Math.Max(timeout, 0)) && result; } - - cur = cur.Next; } return result; } + /// protected override void Dispose(bool disposing) { - if (this.disposed) - { - return; - } - - if (disposing) + if (!this.disposed) { - var cur = this.head; - - while (cur != null) + if (disposing) { - try + for (var cur = this.head; cur != null; cur = cur.Next) { - cur.Value?.Dispose(); + try + { + cur.Value?.Dispose(); + } + catch (Exception ex) + { + OpenTelemetrySdkEventSource.Log.SpanProcessorException(nameof(this.Dispose), ex); + } } - catch (Exception ex) - { - OpenTelemetrySdkEventSource.Log.SpanProcessorException(nameof(this.Dispose), ex); - } - - cur = cur.Next; } + + this.disposed = true; } - this.disposed = true; + base.Dispose(disposing); } private class DoublyLinkedListNode diff --git a/src/OpenTelemetry/DiagnosticSourceInstrumentation/DiagnosticSourceListener.cs b/src/OpenTelemetry/DiagnosticSourceInstrumentation/DiagnosticSourceListener.cs index 8c5c5f3db7e..ad5852878d8 100644 --- a/src/OpenTelemetry/DiagnosticSourceInstrumentation/DiagnosticSourceListener.cs +++ b/src/OpenTelemetry/DiagnosticSourceInstrumentation/DiagnosticSourceListener.cs @@ -17,6 +17,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using OpenTelemetry.Internal; namespace OpenTelemetry.Instrumentation { @@ -26,7 +27,9 @@ internal class DiagnosticSourceListener : IObserver public DiagnosticSourceListener(ListenerHandler handler) { - this.handler = handler ?? throw new ArgumentNullException(nameof(handler)); + Guard.ThrowIfNull(handler, nameof(handler)); + + this.handler = handler; } public void OnCompleted() diff --git a/src/OpenTelemetry/DiagnosticSourceInstrumentation/DiagnosticSourceSubscriber.cs b/src/OpenTelemetry/DiagnosticSourceInstrumentation/DiagnosticSourceSubscriber.cs index 6fc3fb28553..1e1a15dce5f 100644 --- a/src/OpenTelemetry/DiagnosticSourceInstrumentation/DiagnosticSourceSubscriber.cs +++ b/src/OpenTelemetry/DiagnosticSourceInstrumentation/DiagnosticSourceSubscriber.cs @@ -17,17 +17,18 @@ using System.Collections.Generic; using System.Diagnostics; using System.Threading; +using OpenTelemetry.Internal; namespace OpenTelemetry.Instrumentation { internal class DiagnosticSourceSubscriber : IDisposable, IObserver { + private readonly List listenerSubscriptions; private readonly Func handlerFactory; private readonly Func diagnosticSourceFilter; private readonly Func isEnabledFilter; private long disposed; private IDisposable allSourcesSubscription; - private List listenerSubscriptions; public DiagnosticSourceSubscriber( ListenerHandler handler, @@ -41,8 +42,10 @@ public DiagnosticSourceSubscriber( Func diagnosticSourceFilter, Func isEnabledFilter) { + Guard.ThrowIfNull(handlerFactory, nameof(handlerFactory)); + this.listenerSubscriptions = new List(); - this.handlerFactory = handlerFactory ?? throw new ArgumentNullException(nameof(handlerFactory)); + this.handlerFactory = handlerFactory; this.diagnosticSourceFilter = diagnosticSourceFilter; this.isEnabledFilter = isEnabledFilter; } diff --git a/src/OpenTelemetry/DiagnosticSourceInstrumentation/PropertyFetcher.cs b/src/OpenTelemetry/DiagnosticSourceInstrumentation/PropertyFetcher.cs index 933805d3aec..1bcafb11dbf 100644 --- a/src/OpenTelemetry/DiagnosticSourceInstrumentation/PropertyFetcher.cs +++ b/src/OpenTelemetry/DiagnosticSourceInstrumentation/PropertyFetcher.cs @@ -17,6 +17,7 @@ using System; using System.Linq; using System.Reflection; +using OpenTelemetry.Internal; namespace OpenTelemetry.Instrumentation { @@ -45,9 +46,11 @@ public PropertyFetcher(string propertyName) /// Property fetched. public T Fetch(object obj) { - if (!this.TryFetch(obj, out T value)) + Guard.ThrowIfNull(obj, nameof(obj)); + + if (!this.TryFetch(obj, out T value, true)) { - throw new ArgumentException("Supplied object was null or did not match the expected type.", nameof(obj)); + throw new ArgumentException($"Unable to fetch property: '{nameof(obj)}'", nameof(obj)); } return value; @@ -58,10 +61,11 @@ public T Fetch(object obj) /// /// Object to be fetched. /// Fetched value. - /// if the property was fetched. - public bool TryFetch(object obj, out T value) + /// Set this to if we know is not . + /// if the property was fetched. + public bool TryFetch(object obj, out T value, bool skipObjNullCheck = false) { - if (obj == null) + if (!skipObjNullCheck && obj == null) { value = default; return false; @@ -69,14 +73,7 @@ public bool TryFetch(object obj, out T value) if (this.innerFetcher == null) { - var type = obj.GetType().GetTypeInfo(); - var property = type.DeclaredProperties.FirstOrDefault(p => string.Equals(p.Name, this.propertyName, StringComparison.InvariantCultureIgnoreCase)); - if (property == null) - { - property = type.GetProperty(this.propertyName); - } - - this.innerFetcher = PropertyFetch.FetcherForProperty(property); + this.innerFetcher = PropertyFetch.Create(obj.GetType().GetTypeInfo(), this.propertyName); } return this.innerFetcher.TryFetch(obj, out value); @@ -85,22 +82,29 @@ public bool TryFetch(object obj, out T value) // see https://github.com/dotnet/corefx/blob/master/src/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/DiagnosticSourceEventSource.cs private class PropertyFetch { - /// - /// Create a property fetcher from a .NET Reflection PropertyInfo class that - /// represents a property of a particular type. - /// - public static PropertyFetch FetcherForProperty(PropertyInfo propertyInfo) + public static PropertyFetch Create(TypeInfo type, string propertyName) { - if (propertyInfo == null || !typeof(T).IsAssignableFrom(propertyInfo.PropertyType)) + var property = type.DeclaredProperties.FirstOrDefault(p => string.Equals(p.Name, propertyName, StringComparison.InvariantCultureIgnoreCase)); + if (property == null) { - // returns null on any fetch. - return new PropertyFetch(); + property = type.GetProperty(propertyName); } - var typedPropertyFetcher = typeof(TypedPropertyFetch<,>); - var instantiatedTypedPropertyFetcher = typedPropertyFetcher.MakeGenericType( - typeof(T), propertyInfo.DeclaringType, propertyInfo.PropertyType); - return (PropertyFetch)Activator.CreateInstance(instantiatedTypedPropertyFetcher, propertyInfo); + return CreateFetcherForProperty(property); + + static PropertyFetch CreateFetcherForProperty(PropertyInfo propertyInfo) + { + if (propertyInfo == null || !typeof(T).IsAssignableFrom(propertyInfo.PropertyType)) + { + // returns null on any fetch. + return new PropertyFetch(); + } + + var typedPropertyFetcher = typeof(TypedPropertyFetch<,>); + var instantiatedTypedPropertyFetcher = typedPropertyFetcher.MakeGenericType( + typeof(T), propertyInfo.DeclaringType, propertyInfo.PropertyType); + return (PropertyFetch)Activator.CreateInstance(instantiatedTypedPropertyFetcher, propertyInfo); + } } public virtual bool TryFetch(object obj, out T value) @@ -109,13 +113,17 @@ public virtual bool TryFetch(object obj, out T value) return false; } - private class TypedPropertyFetch : PropertyFetch + private sealed class TypedPropertyFetch : PropertyFetch where TDeclaredProperty : T { + private readonly string propertyName; private readonly Func propertyFetch; + private PropertyFetch innerFetcher; + public TypedPropertyFetch(PropertyInfo property) { + this.propertyName = property.Name; this.propertyFetch = (Func)property.GetMethod.CreateDelegate(typeof(Func)); } @@ -127,8 +135,12 @@ public override bool TryFetch(object obj, out T value) return true; } - value = default; - return false; + if (this.innerFetcher == null) + { + this.innerFetcher = Create(obj.GetType().GetTypeInfo(), this.propertyName); + } + + return this.innerFetcher.TryFetch(obj, out value); } } } diff --git a/src/OpenTelemetry/Internal/CircularBuffer.cs b/src/OpenTelemetry/Internal/CircularBuffer.cs index 58f1f26dec3..8cc1f22ff2c 100644 --- a/src/OpenTelemetry/Internal/CircularBuffer.cs +++ b/src/OpenTelemetry/Internal/CircularBuffer.cs @@ -14,7 +14,6 @@ // limitations under the License. // -using System; using System.Runtime.CompilerServices; using System.Threading; @@ -37,10 +36,7 @@ internal class CircularBuffer /// The capacity of the circular buffer, must be a positive integer. public CircularBuffer(int capacity) { - if (capacity <= 0) - { - throw new ArgumentOutOfRangeException(nameof(capacity), capacity, "capacity should be greater than zero."); - } + Guard.ThrowIfOutOfRange(capacity, nameof(capacity), min: 1); this.Capacity = capacity; this.trait = new T[capacity]; @@ -83,10 +79,7 @@ public int Count /// public bool Add(T value) { - if (value == null) - { - throw new ArgumentNullException(nameof(value)); - } + Guard.ThrowIfNull(value, nameof(value)); while (true) { @@ -126,10 +119,7 @@ public bool TryAdd(T value, int maxSpinCount) return this.Add(value); } - if (value == null) - { - throw new ArgumentNullException(nameof(value)); - } + Guard.ThrowIfNull(value, nameof(value)); var spinCountDown = maxSpinCount; diff --git a/src/OpenTelemetry/Internal/EnvironmentVariableHelper.cs b/src/OpenTelemetry/Internal/EnvironmentVariableHelper.cs new file mode 100644 index 00000000000..0061000c3f0 --- /dev/null +++ b/src/OpenTelemetry/Internal/EnvironmentVariableHelper.cs @@ -0,0 +1,115 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Globalization; +using System.Security; + +namespace OpenTelemetry.Internal +{ + /// + /// EnvironmentVariableHelper facilitates parsing environment variable values as defined by + /// + /// the specification. + /// + internal static class EnvironmentVariableHelper + { + /// + /// Reads an environment variable without any parsing. + /// + /// The name of the environment variable. + /// The parsed value of the environment variable. + /// + /// Returns true when a non-empty value was read; otherwise, false. + /// + public static bool LoadString(string envVarKey, out string result) + { + result = null; + + try + { + result = Environment.GetEnvironmentVariable(envVarKey); + } + catch (SecurityException ex) + { + // The caller does not have the required permission to + // retrieve the value of an environment variable from the current process. + OpenTelemetrySdkEventSource.Log.MissingPermissionsToReadEnvironmentVariable(ex); + return false; + } + + return !string.IsNullOrEmpty(result); + } + + /// + /// Reads an environment variable and parses is as a + /// + /// numeric value - a non-negative decimal integer. + /// + /// The name of the environment variable. + /// The parsed value of the environment variable. + /// + /// Returns true when a non-empty value was read; otherwise, false. + /// + /// + /// Thrown when failed to parse the non-empty value. + /// + public static bool LoadNumeric(string envVarKey, out int result) + { + result = 0; + + if (!LoadString(envVarKey, out string value)) + { + return false; + } + + if (!int.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out result)) + { + throw new FormatException($"{envVarKey} environment variable has an invalid value: '${value}'"); + } + + return true; + } + + /// + /// Reads an environment variable and parses it as a . + /// + /// The name of the environment variable. + /// The parsed value of the environment variable. + /// + /// Returns true when a non-empty value was read; otherwise, false. + /// + /// + /// Thrown when failed to parse the non-empty value. + /// + public static bool LoadUri(string envVarKey, out Uri result) + { + result = null; + + if (!LoadString(envVarKey, out string value)) + { + return false; + } + + if (!Uri.TryCreate(value, UriKind.Absolute, out result)) + { + throw new FormatException($"{envVarKey} environment variable has an invalid value: '${value}'"); + } + + return true; + } + } +} diff --git a/src/OpenTelemetry/Internal/OpenTelemetrySdkEventSource.cs b/src/OpenTelemetry/Internal/OpenTelemetrySdkEventSource.cs index 11cbd2e696c..5143d338078 100644 --- a/src/OpenTelemetry/Internal/OpenTelemetrySdkEventSource.cs +++ b/src/OpenTelemetry/Internal/OpenTelemetrySdkEventSource.cs @@ -21,6 +21,7 @@ #endif using System.Diagnostics; using System.Diagnostics.Tracing; +using System.Security; namespace OpenTelemetry.Internal { @@ -54,38 +55,48 @@ public void TracestateExtractException(Exception ex) } [NonEvent] - public void MetricObserverCallbackException(string metricName, Exception ex) + public void MetricObserverCallbackException(Exception exception) { if (this.IsEnabled(EventLevel.Warning, EventKeywords.All)) { - this.MetricObserverCallbackError(metricName, ex.ToInvariantString()); + if (exception is AggregateException aggregateException) + { + foreach (var ex in aggregateException.InnerExceptions) + { + this.ObservableInstrumentCallbackException(ex.ToInvariantString()); + } + } + else + { + this.ObservableInstrumentCallbackException(exception.ToInvariantString()); + } } } [NonEvent] - public void TracestateKeyIsInvalid(ReadOnlySpan key) + public void MetricReaderException(string methodName, Exception ex) { - if (this.IsEnabled(EventLevel.Warning, EventKeywords.All)) + if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) { - this.TracestateKeyIsInvalid(key.ToString()); + this.MetricReaderException(methodName, ex.ToInvariantString()); } } [NonEvent] - public void TracestateValueIsInvalid(ReadOnlySpan value) + public void TracestateKeyIsInvalid(ReadOnlySpan key) { if (this.IsEnabled(EventLevel.Warning, EventKeywords.All)) { - this.TracestateValueIsInvalid(value.ToString()); + this.TracestateKeyIsInvalid(key.ToString()); } } [NonEvent] - public void MetricControllerException(Exception ex) + public void TracestateValueIsInvalid(ReadOnlySpan value) { if (this.IsEnabled(EventLevel.Warning, EventKeywords.All)) { - this.MetricControllerException(ex.ToInvariantString()); + this.TracestateValueIsInvalid(value.ToString()); } } @@ -94,7 +105,14 @@ public void ActivityStarted(Activity activity) { if (this.IsEnabled(EventLevel.Verbose, EventKeywords.All)) { - this.ActivityStarted(activity.OperationName, activity.Id); + // Accessing activity.Id here will cause the Id to be initialized + // before the sampler runs in case where the activity is created using legacy way + // i.e. new Activity("Operation name"). This will result in Id not reflecting the + // correct sampling flags + // https://github.com/dotnet/runtime/issues/61857 + var activityId = string.Concat("00-", activity.TraceId.ToHexString(), "-", activity.SpanId.ToHexString()); + activityId = string.Concat(activityId, activity.ActivityTraceFlags.HasFlag(ActivityTraceFlags.Recorded) ? "-01" : "-00"); + this.ActivityStarted(activity.OperationName, activityId); } } @@ -125,6 +143,43 @@ public void TracerProviderException(string evnt, Exception ex) } } + [NonEvent] + public void MeterProviderException(string methodName, Exception ex) + { + if (this.IsEnabled(EventLevel.Error, EventKeywords.All)) + { + this.MeterProviderException(methodName, ex.ToInvariantString()); + } + } + + [NonEvent] + public void MissingPermissionsToReadEnvironmentVariable(SecurityException ex) + { + if (this.IsEnabled(EventLevel.Warning, EventKeywords.All)) + { + this.MissingPermissionsToReadEnvironmentVariable(ex.ToInvariantString()); + } + } + + [NonEvent] + public void DroppedExportProcessorItems(string exportProcessorName, string exporterName, long droppedCount) + { + if (droppedCount > 0) + { + if (this.IsEnabled(EventLevel.Warning, EventKeywords.All)) + { + this.ExistsDroppedExportProcessorItems(exportProcessorName, exporterName, droppedCount); + } + } + else + { + if (this.IsEnabled(EventLevel.Informational, EventKeywords.All)) + { + this.NoDroppedExportProcessorItems(exportProcessorName, exporterName); + } + } + } + [Event(1, Message = "Span processor queue size reached maximum. Throttling spans.", Level = EventLevel.Warning)] public void SpanProcessorQueueIsExhausted() { @@ -209,40 +264,10 @@ public void AttemptToActivateOobSpan(string spanName) this.WriteEvent(15, spanName); } - [Event(16, Message = "Exception occurring while invoking Metric Observer callback. '{0}' Exception: '{1}'", Level = EventLevel.Warning)] - public void MetricObserverCallbackError(string metricName, string exception) + [Event(16, Message = "Exception occurred while invoking Observable instrument callback. Exception: '{0}'", Level = EventLevel.Warning)] + public void ObservableInstrumentCallbackException(string exception) { - this.WriteEvent(16, metricName, exception); - } - - [Event(17, Message = "Batcher finished collection with '{0}' metrics.", Level = EventLevel.Informational)] - public void BatcherCollectionCompleted(int count) - { - this.WriteEvent(17, count); - } - - [Event(18, Message = "Collection completed in '{0}' msecs.", Level = EventLevel.Informational)] - public void CollectionCompleted(long msec) - { - this.WriteEvent(18, msec); - } - - [Event(19, Message = "Exception occurred in Metric Controller while processing metrics from one Collect cycle. This does not shutdown controller and subsequent collections will be done. Exception: '{0}'", Level = EventLevel.Warning)] - public void MetricControllerException(string exception) - { - this.WriteEvent(19, exception); - } - - [Event(20, Message = "Meter Collect Invoked for Meter: '{0}'", Level = EventLevel.Verbose)] - public void MeterCollectInvoked(string meterName) - { - this.WriteEvent(20, meterName); - } - - [Event(21, Message = "Metric Export failed with error '{0}'.", Level = EventLevel.Warning)] - public void MetricExporterErrorResult(int exportResult) - { - this.WriteEvent(21, exportResult); + this.WriteEvent(16, exception); } [Event(22, Message = "ForceFlush complete. '{0}' spans left in queue unprocessed.", Level = EventLevel.Informational)] @@ -275,18 +300,60 @@ public void SelfDiagnosticsFileCreateException(string logDirectory, string excep this.WriteEvent(26, logDirectory, exception); } - [Event(27, Message = "Failed to create resource from ResourceDetector: '{0}' due to '{1}'.", Level = EventLevel.Warning)] - public void ResourceDetectorFailed(string resourceDetector, string issue) - { - this.WriteEvent(27, resourceDetector, issue); - } - [Event(28, Message = "Unknown error in TracerProvider '{0}': '{1}'.", Level = EventLevel.Error)] public void TracerProviderException(string evnt, string ex) { this.WriteEvent(28, evnt, ex); } + [Event(29, Message = "Failed to parse environment variable: '{0}', value: '{1}'.", Level = EventLevel.Warning)] + public void FailedToParseEnvironmentVariable(string name, string value) + { + this.WriteEvent(29, name, value); + } + + [Event(30, Message = "Missing permissions to read environment variable: '{0}'", Level = EventLevel.Warning)] + public void MissingPermissionsToReadEnvironmentVariable(string exception) + { + this.WriteEvent(30, exception); + } + + [Event(31, Message = "'{0}' exporting to '{1}' dropped '0' items.", Level = EventLevel.Informational)] + public void NoDroppedExportProcessorItems(string exportProcessorName, string exporterName) + { + this.WriteEvent(31, exportProcessorName, exporterName); + } + + [Event(32, Message = "'{0}' exporting to '{1}' dropped '{2}' item(s) due to buffer full.", Level = EventLevel.Warning)] + public void ExistsDroppedExportProcessorItems(string exportProcessorName, string exporterName, long droppedCount) + { + this.WriteEvent(32, exportProcessorName, exporterName, droppedCount); + } + + [Event(33, Message = "Measurements from Instrument '{0}', Meter '{1}' will be ignored. Reason: '{2}'. Suggested action: '{3}'", Level = EventLevel.Warning)] + public void MetricInstrumentIgnored(string instrumentName, string meterName, string reason, string fix) + { + this.WriteEvent(33, instrumentName, meterName, reason, fix); + } + + [Event(34, Message = "Unknown error in MetricReader event '{0}': '{1}'.", Level = EventLevel.Error)] + public void MetricReaderException(string methodName, string ex) + { + this.WriteEvent(34, methodName, ex); + } + + [Event(35, Message = "Unknown error in MeterProvider '{0}': '{1}'.", Level = EventLevel.Error)] + public void MeterProviderException(string methodName, string ex) + { + this.WriteEvent(35, methodName, ex); + } + + [Event(36, Message = "Measurement dropped from Instrument Name/Metric Stream Name '{0}'. Reason: '{1}'. Suggested action: '{2}'", Level = EventLevel.Warning)] + public void MeasurementDropped(string instrumentName, string reason, string fix) + { + this.WriteEvent(36, instrumentName, reason, fix); + } + #if DEBUG public class OpenTelemetryEventListener : EventListener { diff --git a/src/OpenTelemetry/Internal/PooledList.cs b/src/OpenTelemetry/Internal/PooledList.cs index 3e7be9e57d6..52eea3d3eb4 100644 --- a/src/OpenTelemetry/Internal/PooledList.cs +++ b/src/OpenTelemetry/Internal/PooledList.cs @@ -52,12 +52,9 @@ public static PooledList Create() public static void Add(ref PooledList list, T item) { - var buffer = list.buffer; + Guard.ThrowIfNull(list.buffer, $"{nameof(list)}.{nameof(list.buffer)}"); - if (buffer == null) - { - throw new InvalidOperationException("Items cannot be added to an empty pool instance."); - } + var buffer = list.buffer; if (list.Count >= buffer.Length) { diff --git a/src/OpenTelemetry/Internal/SelfDiagnosticsEventListener.cs b/src/OpenTelemetry/Internal/SelfDiagnosticsEventListener.cs index dfc3c01f1c9..5581f5ee2ab 100644 --- a/src/OpenTelemetry/Internal/SelfDiagnosticsEventListener.cs +++ b/src/OpenTelemetry/Internal/SelfDiagnosticsEventListener.cs @@ -43,8 +43,10 @@ internal class SelfDiagnosticsEventListener : EventListener public SelfDiagnosticsEventListener(EventLevel logLevel, SelfDiagnosticsConfigRefresher configRefresher) { + Guard.ThrowIfNull(configRefresher, nameof(configRefresher)); + this.logLevel = logLevel; - this.configRefresher = configRefresher ?? throw new ArgumentNullException(nameof(configRefresher)); + this.configRefresher = configRefresher; List eventSources; lock (this.lockObj) diff --git a/src/OpenTelemetry/Logs/OpenTelemetryLogger.cs b/src/OpenTelemetry/Logs/OpenTelemetryLogger.cs index 11ca152d64a..d600a305cde 100644 --- a/src/OpenTelemetry/Logs/OpenTelemetryLogger.cs +++ b/src/OpenTelemetry/Logs/OpenTelemetryLogger.cs @@ -18,6 +18,7 @@ using System.Collections.Generic; using System.Runtime.CompilerServices; using Microsoft.Extensions.Logging; +using OpenTelemetry.Internal; namespace OpenTelemetry.Logs { @@ -28,8 +29,11 @@ internal class OpenTelemetryLogger : ILogger internal OpenTelemetryLogger(string categoryName, OpenTelemetryLoggerProvider provider) { - this.categoryName = categoryName ?? throw new ArgumentNullException(nameof(categoryName)); - this.provider = provider ?? throw new ArgumentNullException(nameof(provider)); + Guard.ThrowIfNull(categoryName, nameof(categoryName)); + Guard.ThrowIfNull(provider, nameof(provider)); + + this.categoryName = categoryName; + this.provider = provider; } internal IExternalScopeProvider ScopeProvider { get; set; } @@ -54,7 +58,7 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except logLevel, eventId, options.IncludeFormattedMessage ? formatter?.Invoke(state, exception) : null, - options.ParseStateValues ? null : (object)state, + options.ParseStateValues ? null : state, exception, options.ParseStateValues ? this.ParseState(state) : null); diff --git a/src/OpenTelemetry/Logs/OpenTelemetryLoggerOptions.cs b/src/OpenTelemetry/Logs/OpenTelemetryLoggerOptions.cs index 2ef00398638..6b6b1542ead 100644 --- a/src/OpenTelemetry/Logs/OpenTelemetryLoggerOptions.cs +++ b/src/OpenTelemetry/Logs/OpenTelemetryLoggerOptions.cs @@ -14,8 +14,8 @@ // limitations under the License. // -using System; using System.Collections.Generic; +using OpenTelemetry.Internal; using OpenTelemetry.Resources; namespace OpenTelemetry.Logs @@ -58,10 +58,7 @@ public class OpenTelemetryLoggerOptions /// Returns for chaining. public OpenTelemetryLoggerOptions AddProcessor(BaseProcessor processor) { - if (processor == null) - { - throw new ArgumentNullException(nameof(processor)); - } + Guard.ThrowIfNull(processor, nameof(processor)); this.Processors.Add(processor); @@ -76,8 +73,9 @@ public OpenTelemetryLoggerOptions AddProcessor(BaseProcessor processo /// Returns for chaining. public OpenTelemetryLoggerOptions SetResourceBuilder(ResourceBuilder resourceBuilder) { - this.ResourceBuilder = resourceBuilder ?? throw new ArgumentNullException(nameof(resourceBuilder)); + Guard.ThrowIfNull(resourceBuilder, nameof(resourceBuilder)); + this.ResourceBuilder = resourceBuilder; return this; } } diff --git a/src/OpenTelemetry/Logs/OpenTelemetryLoggerProvider.cs b/src/OpenTelemetry/Logs/OpenTelemetryLoggerProvider.cs index 16d0f1f6b0d..4e1dd445cf2 100644 --- a/src/OpenTelemetry/Logs/OpenTelemetryLoggerProvider.cs +++ b/src/OpenTelemetry/Logs/OpenTelemetryLoggerProvider.cs @@ -14,10 +14,10 @@ // limitations under the License. // -using System; using System.Collections; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using OpenTelemetry.Internal; using OpenTelemetry.Resources; namespace OpenTelemetry.Logs @@ -46,8 +46,9 @@ public OpenTelemetryLoggerProvider(IOptionsMonitor o internal OpenTelemetryLoggerProvider(OpenTelemetryLoggerOptions options) { - this.Options = options ?? throw new ArgumentNullException(nameof(options)); + Guard.ThrowIfNull(options, nameof(options)); + this.Options = options; this.Resource = options.ResourceBuilder.Build(); foreach (var processor in options.Processors) @@ -96,10 +97,7 @@ public ILogger CreateLogger(string categoryName) internal OpenTelemetryLoggerProvider AddProcessor(BaseProcessor processor) { - if (processor == null) - { - throw new ArgumentNullException(nameof(processor)); - } + Guard.ThrowIfNull(processor, nameof(processor)); processor.SetParentProvider(this); @@ -125,19 +123,19 @@ internal OpenTelemetryLoggerProvider AddProcessor(BaseProcessor proce protected override void Dispose(bool disposing) { - if (this.disposed) + if (!this.disposed) { - return; - } + if (disposing) + { + // Wait for up to 5 seconds grace period + this.Processor?.Shutdown(5000); + this.Processor?.Dispose(); + } - if (disposing) - { - // Wait for up to 5 seconds grace period - this.Processor?.Shutdown(5000); - this.Processor?.Dispose(); + this.disposed = true; } - this.disposed = true; + base.Dispose(disposing); } } } diff --git a/src/OpenTelemetry/Logs/OpenTelemetryLoggingExtensions.cs b/src/OpenTelemetry/Logs/OpenTelemetryLoggingExtensions.cs index 7affea1ef02..0b48ea5d35e 100644 --- a/src/OpenTelemetry/Logs/OpenTelemetryLoggingExtensions.cs +++ b/src/OpenTelemetry/Logs/OpenTelemetryLoggingExtensions.cs @@ -19,6 +19,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Configuration; +using OpenTelemetry.Internal; using OpenTelemetry.Logs; namespace Microsoft.Extensions.Logging @@ -27,10 +28,7 @@ public static class OpenTelemetryLoggingExtensions { public static ILoggingBuilder AddOpenTelemetry(this ILoggingBuilder builder, Action configure = null) { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } + Guard.ThrowIfNull(builder, nameof(builder)); builder.AddConfiguration(); builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); diff --git a/src/OpenTelemetry/Metrics/DataPoint/IDataPoint.cs b/src/OpenTelemetry/Metrics/AggregationTemporality.cs similarity index 68% rename from src/OpenTelemetry/Metrics/DataPoint/IDataPoint.cs rename to src/OpenTelemetry/Metrics/AggregationTemporality.cs index 4f0892795dc..c78816addb3 100644 --- a/src/OpenTelemetry/Metrics/DataPoint/IDataPoint.cs +++ b/src/OpenTelemetry/Metrics/AggregationTemporality.cs @@ -1,4 +1,4 @@ -// +// // Copyright The OpenTelemetry Authors // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,14 +15,20 @@ // using System; -using System.Collections.Generic; namespace OpenTelemetry.Metrics { - public interface IDataPoint : IDataValue + [Flags] + public enum AggregationTemporality : byte { - DateTimeOffset Timestamp { get; } + /// + /// Cumulative. + /// + Cumulative = 0b1, - KeyValuePair[] Tags { get; } + /// + /// Delta. + /// + Delta = 0b10, } } diff --git a/src/OpenTelemetry/Metrics/AggregationTemporalityAttribute.cs b/src/OpenTelemetry/Metrics/AggregationTemporalityAttribute.cs new file mode 100644 index 00000000000..e3d2212224b --- /dev/null +++ b/src/OpenTelemetry/Metrics/AggregationTemporalityAttribute.cs @@ -0,0 +1,33 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; + +namespace OpenTelemetry.Metrics +{ + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] + public sealed class AggregationTemporalityAttribute : Attribute + { + private AggregationTemporality temporality; + + public AggregationTemporalityAttribute(AggregationTemporality temporality) + { + this.temporality = temporality; + } + + public AggregationTemporality Temporality => this.temporality; + } +} diff --git a/src/OpenTelemetry/Metrics/AggregationType.cs b/src/OpenTelemetry/Metrics/AggregationType.cs new file mode 100644 index 00000000000..bd87c9e5b4f --- /dev/null +++ b/src/OpenTelemetry/Metrics/AggregationType.cs @@ -0,0 +1,66 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace OpenTelemetry.Metrics +{ + internal enum AggregationType + { + /// + /// Invalid. + /// + Invalid = -1, + + /// + /// Calculate SUM from incoming delta measurements. + /// + LongSumIncomingDelta = 0, + + /// + /// Calculate SUM from incoming cumulative measurements. + /// + LongSumIncomingCumulative = 1, + + /// + /// Calculate SUM from incoming delta measurements. + /// + DoubleSumIncomingDelta = 2, + + /// + /// Calculate SUM from incoming cumulative measurements. + /// + DoubleSumIncomingCumulative = 3, + + /// + /// Keep LastValue. + /// + LongGauge = 4, + + /// + /// Keep LastValue. + /// + DoubleGauge = 5, + + /// + /// Histogram. + /// + Histogram = 6, + + /// + /// Histogram with sum, count only. + /// + HistogramSumCount = 7, + } +} diff --git a/src/OpenTelemetry/Metrics/AggregatorStore.cs b/src/OpenTelemetry/Metrics/AggregatorStore.cs index e749e13fdc0..a4b3df38aef 100644 --- a/src/OpenTelemetry/Metrics/AggregatorStore.cs +++ b/src/OpenTelemetry/Metrics/AggregatorStore.cs @@ -15,192 +15,385 @@ // using System; +using System.Collections.Concurrent; using System.Collections.Generic; -using System.Diagnostics.Metrics; +using System.Runtime.CompilerServices; +using System.Threading; +using OpenTelemetry.Internal; namespace OpenTelemetry.Metrics { - internal class AggregatorStore + internal sealed class AggregatorStore { - private static readonly string[] EmptySeqKey = new string[0]; - private static readonly object[] EmptySeqValue = new object[0]; - private readonly Instrument instrument; - private readonly object lockKeyValue2MetricAggs = new object(); + private static readonly ObjectArrayEqualityComparer ObjectArrayComparer = new ObjectArrayEqualityComparer(); + private readonly object lockZeroTags = new object(); + private readonly HashSet tagKeysInteresting; + private readonly int tagsKeysInterestingCount; // Two-Level lookup. TagKeys x [ TagValues x Metrics ] - private readonly Dictionary> keyValue2MetricAggs = - new Dictionary>(new StringArrayEqualityComparer()); - - private IAggregator[] tag0Metrics = null; - - internal AggregatorStore(Instrument instrument) + private readonly ConcurrentDictionary> keyValue2MetricAggs = + new ConcurrentDictionary>(new StringArrayEqualityComparer()); + + private readonly AggregationTemporality temporality; + private readonly string name; + private readonly string metricPointCapHitMessage; + private readonly bool outputDelta; + private readonly MetricPoint[] metricPoints; + private readonly int[] currentMetricPointBatch; + private readonly AggregationType aggType; + private readonly double[] histogramBounds; + private readonly UpdateLongDelegate updateLongCallback; + private readonly UpdateDoubleDelegate updateDoubleCallback; + private readonly int maxMetricPoints; + private int metricPointIndex = 0; + private int batchSize = 0; + private int metricCapHitMessageLogged; + private bool zeroTagMetricPointInitialized; + private DateTimeOffset startTimeExclusive; + private DateTimeOffset endTimeInclusive; + + internal AggregatorStore( + string name, + AggregationType aggType, + AggregationTemporality temporality, + int maxMetricPoints, + double[] histogramBounds, + string[] tagKeysInteresting = null) { - this.instrument = instrument; + this.name = name; + this.maxMetricPoints = maxMetricPoints; + this.metricPointCapHitMessage = $"Maximum MetricPoints limit reached for this Metric stream. Configured limit: {this.maxMetricPoints}"; + this.metricPoints = new MetricPoint[maxMetricPoints]; + this.currentMetricPointBatch = new int[maxMetricPoints]; + this.aggType = aggType; + this.temporality = temporality; + this.outputDelta = temporality == AggregationTemporality.Delta ? true : false; + this.histogramBounds = histogramBounds; + this.startTimeExclusive = DateTimeOffset.UtcNow; + if (tagKeysInteresting == null) + { + this.updateLongCallback = this.UpdateLong; + this.updateDoubleCallback = this.UpdateDouble; + } + else + { + this.updateLongCallback = this.UpdateLongCustomTags; + this.updateDoubleCallback = this.UpdateDoubleCustomTags; + var hs = new HashSet(tagKeysInteresting, StringComparer.Ordinal); + this.tagKeysInteresting = hs; + this.tagsKeysInterestingCount = hs.Count; + } } - internal IAggregator[] MapToMetrics(string[] seqKey, object[] seqVal) - { - var aggregators = new List(); + private delegate void UpdateLongDelegate(long value, ReadOnlySpan> tags); - var tags = new KeyValuePair[seqKey.Length]; - for (int i = 0; i < seqKey.Length; i++) - { - tags[i] = new KeyValuePair(seqKey[i], seqVal[i]); - } + private delegate void UpdateDoubleDelegate(double value, ReadOnlySpan> tags); - var dt = DateTimeOffset.UtcNow; + internal void Update(long value, ReadOnlySpan> tags) + { + this.updateLongCallback(value, tags); + } - // TODO: Need to map each instrument to metrics (based on View API) - // TODO: move most of this logic out of hotpath, and to MeterProvider's - // InstrumentPublished event, which is once per instrument creation. + internal void Update(double value, ReadOnlySpan> tags) + { + this.updateDoubleCallback(value, tags); + } - if (this.instrument.GetType() == typeof(Counter) - || this.instrument.GetType() == typeof(Counter) - || this.instrument.GetType() == typeof(Counter) - || this.instrument.GetType() == typeof(Counter)) - { - aggregators.Add(new SumMetricAggregatorLong(this.instrument.Name, this.instrument.Description, this.instrument.Unit, this.instrument.Meter, dt, tags)); - } - else if (this.instrument.GetType() == typeof(Counter) - || this.instrument.GetType() == typeof(Counter)) + internal int Snapshot() + { + this.batchSize = 0; + var indexSnapshot = Math.Min(this.metricPointIndex, this.maxMetricPoints - 1); + if (this.temporality == AggregationTemporality.Delta) { - aggregators.Add(new SumMetricAggregatorDouble(this.instrument.Name, this.instrument.Description, this.instrument.Unit, this.instrument.Meter, dt, tags)); + this.SnapshotDelta(indexSnapshot); } - else if (this.instrument.GetType().Name.Contains("Gauge")) + else { - aggregators.Add(new GaugeMetricAggregator(this.instrument.Name, this.instrument.Description, this.instrument.Unit, this.instrument.Meter, dt, tags)); + this.SnapshotCumulative(indexSnapshot); } - else if (this.instrument.GetType().Name.Contains("Histogram")) + + this.endTimeInclusive = DateTimeOffset.UtcNow; + return this.batchSize; + } + + internal void SnapshotDelta(int indexSnapshot) + { + for (int i = 0; i <= indexSnapshot; i++) { - aggregators.Add(new HistogramMetricAggregator(this.instrument.Name, this.instrument.Description, this.instrument.Unit, this.instrument.Meter, dt, tags)); + ref var metricPoint = ref this.metricPoints[i]; + if (metricPoint.MetricPointStatus == MetricPointStatus.NoCollectPending) + { + continue; + } + + metricPoint.TakeSnapshot(this.outputDelta); + this.currentMetricPointBatch[this.batchSize] = i; + this.batchSize++; } - else + + if (this.endTimeInclusive != default) { - aggregators.Add(new SummaryMetricAggregator(this.instrument.Name, this.instrument.Description, this.instrument.Unit, this.instrument.Meter, dt, tags, false)); + this.startTimeExclusive = this.endTimeInclusive; } - - return aggregators.ToArray(); } - internal IAggregator[] FindMetricAggregators(ReadOnlySpan> tags) + internal void SnapshotCumulative(int indexSnapshot) { - int len = tags.Length; - - if (len == 0) + for (int i = 0; i <= indexSnapshot; i++) { - if (this.tag0Metrics == null) + ref var metricPoint = ref this.metricPoints[i]; + if (metricPoint.StartTime == default) { - this.tag0Metrics = this.MapToMetrics(AggregatorStore.EmptySeqKey, AggregatorStore.EmptySeqValue); + continue; } - return this.tag0Metrics; + metricPoint.TakeSnapshot(this.outputDelta); + this.currentMetricPointBatch[this.batchSize] = i; + this.batchSize++; } + } - var storage = ThreadStaticStorage.GetStorage(); - - storage.SplitToKeysAndValues(tags, out var tagKey, out var tagValue); + internal MetricPointsAccessor GetMetricPoints() + { + return new MetricPointsAccessor(this.metricPoints, this.currentMetricPointBatch, this.batchSize, this.startTimeExclusive, this.endTimeInclusive); + } - if (len > 1) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void InitializeZeroTagPointIfNotInitialized() + { + if (!this.zeroTagMetricPointInitialized) { - Array.Sort(tagKey, tagValue); + lock (this.lockZeroTags) + { + if (!this.zeroTagMetricPointInitialized) + { + var dt = DateTimeOffset.UtcNow; + this.metricPoints[0] = new MetricPoint(this.aggType, dt, null, null, this.histogramBounds); + this.zeroTagMetricPointInitialized = true; + } + } } + } - IAggregator[] metrics; + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int LookupAggregatorStore(string[] tagKeys, object[] tagValues, int length) + { + int aggregatorIndex; + string[] seqKey = null; - lock (this.lockKeyValue2MetricAggs) + // GetOrAdd by TagKeys at 1st Level of 2-level dictionary structure. + // Get back a Dictionary of [ Values x Metrics[] ]. + if (!this.keyValue2MetricAggs.TryGetValue(tagKeys, out var value2metrics)) { - string[] seqKey = null; + // Note: We are using storage from ThreadStatic, so need to make a deep copy for Dictionary storage. + seqKey = new string[length]; + tagKeys.CopyTo(seqKey, 0); - // GetOrAdd by TagKey at 1st Level of 2-level dictionary structure. - // Get back a Dictionary of [ Values x Metrics[] ]. - if (!this.keyValue2MetricAggs.TryGetValue(tagKey, out var value2metrics)) + value2metrics = new ConcurrentDictionary(ObjectArrayComparer); + if (!this.keyValue2MetricAggs.TryAdd(seqKey, value2metrics)) { - // Note: We are using storage from ThreadStatic, so need to make a deep copy for Dictionary storage. - - seqKey = new string[len]; - tagKey.CopyTo(seqKey, 0); - - value2metrics = new Dictionary(new ObjectArrayEqualityComparer()); - this.keyValue2MetricAggs.Add(seqKey, value2metrics); + this.keyValue2MetricAggs.TryGetValue(seqKey, out value2metrics); } + } - // GetOrAdd by TagValue at 2st Level of 2-level dictionary structure. - // Get back Metrics[]. - if (!value2metrics.TryGetValue(tagValue, out metrics)) + // GetOrAdd by TagValues at 2st Level of 2-level dictionary structure. + // Get back Metrics[]. + if (!value2metrics.TryGetValue(tagValues, out aggregatorIndex)) + { + aggregatorIndex = this.metricPointIndex; + if (aggregatorIndex >= this.maxMetricPoints) { - // Note: We are using storage from ThreadStatic, so need to make a deep copy for Dictionary storage. + // sorry! out of data points. + // TODO: Once we support cleanup of + // unused points (typically with delta) + // we can re-claim them here. + return -1; + } - if (seqKey == null) + lock (value2metrics) + { + // check again after acquiring lock. + if (!value2metrics.TryGetValue(tagValues, out aggregatorIndex)) { - seqKey = new string[len]; - tagKey.CopyTo(seqKey, 0); - } + aggregatorIndex = Interlocked.Increment(ref this.metricPointIndex); + if (aggregatorIndex >= this.maxMetricPoints) + { + // sorry! out of data points. + // TODO: Once we support cleanup of + // unused points (typically with delta) + // we can re-claim them here. + return -1; + } + + // Note: We are using storage from ThreadStatic, so need to make a deep copy for Dictionary storage. + if (seqKey == null) + { + seqKey = new string[length]; + tagKeys.CopyTo(seqKey, 0); + } - var seqVal = new object[len]; - tagValue.CopyTo(seqVal, 0); + var seqVal = new object[length]; + tagValues.CopyTo(seqVal, 0); - metrics = this.MapToMetrics(seqKey, seqVal); + ref var metricPoint = ref this.metricPoints[aggregatorIndex]; + var dt = DateTimeOffset.UtcNow; + metricPoint = new MetricPoint(this.aggType, dt, seqKey, seqVal, this.histogramBounds); - value2metrics.Add(seqVal, metrics); + // Add to dictionary *after* initializing MetricPoint + // as other threads can start writing to the + // MetricPoint, if dictionary entry found. + value2metrics.TryAdd(seqVal, aggregatorIndex); + } } } - return metrics; + return aggregatorIndex; } - internal void Update(T value, ReadOnlySpan> tags) - where T : struct + private void UpdateLong(long value, ReadOnlySpan> tags) { - // TODO: We can isolate the cost of each user-added aggregator in - // the hot path by queuing the DataPoint, and doing the Update as - // part of the Collect() instead. Thus, we only pay for the price - // of queueing a DataPoint in the Hot Path + try + { + var index = this.FindMetricAggregatorsDefault(tags); + if (index < 0) + { + if (Interlocked.CompareExchange(ref this.metricCapHitMessageLogged, 1, 0) == 0) + { + OpenTelemetrySdkEventSource.Log.MeasurementDropped(this.name, this.metricPointCapHitMessage, "Modify instrumentation to reduce the number of unique key/value pair combinations. Or use MeterProviderBuilder.SetMaxMetricPointsPerMetricStream to set higher limit."); + } - var metricAggregators = this.FindMetricAggregators(tags); + return; + } - foreach (var metricAggregator in metricAggregators) + this.metricPoints[index].Update(value); + } + catch (Exception) { - metricAggregator.Update(value); + OpenTelemetrySdkEventSource.Log.MeasurementDropped(this.name, "SDK internal error occurred.", "Contact SDK owners."); } } - internal List Collect(bool isDelta, DateTimeOffset dt) + private void UpdateLongCustomTags(long value, ReadOnlySpan> tags) { - var collectedMetrics = new List(); + try + { + var index = this.FindMetricAggregatorsCustomTag(tags); + if (index < 0) + { + if (Interlocked.CompareExchange(ref this.metricCapHitMessageLogged, 1, 0) == 0) + { + OpenTelemetrySdkEventSource.Log.MeasurementDropped(this.name, this.metricPointCapHitMessage, "Modify instrumentation to reduce the number of unique key/value pair combinations. Or use MeterProviderBuilder.SetMaxMetricPointsPerMetricStream to set higher limit."); + } + + return; + } + + this.metricPoints[index].Update(value); + } + catch (Exception) + { + OpenTelemetrySdkEventSource.Log.MeasurementDropped(this.name, "SDK internal error occurred.", "Contact SDK owners."); + } + } - if (this.tag0Metrics != null) + private void UpdateDouble(double value, ReadOnlySpan> tags) + { + try { - foreach (var aggregator in this.tag0Metrics) + var index = this.FindMetricAggregatorsDefault(tags); + if (index < 0) { - var m = aggregator.Collect(dt, isDelta); - if (m != null) + if (Interlocked.CompareExchange(ref this.metricCapHitMessageLogged, 1, 0) == 0) { - collectedMetrics.Add(m); + OpenTelemetrySdkEventSource.Log.MeasurementDropped(this.name, this.metricPointCapHitMessage, "Modify instrumentation to reduce the number of unique key/value pair combinations. Or use MeterProviderBuilder.SetMaxMetricPointsPerMetricStream to set higher limit."); } + + return; } + + this.metricPoints[index].Update(value); } + catch (Exception) + { + OpenTelemetrySdkEventSource.Log.MeasurementDropped(this.name, "SDK internal error occurred.", "Contact SDK owners."); + } + } - // Lock to prevent new time series from being added - // until collect is done. - lock (this.lockKeyValue2MetricAggs) + private void UpdateDoubleCustomTags(double value, ReadOnlySpan> tags) + { + try { - foreach (var keys in this.keyValue2MetricAggs) + var index = this.FindMetricAggregatorsCustomTag(tags); + if (index < 0) { - foreach (var values in keys.Value) + if (Interlocked.CompareExchange(ref this.metricCapHitMessageLogged, 1, 0) == 0) { - foreach (var metric in values.Value) - { - var m = metric.Collect(dt, isDelta); - if (m != null) - { - collectedMetrics.Add(m); - } - } + OpenTelemetrySdkEventSource.Log.MeasurementDropped(this.name, this.metricPointCapHitMessage, "Modify instrumentation to reduce the number of unique key/value pair combinations. Or use MeterProviderBuilder.SetMaxMetricPointsPerMetricStream to set higher limit."); } + + return; } + + this.metricPoints[index].Update(value); + } + catch (Exception) + { + OpenTelemetrySdkEventSource.Log.MeasurementDropped(this.name, "SDK internal error occurred.", "Contact SDK owners."); + } + } + + private int FindMetricAggregatorsDefault(ReadOnlySpan> tags) + { + int tagLength = tags.Length; + if (tagLength == 0) + { + this.InitializeZeroTagPointIfNotInitialized(); + return 0; + } + + var storage = ThreadStaticStorage.GetStorage(); + + storage.SplitToKeysAndValues(tags, tagLength, out var tagKeys, out var tagValues); + + if (tagLength > 1) + { + Array.Sort(tagKeys, tagValues); + } + + return this.LookupAggregatorStore(tagKeys, tagValues, tagLength); + } + + private int FindMetricAggregatorsCustomTag(ReadOnlySpan> tags) + { + int tagLength = tags.Length; + if (tagLength == 0 || this.tagsKeysInterestingCount == 0) + { + this.InitializeZeroTagPointIfNotInitialized(); + return 0; + } + + // TODO: Get only interesting tags + // from the incoming tags + + var storage = ThreadStaticStorage.GetStorage(); + + storage.SplitToKeysAndValues(tags, tagLength, this.tagKeysInteresting, out var tagKeys, out var tagValues, out var actualLength); + + // Actual number of tags depend on how many + // of the incoming tags has user opted to + // select. + if (actualLength == 0) + { + this.InitializeZeroTagPointIfNotInitialized(); + return 0; + } + + if (actualLength > 1) + { + Array.Sort(tagKeys, tagValues); } - return collectedMetrics; + return this.LookupAggregatorStore(tagKeys, tagValues, actualLength); } } } diff --git a/src/OpenTelemetry/Metrics/BaseExportingMetricReader.cs b/src/OpenTelemetry/Metrics/BaseExportingMetricReader.cs new file mode 100644 index 00000000000..4b7b845a754 --- /dev/null +++ b/src/OpenTelemetry/Metrics/BaseExportingMetricReader.cs @@ -0,0 +1,161 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Diagnostics; +using System.Threading; +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Metrics +{ + public class BaseExportingMetricReader : MetricReader + { + protected readonly BaseExporter exporter; + private readonly ExportModes supportedExportModes = ExportModes.Push | ExportModes.Pull; + private bool disposed; + + public BaseExportingMetricReader(BaseExporter exporter) + { + Guard.ThrowIfNull(exporter, nameof(exporter)); + + this.exporter = exporter; + + var exportorType = exporter.GetType(); + var attributes = exportorType.GetCustomAttributes(typeof(AggregationTemporalityAttribute), true); + if (attributes.Length > 0) + { + var attr = (AggregationTemporalityAttribute)attributes[attributes.Length - 1]; + this.Temporality = attr.Temporality; + } + + attributes = exportorType.GetCustomAttributes(typeof(ExportModesAttribute), true); + if (attributes.Length > 0) + { + var attr = (ExportModesAttribute)attributes[attributes.Length - 1]; + this.supportedExportModes = attr.Supported; + } + + if (exporter is IPullMetricExporter pullExporter) + { + if (this.supportedExportModes.HasFlag(ExportModes.Push)) + { + pullExporter.Collect = this.Collect; + } + else + { + pullExporter.Collect = (timeoutMilliseconds) => + { + using (PullMetricScope.Begin()) + { + return this.Collect(timeoutMilliseconds); + } + }; + } + } + } + + internal BaseExporter Exporter => this.exporter; + + protected ExportModes SupportedExportModes => this.supportedExportModes; + + internal override void SetParentProvider(BaseProvider parentProvider) + { + base.SetParentProvider(parentProvider); + this.exporter.ParentProvider = parentProvider; + } + + /// + internal override bool ProcessMetrics(in Batch metrics, int timeoutMilliseconds) + { + // TODO: Do we need to consider timeout here? + try + { + return this.exporter.Export(metrics) == ExportResult.Success; + } + catch (Exception ex) + { + OpenTelemetrySdkEventSource.Log.MetricReaderException(nameof(this.ProcessMetrics), ex); + return false; + } + } + + /// + protected override bool OnCollect(int timeoutMilliseconds) + { + if (this.supportedExportModes.HasFlag(ExportModes.Push)) + { + return base.OnCollect(timeoutMilliseconds); + } + + if (this.supportedExportModes.HasFlag(ExportModes.Pull) && PullMetricScope.IsPullAllowed) + { + return base.OnCollect(timeoutMilliseconds); + } + + // TODO: add some error log + return false; + } + + /// + protected override bool OnShutdown(int timeoutMilliseconds) + { + var result = true; + + if (timeoutMilliseconds == Timeout.Infinite) + { + result = this.Collect(Timeout.Infinite) && result; + result = this.exporter.Shutdown(Timeout.Infinite) && result; + } + else + { + var sw = Stopwatch.StartNew(); + result = this.Collect(timeoutMilliseconds) && result; + var timeout = timeoutMilliseconds - sw.ElapsedMilliseconds; + result = this.exporter.Shutdown((int)Math.Max(timeout, 0)) && result; + } + + return result; + } + + /// + protected override void Dispose(bool disposing) + { + if (!this.disposed) + { + if (disposing) + { + try + { + if (this.exporter is IPullMetricExporter pullExporter) + { + pullExporter.Collect = null; + } + + this.exporter.Dispose(); + } + catch (Exception ex) + { + OpenTelemetrySdkEventSource.Log.MetricReaderException(nameof(this.Dispose), ex); + } + } + + this.disposed = true; + } + + base.Dispose(disposing); + } + } +} diff --git a/src/OpenTelemetry/Metrics/CompositeMetricReader.cs b/src/OpenTelemetry/Metrics/CompositeMetricReader.cs new file mode 100644 index 00000000000..537cec59385 --- /dev/null +++ b/src/OpenTelemetry/Metrics/CompositeMetricReader.cs @@ -0,0 +1,197 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Metrics +{ + /// + /// CompositeMetricReader that does not deal with adding metrics and recording measurements. + /// + internal sealed partial class CompositeMetricReader : MetricReader + { + private readonly DoublyLinkedListNode head; + private DoublyLinkedListNode tail; + private bool disposed; + private int count; + + public CompositeMetricReader(IEnumerable readers) + { + Guard.ThrowIfNull(readers, nameof(readers)); + + using var iter = readers.GetEnumerator(); + if (!iter.MoveNext()) + { + throw new ArgumentException($"'{iter}' is null or empty", nameof(iter)); + } + + this.head = new DoublyLinkedListNode(iter.Current); + this.tail = this.head; + this.count++; + + while (iter.MoveNext()) + { + this.AddReader(iter.Current); + } + } + + public CompositeMetricReader AddReader(MetricReader reader) + { + Guard.ThrowIfNull(reader, nameof(reader)); + + var node = new DoublyLinkedListNode(reader) + { + Previous = this.tail, + }; + this.tail.Next = node; + this.tail = node; + this.count++; + + return this; + } + + public Enumerator GetEnumerator() => new Enumerator(this.head); + + /// + internal override bool ProcessMetrics(in Batch metrics, int timeoutMilliseconds) + { + // CompositeMetricReader delegates the work to its underlying readers, + // so CompositeMetricReader.ProcessMetrics should never be called. + throw new NotSupportedException(); + } + + /// + protected override bool OnCollect(int timeoutMilliseconds = Timeout.Infinite) + { + var result = true; + var sw = timeoutMilliseconds == Timeout.Infinite + ? null + : Stopwatch.StartNew(); + + for (var cur = this.head; cur != null; cur = cur.Next) + { + if (sw == null) + { + result = cur.Value.Collect(Timeout.Infinite) && result; + } + else + { + var timeout = timeoutMilliseconds - sw.ElapsedMilliseconds; + + // notify all the readers, even if we run overtime + result = cur.Value.Collect((int)Math.Max(timeout, 0)) && result; + } + } + + return result; + } + + /// + protected override bool OnShutdown(int timeoutMilliseconds) + { + var result = true; + var sw = timeoutMilliseconds == Timeout.Infinite + ? null + : Stopwatch.StartNew(); + + for (var cur = this.head; cur != null; cur = cur.Next) + { + if (sw == null) + { + result = cur.Value.Shutdown(Timeout.Infinite) && result; + } + else + { + var timeout = timeoutMilliseconds - sw.ElapsedMilliseconds; + + // notify all the readers, even if we run overtime + result = cur.Value.Shutdown((int)Math.Max(timeout, 0)) && result; + } + } + + return result; + } + + protected override void Dispose(bool disposing) + { + if (!this.disposed) + { + if (disposing) + { + for (var cur = this.head; cur != null; cur = cur.Next) + { + try + { + cur.Value?.Dispose(); + } + catch (Exception) + { + // TODO: which event source do we use? + // OpenTelemetrySdkEventSource.Log.SpanProcessorException(nameof(this.Dispose), ex); + } + } + } + + this.disposed = true; + } + + base.Dispose(disposing); + } + + public struct Enumerator + { + private DoublyLinkedListNode node; + + internal Enumerator(DoublyLinkedListNode node) + { + this.node = node; + this.Current = null; + } + + public MetricReader Current { get; private set; } + + public bool MoveNext() + { + if (this.node != null) + { + this.Current = this.node.Value; + this.node = this.node.Next; + return true; + } + + return false; + } + } + + internal class DoublyLinkedListNode + { + public readonly MetricReader Value; + + public DoublyLinkedListNode(MetricReader value) + { + this.Value = value; + } + + public DoublyLinkedListNode Previous { get; set; } + + public DoublyLinkedListNode Next { get; set; } + } + } +} diff --git a/src/OpenTelemetry/Metrics/CompositeMetricReaderExt.cs b/src/OpenTelemetry/Metrics/CompositeMetricReaderExt.cs new file mode 100644 index 00000000000..11693017fb9 --- /dev/null +++ b/src/OpenTelemetry/Metrics/CompositeMetricReaderExt.cs @@ -0,0 +1,149 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.Metrics; + +namespace OpenTelemetry.Metrics +{ + /// + /// CompositeMetricReader that deals with adding metrics and recording measurements. + /// + internal sealed partial class CompositeMetricReader + { + internal List AddMetricsWithNoViews(Instrument instrument) + { + var metrics = new List(this.count); + for (var cur = this.head; cur != null; cur = cur.Next) + { + var metric = cur.Value.AddMetricWithNoViews(instrument); + metrics.Add(metric); + } + + return metrics; + } + + internal void RecordSingleStreamLongMeasurements(List metrics, long value, ReadOnlySpan> tags) + { + Debug.Assert(metrics.Count == this.count, "The count of metrics to be updated for a CompositeReader must match the number of individual readers."); + + int index = 0; + for (var cur = this.head; cur != null; cur = cur.Next) + { + if (metrics[index] != null) + { + cur.Value.RecordSingleStreamLongMeasurement(metrics[index], value, tags); + } + + index++; + } + } + + internal void RecordSingleStreamDoubleMeasurements(List metrics, double value, ReadOnlySpan> tags) + { + Debug.Assert(metrics.Count == this.count, "The count of metrics to be updated for a CompositeReader must match the number of individual readers."); + + int index = 0; + for (var cur = this.head; cur != null; cur = cur.Next) + { + if (metrics[index] != null) + { + cur.Value.RecordSingleStreamDoubleMeasurement(metrics[index], value, tags); + } + + index++; + } + } + + internal List> AddMetricsSuperListWithViews(Instrument instrument, List metricStreamConfigs) + { + var metricsSuperList = new List>(this.count); + for (var cur = this.head; cur != null; cur = cur.Next) + { + var metrics = cur.Value.AddMetricsListWithViews(instrument, metricStreamConfigs); + metricsSuperList.Add(metrics); + } + + return metricsSuperList; + } + + internal void RecordLongMeasurements(List> metricsSuperList, long value, ReadOnlySpan> tags) + { + Debug.Assert(metricsSuperList.Count == this.count, "The count of metrics to be updated for a CompositeReader must match the number of individual readers."); + + int index = 0; + for (var cur = this.head; cur != null; cur = cur.Next) + { + if (metricsSuperList[index].Count > 0) + { + cur.Value.RecordLongMeasurement(metricsSuperList[index], value, tags); + } + + index++; + } + } + + internal void RecordDoubleMeasurements(List> metricsSuperList, double value, ReadOnlySpan> tags) + { + Debug.Assert(metricsSuperList.Count == this.count, "The count of metrics to be updated for a CompositeReader must match the number of individual readers."); + + int index = 0; + for (var cur = this.head; cur != null; cur = cur.Next) + { + if (metricsSuperList[index].Count > 0) + { + cur.Value.RecordDoubleMeasurement(metricsSuperList[index], value, tags); + } + + index++; + } + } + + internal void CompleteSingleStreamMeasurements(List metrics) + { + Debug.Assert(metrics.Count == this.count, "The count of metrics to be updated for a CompositeReader must match the number of individual readers."); + + int index = 0; + for (var cur = this.head; cur != null; cur = cur.Next) + { + if (metrics[index] != null) + { + cur.Value.CompleteSingleStreamMeasurement(metrics[index]); + } + + index++; + } + } + + internal void CompleteMesaurements(List> metricsSuperList) + { + Debug.Assert(metricsSuperList.Count == this.count, "The count of metrics to be updated for a CompositeReader must match the number of individual readers."); + + int index = 0; + for (var cur = this.head; cur != null; cur = cur.Next) + { + if (metricsSuperList[index].Count > 0) + { + cur.Value.CompleteMeasurement(metricsSuperList[index]); + } + + index++; + } + } + } +} diff --git a/src/OpenTelemetry/Metrics/DataPoint/DataPoint.cs b/src/OpenTelemetry/Metrics/DataPoint/DataPoint.cs deleted file mode 100644 index 83fa90612fc..00000000000 --- a/src/OpenTelemetry/Metrics/DataPoint/DataPoint.cs +++ /dev/null @@ -1,70 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -using System; -using System.Collections.Generic; - -namespace OpenTelemetry.Metrics -{ - internal readonly struct DataPoint : IDataPoint - { - private static readonly KeyValuePair[] EmptyTag = new KeyValuePair[0]; - - private readonly IDataValue value; - - internal DataPoint(DateTimeOffset timestamp, long value, KeyValuePair[] tags) - { - this.Timestamp = timestamp; - this.Tags = tags; - this.value = new DataValue(value); - } - - internal DataPoint(DateTimeOffset timestamp, double value, KeyValuePair[] tags) - { - this.Timestamp = timestamp; - this.Tags = tags; - this.value = new DataValue(value); - } - - internal DataPoint(DateTimeOffset timestamp, IDataValue value, KeyValuePair[] tags) - { - this.Timestamp = timestamp; - this.Tags = tags; - this.value = value; - } - - internal DataPoint(DateTimeOffset timestamp, long value) - : this(timestamp, value, DataPoint.EmptyTag) - { - } - - internal DataPoint(DateTimeOffset timestamp, double value) - : this(timestamp, value, DataPoint.EmptyTag) - { - } - - internal DataPoint(DateTimeOffset timestamp, IDataValue value) - : this(timestamp, value, DataPoint.EmptyTag) - { - } - - public DateTimeOffset Timestamp { get; } - - public readonly KeyValuePair[] Tags { get; } - - public object Value => this.value.Value; - } -} diff --git a/src/OpenTelemetry/Metrics/DataPoint/DataPoint{T}.cs b/src/OpenTelemetry/Metrics/DataPoint/DataPoint{T}.cs deleted file mode 100644 index e7b895d54c6..00000000000 --- a/src/OpenTelemetry/Metrics/DataPoint/DataPoint{T}.cs +++ /dev/null @@ -1,59 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -using System; -using System.Collections.Generic; - -namespace OpenTelemetry.Metrics -{ - internal readonly struct DataPoint : IDataPoint - where T : struct - { - private static readonly KeyValuePair[] EmptyTag = new KeyValuePair[0]; - - private readonly IDataValue value; - - internal DataPoint(DateTimeOffset timestamp, T value, KeyValuePair[] tags) - { - this.Timestamp = timestamp; - this.Tags = tags; - this.value = new DataValue(value); - } - - internal DataPoint(DateTimeOffset timestamp, IDataValue value, KeyValuePair[] tags) - { - this.Timestamp = timestamp; - this.Tags = tags; - this.value = value; - } - - internal DataPoint(DateTimeOffset timestamp, T value) - : this(timestamp, value, DataPoint.EmptyTag) - { - } - - internal DataPoint(DateTimeOffset timestamp, IDataValue value) - : this(timestamp, value, DataPoint.EmptyTag) - { - } - - public DateTimeOffset Timestamp { get; } - - public readonly KeyValuePair[] Tags { get; } - - public object Value => this.value.Value; - } -} diff --git a/src/OpenTelemetry/Metrics/DataPoint/DataValue.cs b/src/OpenTelemetry/Metrics/DataPoint/DataValue.cs deleted file mode 100644 index cfbe935a226..00000000000 --- a/src/OpenTelemetry/Metrics/DataPoint/DataValue.cs +++ /dev/null @@ -1,46 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -namespace OpenTelemetry.Metrics -{ - public readonly struct DataValue : IDataValue - { - private readonly IDataValue value; - - internal DataValue(int value) - { - // Promote to long - this.value = new DataValue(value); - } - - internal DataValue(long value) - { - this.value = new DataValue(value); - } - - internal DataValue(double value) - { - this.value = new DataValue(value); - } - - internal DataValue(IDataValue value) - { - this.value = value; - } - - public object Value => this.value.Value; - } -} diff --git a/src/OpenTelemetry/Metrics/DataPoint/Exemplar.cs b/src/OpenTelemetry/Metrics/DataPoint/Exemplar.cs deleted file mode 100644 index 7b9798d3fc2..00000000000 --- a/src/OpenTelemetry/Metrics/DataPoint/Exemplar.cs +++ /dev/null @@ -1,81 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -using System; -using System.Collections.Generic; -using System.Diagnostics; - -namespace OpenTelemetry.Metrics -{ - internal readonly struct Exemplar : IExemplar - { - private static readonly KeyValuePair[] EmptyTag = new KeyValuePair[0]; - - private readonly IDataValue value; - - internal Exemplar(DateTimeOffset timestamp, long value, ActivityTraceId traceId, ActivitySpanId spanId, KeyValuePair[] filteredTags) - { - this.Timestamp = timestamp; - this.FilteredTags = filteredTags; - this.SpanId = spanId; - this.TraceId = traceId; - this.value = new DataValue(value); - } - - internal Exemplar(DateTimeOffset timestamp, double value, ActivityTraceId traceId, ActivitySpanId spanId, KeyValuePair[] filteredTags) - { - this.Timestamp = timestamp; - this.FilteredTags = filteredTags; - this.SpanId = spanId; - this.TraceId = traceId; - this.value = new DataValue(value); - } - - internal Exemplar(DateTimeOffset timestamp, IDataValue value, ActivityTraceId traceId, ActivitySpanId spanId, KeyValuePair[] filteredTags) - { - this.Timestamp = timestamp; - this.FilteredTags = filteredTags; - this.SpanId = spanId; - this.TraceId = traceId; - this.value = value; - } - - internal Exemplar(DateTimeOffset timestamp, long value) - : this(timestamp, value, default, default, Exemplar.EmptyTag) - { - } - - internal Exemplar(DateTimeOffset timestamp, double value) - : this(timestamp, value, default, default, Exemplar.EmptyTag) - { - } - - internal Exemplar(DateTimeOffset timestamp, IDataValue value) - : this(timestamp, value, default, default, Exemplar.EmptyTag) - { - } - - public DateTimeOffset Timestamp { get; } - - public readonly KeyValuePair[] FilteredTags { get; } - - public readonly ActivityTraceId TraceId { get; } - - public readonly ActivitySpanId SpanId { get; } - - public object Value => this.value.Value; - } -} diff --git a/src/OpenTelemetry/Metrics/DataPoint/Exemplar{T}.cs b/src/OpenTelemetry/Metrics/DataPoint/Exemplar{T}.cs deleted file mode 100644 index 140e0f799c9..00000000000 --- a/src/OpenTelemetry/Metrics/DataPoint/Exemplar{T}.cs +++ /dev/null @@ -1,68 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -using System; -using System.Collections.Generic; -using System.Diagnostics; - -namespace OpenTelemetry.Metrics -{ - internal readonly struct Exemplar : IExemplar - where T : struct - { - private static readonly KeyValuePair[] EmptyTag = new KeyValuePair[0]; - - private readonly IDataValue value; - - internal Exemplar(DateTimeOffset timestamp, T value, ActivityTraceId traceId, ActivitySpanId spanId, KeyValuePair[] filteredTags) - { - this.Timestamp = timestamp; - this.FilteredTags = filteredTags; - this.SpanId = spanId; - this.TraceId = traceId; - this.value = new DataValue(value); - } - - internal Exemplar(DateTimeOffset timestamp, IDataValue value, ActivityTraceId traceId, ActivitySpanId spanId, KeyValuePair[] filteredTags) - { - this.Timestamp = timestamp; - this.FilteredTags = filteredTags; - this.SpanId = spanId; - this.TraceId = traceId; - this.value = value; - } - - internal Exemplar(DateTimeOffset timestamp, T value) - : this(timestamp, value, default, default, Exemplar.EmptyTag) - { - } - - internal Exemplar(DateTimeOffset timestamp, IDataValue value) - : this(timestamp, value, default, default, Exemplar.EmptyTag) - { - } - - public DateTimeOffset Timestamp { get; } - - public readonly KeyValuePair[] FilteredTags { get; } - - public readonly ActivityTraceId TraceId { get; } - - public readonly ActivitySpanId SpanId { get; } - - public object Value => this.value.Value; - } -} diff --git a/src/OpenTelemetry/Metrics/ExplicitBucketHistogramConfiguration.cs b/src/OpenTelemetry/Metrics/ExplicitBucketHistogramConfiguration.cs new file mode 100644 index 00000000000..7ed4a0fadd2 --- /dev/null +++ b/src/OpenTelemetry/Metrics/ExplicitBucketHistogramConfiguration.cs @@ -0,0 +1,30 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace OpenTelemetry.Metrics +{ + public class ExplicitBucketHistogramConfiguration : MetricStreamConfiguration + { + /// + /// Gets or sets the values representing explicit histogram bucket + /// boundary values. + /// + /// + /// The array must be in ascending order with distinct values. + /// + public double[] Boundaries { get; set; } + } +} diff --git a/src/OpenTelemetry/Metrics/MetricAggregators/MetricType.cs b/src/OpenTelemetry/Metrics/ExportModes.cs similarity index 56% rename from src/OpenTelemetry/Metrics/MetricAggregators/MetricType.cs rename to src/OpenTelemetry/Metrics/ExportModes.cs index c5af3b5d6ce..ee832bd9047 100644 --- a/src/OpenTelemetry/Metrics/MetricAggregators/MetricType.cs +++ b/src/OpenTelemetry/Metrics/ExportModes.cs @@ -1,4 +1,4 @@ -// +// // Copyright The OpenTelemetry Authors // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,38 +14,34 @@ // limitations under the License. // +using System; + namespace OpenTelemetry.Metrics { - public enum MetricType + [Flags] + public enum ExportModes : byte { - /// - /// Sum of Long type. - /// - LongSum = 0, - - /// - /// Sum of Double type. - /// - DoubleSum = 1, - - /// - /// Gauge of Long type. - /// - LongGauge = 2, - - /// - /// Gauge of Double type. - /// - DoubleGauge = 3, + /* + 0 0 0 0 0 0 0 0 + | | | | | | | | + | | | | | | | +--- Push + | | | | | | +----- Pull + | | | | | +------- (reserved) + | | | | +--------- (reserved) + | | | +----------- (reserved) + | | +------------- (reserved) + | +--------------- (reserved) + +----------------- (reserved) + */ /// - /// Histogram. + /// Push. /// - Histogram = 4, + Push = 0b1, /// - /// Summary. + /// Pull. /// - Summary = 5, + Pull = 0b10, } } diff --git a/src/OpenTelemetry/Metrics/ExportModesAttribute.cs b/src/OpenTelemetry/Metrics/ExportModesAttribute.cs new file mode 100644 index 00000000000..a3a74cf59fd --- /dev/null +++ b/src/OpenTelemetry/Metrics/ExportModesAttribute.cs @@ -0,0 +1,33 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; + +namespace OpenTelemetry.Metrics +{ + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] + public sealed class ExportModesAttribute : Attribute + { + private ExportModes supportedExportModes; + + public ExportModesAttribute(ExportModes supported) + { + this.supportedExportModes = supported; + } + + public ExportModes Supported => this.supportedExportModes; + } +} diff --git a/src/OpenTelemetry/Metrics/MetricAggregators/HistogramBucket.cs b/src/OpenTelemetry/Metrics/HistogramBucket.cs similarity index 51% rename from src/OpenTelemetry/Metrics/MetricAggregators/HistogramBucket.cs rename to src/OpenTelemetry/Metrics/HistogramBucket.cs index b54ae5e9dd2..fc876fa4cf1 100644 --- a/src/OpenTelemetry/Metrics/MetricAggregators/HistogramBucket.cs +++ b/src/OpenTelemetry/Metrics/HistogramBucket.cs @@ -16,10 +16,26 @@ namespace OpenTelemetry.Metrics { - public struct HistogramBucket + /// + /// Represents a bucket in the histogram metric type. + /// + public readonly struct HistogramBucket { - public double LowBoundary; - public double HighBoundary; - public long Count; + internal HistogramBucket(double explicitBound, long bucketCount) + { + this.ExplicitBound = explicitBound; + this.BucketCount = bucketCount; + } + + /// + /// Gets the configured bounds for the bucket or for the catch-all bucket. + /// + public double ExplicitBound { get; } + + /// + /// Gets the count of items in the bucket. + /// + public long BucketCount { get; } } } diff --git a/src/OpenTelemetry/Metrics/HistogramBuckets.cs b/src/OpenTelemetry/Metrics/HistogramBuckets.cs new file mode 100644 index 00000000000..a68033d4300 --- /dev/null +++ b/src/OpenTelemetry/Metrics/HistogramBuckets.cs @@ -0,0 +1,94 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace OpenTelemetry.Metrics +{ + /// + /// A collection of s associated with a histogram metric type. + /// + // Note: Does not implement IEnumerable<> to prevent accidental boxing. + public class HistogramBuckets + { + internal readonly double[] ExplicitBounds; + + internal readonly long[] RunningBucketCounts; + + internal readonly long[] SnapshotBucketCounts; + + internal double RunningSum; + + internal double SnapshotSum; + + internal HistogramBuckets(double[] explicitBounds) + { + this.ExplicitBounds = explicitBounds; + this.RunningBucketCounts = explicitBounds != null ? new long[explicitBounds.Length + 1] : null; + this.SnapshotBucketCounts = explicitBounds != null ? new long[explicitBounds.Length + 1] : new long[0]; + } + + internal object LockObject => this.SnapshotBucketCounts; + + public Enumerator GetEnumerator() => new(this); + + /// + /// Enumerates the elements of a . + /// + // Note: Does not implement IEnumerator<> to prevent accidental boxing. + public struct Enumerator + { + private readonly int numberOfBuckets; + private readonly HistogramBuckets histogramMeasurements; + private int index; + + internal Enumerator(HistogramBuckets histogramMeasurements) + { + this.histogramMeasurements = histogramMeasurements; + this.index = 0; + this.Current = default; + this.numberOfBuckets = histogramMeasurements.SnapshotBucketCounts.Length; + } + + /// + /// Gets the at the current position of the enumerator. + /// + public HistogramBucket Current { get; private set; } + + /// + /// Advances the enumerator to the next element of the . + /// + /// if the enumerator was + /// successfully advanced to the next element; if the enumerator has passed the end of the + /// collection. + public bool MoveNext() + { + if (this.index < this.numberOfBuckets) + { + double explicitBound = this.index < this.numberOfBuckets - 1 + ? this.histogramMeasurements.ExplicitBounds[this.index] + : double.PositiveInfinity; + long bucketCount = this.histogramMeasurements.SnapshotBucketCounts[this.index]; + this.Current = new HistogramBucket(explicitBound, bucketCount); + this.index++; + return true; + } + + return false; + } + } + } +} diff --git a/src/OpenTelemetry/Metrics/MetricAggregators/IAggregator.cs b/src/OpenTelemetry/Metrics/IPullMetricExporter.cs similarity index 68% rename from src/OpenTelemetry/Metrics/MetricAggregators/IAggregator.cs rename to src/OpenTelemetry/Metrics/IPullMetricExporter.cs index da9f3ab54f2..aac55247fdf 100644 --- a/src/OpenTelemetry/Metrics/MetricAggregators/IAggregator.cs +++ b/src/OpenTelemetry/Metrics/IPullMetricExporter.cs @@ -1,4 +1,4 @@ -// +// // Copyright The OpenTelemetry Authors // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,11 +18,11 @@ namespace OpenTelemetry.Metrics { - internal interface IAggregator + /// + /// Describes a type of which supports . + /// + public interface IPullMetricExporter { - void Update(T value) - where T : struct; - - IMetric Collect(DateTimeOffset dt, bool isDelta); + Func Collect { get; set; } } } diff --git a/src/OpenTelemetry/Metrics/MeterProviderBuilderBase.cs b/src/OpenTelemetry/Metrics/MeterProviderBuilderBase.cs new file mode 100644 index 00000000000..18817cf5b66 --- /dev/null +++ b/src/OpenTelemetry/Metrics/MeterProviderBuilderBase.cs @@ -0,0 +1,161 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.Metrics; +using System.Text.RegularExpressions; +using OpenTelemetry.Internal; +using OpenTelemetry.Resources; + +namespace OpenTelemetry.Metrics +{ + /// + /// Build MeterProvider with Resource, Readers, and Instrumentation. + /// + public abstract class MeterProviderBuilderBase : MeterProviderBuilder + { + internal const int MaxMetricsDefault = 1000; + internal const int MaxMetricPointsPerMetricDefault = 2000; + private readonly List instrumentationFactories = new List(); + private readonly List meterSources = new List(); + private readonly List> viewConfigs = new List>(); + private ResourceBuilder resourceBuilder = ResourceBuilder.CreateDefault(); + private int maxMetricStreams = MaxMetricsDefault; + private int maxMetricPointsPerMetricStream = MaxMetricPointsPerMetricDefault; + + protected MeterProviderBuilderBase() + { + } + + internal List MetricReaders { get; } = new List(); + + /// + public override MeterProviderBuilder AddInstrumentation(Func instrumentationFactory) + { + Guard.ThrowIfNull(instrumentationFactory, nameof(instrumentationFactory)); + + this.instrumentationFactories.Add( + new InstrumentationFactory( + typeof(TInstrumentation).Name, + "semver:" + typeof(TInstrumentation).Assembly.GetName().Version, + instrumentationFactory)); + + return this; + } + + /// + public override MeterProviderBuilder AddMeter(params string[] names) + { + Guard.ThrowIfNull(names, nameof(names)); + + foreach (var name in names) + { + Guard.ThrowIfNullOrWhitespace(name, nameof(name)); + + this.meterSources.Add(name); + } + + return this; + } + + internal MeterProviderBuilder AddReader(MetricReader reader) + { + this.MetricReaders.Add(reader); + return this; + } + + internal MeterProviderBuilder AddView(string instrumentName, string name) + { + return this.AddView(instrumentName, new MetricStreamConfiguration() { Name = name }); + } + + internal MeterProviderBuilder AddView(string instrumentName, MetricStreamConfiguration metricStreamConfiguration) + { + if (instrumentName.IndexOf('*') != -1) + { + var pattern = '^' + Regex.Escape(instrumentName).Replace("\\*", ".*"); + var regex = new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase); + return this.AddView(instrument => regex.IsMatch(instrument.Name) ? metricStreamConfiguration : null); + } + else + { + return this.AddView(instrument => instrument.Name.Equals(instrumentName, StringComparison.OrdinalIgnoreCase) ? metricStreamConfiguration : null); + } + } + + internal MeterProviderBuilder AddView(Func viewConfig) + { + this.viewConfigs.Add(viewConfig); + return this; + } + + internal MeterProviderBuilder SetMaxMetricStreams(int maxMetricStreams) + { + Guard.ThrowIfOutOfRange(maxMetricStreams, min: 1); + + this.maxMetricStreams = maxMetricStreams; + return this; + } + + internal MeterProviderBuilder SetMaxMetricPointsPerMetricStream(int maxMetricPointsPerMetricStream) + { + Guard.ThrowIfOutOfRange(maxMetricPointsPerMetricStream, min: 1); + + this.maxMetricPointsPerMetricStream = maxMetricPointsPerMetricStream; + return this; + } + + internal MeterProviderBuilder SetResourceBuilder(ResourceBuilder resourceBuilder) + { + Debug.Assert(resourceBuilder != null, $"{nameof(resourceBuilder)} must not be null"); + + this.resourceBuilder = resourceBuilder; + return this; + } + + /// + /// Run the configured actions to initialize the . + /// + /// . + protected MeterProvider Build() + { + return new MeterProviderSdk( + this.resourceBuilder.Build(), + this.meterSources, + this.instrumentationFactories, + this.viewConfigs, + this.maxMetricStreams, + this.maxMetricPointsPerMetricStream, + this.MetricReaders.ToArray()); + } + + internal readonly struct InstrumentationFactory + { + public readonly string Name; + public readonly string Version; + public readonly Func Factory; + + internal InstrumentationFactory(string name, string version, Func factory) + { + this.Name = name; + this.Version = version; + this.Factory = factory; + } + } + } +} diff --git a/src/OpenTelemetry/Metrics/MeterProviderBuilderExtensions.cs b/src/OpenTelemetry/Metrics/MeterProviderBuilderExtensions.cs index 14bc00754e0..199a11bb499 100644 --- a/src/OpenTelemetry/Metrics/MeterProviderBuilderExtensions.cs +++ b/src/OpenTelemetry/Metrics/MeterProviderBuilderExtensions.cs @@ -14,6 +14,8 @@ // limitations under the License. // +using System; +using System.Diagnostics.Metrics; using OpenTelemetry.Resources; namespace OpenTelemetry.Metrics @@ -24,16 +26,161 @@ namespace OpenTelemetry.Metrics public static class MeterProviderBuilderExtensions { /// - /// Add metric processor. + /// Adds a reader to the provider. /// /// . - /// Measurement Processors. + /// . /// . - public static MeterProviderBuilder AddMetricProcessor(this MeterProviderBuilder meterProviderBuilder, MetricProcessor processor) + public static MeterProviderBuilder AddReader(this MeterProviderBuilder meterProviderBuilder, MetricReader reader) { - if (meterProviderBuilder is MeterProviderBuilderSdk meterProviderBuilderSdk) + if (meterProviderBuilder is MeterProviderBuilderBase meterProviderBuilderBase) + { + return meterProviderBuilderBase.AddReader(reader); + } + + return meterProviderBuilder; + } + + /// + /// Add metric view, which can be used to customize the Metrics outputted + /// from the SDK. The views are applied in the order they are added. + /// + /// . + /// Name of the instrument, to be used as part of Instrument selection criteria. + /// Name of the view. This will be used as name of resulting metrics stream. + /// . + /// See View specification here : https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#view. + public static MeterProviderBuilder AddView(this MeterProviderBuilder meterProviderBuilder, string instrumentName, string name) + { + if (!MeterProviderBuilderSdk.IsValidInstrumentName(name)) + { + throw new ArgumentException($"Custom view name {name} is invalid.", nameof(name)); + } + + if (instrumentName.IndexOf('*') != -1) + { + throw new ArgumentException( + $"Instrument selection criteria is invalid. Instrument name '{instrumentName}' " + + $"contains a wildcard character. This is not allowed when using a view to " + + $"rename a metric stream as it would lead to conflicting metric stream names.", + nameof(instrumentName)); + } + + if (meterProviderBuilder is MeterProviderBuilderBase meterProviderBuilderBase) + { + return meterProviderBuilderBase.AddView(instrumentName, name); + } + + return meterProviderBuilder; + } + + /// + /// Add metric view, which can be used to customize the Metrics outputted + /// from the SDK. The views are applied in the order they are added. + /// + /// . + /// Name of the instrument, to be used as part of Instrument selection criteria. + /// Aggregation configuration used to produce metrics stream. + /// . + /// See View specification here : https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#view. + public static MeterProviderBuilder AddView(this MeterProviderBuilder meterProviderBuilder, string instrumentName, MetricStreamConfiguration metricStreamConfiguration) + { + if (metricStreamConfiguration == null) + { + throw new ArgumentNullException($"Metric stream configuration cannot be null.", nameof(metricStreamConfiguration)); + } + + if (!MeterProviderBuilderSdk.IsValidViewName(metricStreamConfiguration.Name)) + { + throw new ArgumentException($"Custom view name {metricStreamConfiguration.Name} is invalid.", nameof(metricStreamConfiguration.Name)); + } + + if (metricStreamConfiguration.Name != null && instrumentName.IndexOf('*') != -1) + { + throw new ArgumentException( + $"Instrument selection criteria is invalid. Instrument name '{instrumentName}' " + + $"contains a wildcard character. This is not allowed when using a view to " + + $"rename a metric stream as it would lead to conflicting metric stream names.", + nameof(instrumentName)); + } + + if (metricStreamConfiguration is ExplicitBucketHistogramConfiguration histogramConfiguration) + { + // Validate histogram boundaries + if (histogramConfiguration.Boundaries != null && !IsSortedAndDistinct(histogramConfiguration.Boundaries)) + { + throw new ArgumentException($"Histogram boundaries must be in ascending order with distinct values", nameof(histogramConfiguration.Boundaries)); + } + } + + if (meterProviderBuilder is MeterProviderBuilderBase meterProviderBuilderBase) + { + return meterProviderBuilderBase.AddView(instrumentName, metricStreamConfiguration); + } + + return meterProviderBuilder; + } + + /// + /// Add metric view, which can be used to customize the Metrics outputted + /// from the SDK. The views are applied in the order they are added. + /// + /// . + /// Function to configure aggregation based on the instrument. + /// . + /// See View specification here : https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/metrics/sdk.md#view. + public static MeterProviderBuilder AddView(this MeterProviderBuilder meterProviderBuilder, Func viewConfig) + { + if (meterProviderBuilder is MeterProviderBuilderBase meterProviderBuilderBase) + { + return meterProviderBuilderBase.AddView(viewConfig); + } + + return meterProviderBuilder; + } + + /// + /// Sets the maximum number of Metric streams supported by the MeterProvider. + /// When no Views are configured, every instrument will result in one metric stream, + /// so this control the numbers of instruments supported. + /// When Views are configued, a single instrument can result in multiple metric streams, + /// so this control the number of streams. + /// + /// MeterProviderBuilder instance. + /// Maximum number of metric streams allowed. + /// Returns for chaining. + /// + /// If an instrument is created, but disposed later, this will still be contributing to the limit. + /// This may change in the future. + /// + public static MeterProviderBuilder SetMaxMetricStreams(this MeterProviderBuilder meterProviderBuilder, int maxMetricStreams) + { + if (meterProviderBuilder is MeterProviderBuilderBase meterProviderBuilderBase) { - return meterProviderBuilderSdk.AddMetricProcessor(processor); + meterProviderBuilderBase.SetMaxMetricStreams(maxMetricStreams); + } + + return meterProviderBuilder; + } + + /// + /// Sets the maximum number of MetricPoints allowed per metric stream. + /// This limits the number of unique combinations of key/value pairs used + /// for reporting measurements. + /// + /// MeterProviderBuilder instance. + /// Maximum maximum number of metric points allowed per metric stream. + /// Returns for chaining. + /// + /// If a particular key/value pair combination is used at least once, + /// it will contribute to the limit for the life of the process. + /// This may change in the future. See: https://github.com/open-telemetry/opentelemetry-dotnet/issues/2360. + /// + public static MeterProviderBuilder SetMaxMetricPointsPerMetricStream(this MeterProviderBuilder meterProviderBuilder, int maxMetricPointsPerMetricStream) + { + if (meterProviderBuilder is MeterProviderBuilderBase meterProviderBuilderBase) + { + meterProviderBuilderBase.SetMaxMetricPointsPerMetricStream(maxMetricPointsPerMetricStream); } return meterProviderBuilder; @@ -48,9 +195,9 @@ public static MeterProviderBuilder AddMetricProcessor(this MeterProviderBuilder /// Returns for chaining. public static MeterProviderBuilder SetResourceBuilder(this MeterProviderBuilder meterProviderBuilder, ResourceBuilder resourceBuilder) { - if (meterProviderBuilder is MeterProviderBuilderSdk meterProviderBuilderSdk) + if (meterProviderBuilder is MeterProviderBuilderBase meterProviderBuilderBase) { - meterProviderBuilderSdk.SetResourceBuilder(resourceBuilder); + meterProviderBuilderBase.SetResourceBuilder(resourceBuilder); } return meterProviderBuilder; @@ -63,12 +210,30 @@ public static MeterProviderBuilder SetResourceBuilder(this MeterProviderBuilder /// . public static MeterProvider Build(this MeterProviderBuilder meterProviderBuilder) { + if (meterProviderBuilder is IDeferredMeterProviderBuilder) + { + throw new NotSupportedException("DeferredMeterProviderBuilder requires a ServiceProvider to build."); + } + if (meterProviderBuilder is MeterProviderBuilderSdk meterProviderBuilderSdk) { - return meterProviderBuilderSdk.Build(); + return meterProviderBuilderSdk.BuildSdk(); } return null; } + + private static bool IsSortedAndDistinct(double[] values) + { + for (int i = 1; i < values.Length; i++) + { + if (values[i] <= values[i - 1]) + { + return false; + } + } + + return true; + } } } diff --git a/src/OpenTelemetry/Metrics/MeterProviderBuilderSdk.cs b/src/OpenTelemetry/Metrics/MeterProviderBuilderSdk.cs index fd8055a2694..47bd282cd96 100644 --- a/src/OpenTelemetry/Metrics/MeterProviderBuilderSdk.cs +++ b/src/OpenTelemetry/Metrics/MeterProviderBuilderSdk.cs @@ -14,99 +14,48 @@ // limitations under the License. // -using System; -using System.Collections.Generic; -using OpenTelemetry.Resources; +using System.Text.RegularExpressions; namespace OpenTelemetry.Metrics { - internal class MeterProviderBuilderSdk : MeterProviderBuilder + internal class MeterProviderBuilderSdk : MeterProviderBuilderBase { - private readonly List instrumentationFactories = new List(); - private readonly List meterSources = new List(); - private ResourceBuilder resourceBuilder = ResourceBuilder.CreateDefault(); - - internal MeterProviderBuilderSdk() + private static readonly Regex InstrumentNameRegex = new Regex( + @"^[a-zA-Z][-.\w]{0,62}$", RegexOptions.IgnoreCase | RegexOptions.Compiled); + + /// + /// Returns whether the given instrument name is valid according to the specification. + /// + /// See specification: . + /// The instrument name. + /// Boolean indicating if the instrument is valid. + internal static bool IsValidInstrumentName(string instrumentName) { - } - - internal List MetricProcessors { get; } = new List(); - - public override MeterProviderBuilder AddInstrumentation(Func instrumentationFactory) - { - if (instrumentationFactory == null) + if (string.IsNullOrWhiteSpace(instrumentName)) { - throw new ArgumentNullException(nameof(instrumentationFactory)); + return false; } - this.instrumentationFactories.Add( - new InstrumentationFactory( - typeof(TInstrumentation).Name, - "semver:" + typeof(TInstrumentation).Assembly.GetName().Version, - instrumentationFactory)); - - return this; + return InstrumentNameRegex.IsMatch(instrumentName); } - public override MeterProviderBuilder AddSource(params string[] names) + /// + /// Returns whether the given custom view name is valid according to the specification. + /// + /// See specification: . + /// The view name. + /// Boolean indicating if the instrument is valid. + internal static bool IsValidViewName(string customViewName) { - if (names == null) - { - throw new ArgumentNullException(nameof(names)); - } - - foreach (var name in names) + // Only validate the view name in case it's not null. In case it's null, the view name will be the instrument name as per the spec. + if (customViewName == null) { - if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentException($"{nameof(names)} contains null or whitespace string."); - } - - this.meterSources.Add(name); + return true; } - return this; + return InstrumentNameRegex.IsMatch(customViewName); } - internal MeterProviderBuilderSdk AddMetricProcessor(MetricProcessor processor) - { - if (this.MetricProcessors.Count >= 1) - { - throw new InvalidOperationException("Only one MetricProcessor is allowed."); - } - - this.MetricProcessors.Add(processor); - return this; - } - - internal MeterProviderBuilderSdk SetResourceBuilder(ResourceBuilder resourceBuilder) - { - this.resourceBuilder = resourceBuilder ?? throw new ArgumentNullException(nameof(resourceBuilder)); - return this; - } - - internal MeterProvider Build() - { - return new MeterProviderSdk( - this.resourceBuilder.Build(), - this.meterSources, - this.instrumentationFactories, - this.MetricProcessors.ToArray()); - } - - // TODO: This is copied from TracerProviderBuilderSdk. Move to common location. - internal readonly struct InstrumentationFactory - { - public readonly string Name; - public readonly string Version; - public readonly Func Factory; - - internal InstrumentationFactory(string name, string version, Func factory) - { - this.Name = name; - this.Version = version; - this.Factory = factory; - } - } + internal MeterProvider BuildSdk() => this.Build(); } } diff --git a/src/OpenTelemetry/Metrics/MeterProviderExtensions.cs b/src/OpenTelemetry/Metrics/MeterProviderExtensions.cs new file mode 100644 index 00000000000..d9f6b99dbe1 --- /dev/null +++ b/src/OpenTelemetry/Metrics/MeterProviderExtensions.cs @@ -0,0 +1,144 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Threading; +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Metrics +{ + public static class MeterProviderExtensions + { + /// + /// Flushes all the readers registered under MeterProviderSdk, blocks the current thread + /// until flush completed, shutdown signaled or timed out. + /// + /// MeterProviderSdk instance on which ForceFlush will be called. + /// + /// The number (non-negative) of milliseconds to wait, or + /// Timeout.Infinite to wait indefinitely. + /// + /// + /// Returns true when force flush succeeded; otherwise, false. + /// + /// + /// Thrown when the timeoutMilliseconds is smaller than -1. + /// + /// + /// This function guarantees thread-safety. + /// + public static bool ForceFlush(this MeterProvider provider, int timeoutMilliseconds = Timeout.Infinite) + { + Guard.ThrowIfNull(provider, nameof(provider)); + Guard.ThrowIfInvalidTimeout(timeoutMilliseconds, nameof(timeoutMilliseconds)); + + if (provider is MeterProviderSdk meterProviderSdk) + { + try + { + return meterProviderSdk.OnForceFlush(timeoutMilliseconds); + } + catch (Exception ex) + { + OpenTelemetrySdkEventSource.Log.MeterProviderException(nameof(meterProviderSdk.OnForceFlush), ex); + return false; + } + } + + return true; + } + + /// + /// Attempts to shutdown the MeterProviderSdk, blocks the current thread until + /// shutdown completed or timed out. + /// + /// MeterProviderSdk instance on which Shutdown will be called. + /// + /// The number (non-negative) of milliseconds to wait, or + /// Timeout.Infinite to wait indefinitely. + /// + /// + /// Returns true when shutdown succeeded; otherwise, false. + /// + /// + /// Thrown when the timeoutMilliseconds is smaller than -1. + /// + /// + /// This function guarantees thread-safety. Only the first call will + /// win, subsequent calls will be no-op. + /// + public static bool Shutdown(this MeterProvider provider, int timeoutMilliseconds = Timeout.Infinite) + { + Guard.ThrowIfNull(provider, nameof(provider)); + Guard.ThrowIfInvalidTimeout(timeoutMilliseconds, nameof(timeoutMilliseconds)); + + if (provider is MeterProviderSdk meterProviderSdk) + { + if (Interlocked.Increment(ref meterProviderSdk.ShutdownCount) > 1) + { + return false; // shutdown already called + } + + try + { + return meterProviderSdk.OnShutdown(timeoutMilliseconds); + } + catch (Exception ex) + { + OpenTelemetrySdkEventSource.Log.MeterProviderException(nameof(meterProviderSdk.OnShutdown), ex); + return false; + } + } + + return true; + } + + public static bool TryFindExporter(this MeterProvider provider, out T exporter) + where T : BaseExporter + { + if (provider is MeterProviderSdk meterProviderSdk) + { + return TryFindExporter(meterProviderSdk.Reader, out exporter); + } + + exporter = null; + return false; + + static bool TryFindExporter(MetricReader reader, out T exporter) + { + if (reader is BaseExportingMetricReader exportingMetricReader) + { + exporter = exportingMetricReader.Exporter as T; + return exporter != null; + } + + if (reader is CompositeMetricReader compositeMetricReader) + { + foreach (MetricReader childReader in compositeMetricReader) + { + if (TryFindExporter(childReader, out exporter)) + { + return true; + } + } + } + + exporter = null; + return false; + } + } + } +} diff --git a/src/OpenTelemetry/Metrics/MeterProviderSdk.cs b/src/OpenTelemetry/Metrics/MeterProviderSdk.cs index 027776a7853..6b4956dde74 100644 --- a/src/OpenTelemetry/Metrics/MeterProviderSdk.cs +++ b/src/OpenTelemetry/Metrics/MeterProviderSdk.cs @@ -15,41 +15,63 @@ // using System; -using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.Metrics; using System.Linq; +using System.Text.RegularExpressions; +using OpenTelemetry.Internal; using OpenTelemetry.Resources; namespace OpenTelemetry.Metrics { - public class MeterProviderSdk - : MeterProvider + internal sealed class MeterProviderSdk : MeterProvider { - internal readonly ConcurrentDictionary AggregatorStores = new ConcurrentDictionary(); - + internal int ShutdownCount; private readonly List instrumentations = new List(); + private readonly List> viewConfigs; private readonly object collectLock = new object(); private readonly MeterListener listener; - private readonly List metricProcessors = new List(); + private readonly MetricReader reader; + private readonly CompositeMetricReader compositeMetricReader; + private bool disposed; internal MeterProviderSdk( Resource resource, IEnumerable meterSources, - List instrumentationFactories, - MetricProcessor[] metricProcessors) + List instrumentationFactories, + List> viewConfigs, + int maxMetricStreams, + int maxMetricPointsPerMetricStream, + IEnumerable readers) { this.Resource = resource; + this.viewConfigs = viewConfigs; - // TODO: Replace with single CompositeProcessor. - this.metricProcessors.AddRange(metricProcessors); - - foreach (var processor in this.metricProcessors) + foreach (var reader in readers) { - processor.SetGetMetricFunction(this.Collect); - processor.SetParentProvider(this); + Guard.ThrowIfNull(reader, nameof(reader)); + + reader.SetParentProvider(this); + reader.SetMaxMetricStreams(maxMetricStreams); + reader.SetMaxMetricPointsPerMetricStream(maxMetricPointsPerMetricStream); + + if (this.reader == null) + { + this.reader = reader; + } + else if (this.reader is CompositeMetricReader compositeReader) + { + compositeReader.AddReader(reader); + } + else + { + this.reader = new CompositeMetricReader(new[] { this.reader, reader }); + } } + this.compositeMetricReader = this.reader as CompositeMetricReader; + if (instrumentationFactories.Any()) { foreach (var instrumentationFactory in instrumentationFactories) @@ -59,111 +81,419 @@ internal MeterProviderSdk( } // Setup Listener - var meterSourcesToSubscribe = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var name in meterSources) + Func shouldListenTo = instrument => false; + if (meterSources.Any(s => s.Contains('*'))) { - meterSourcesToSubscribe[name] = true; + var regex = GetWildcardRegex(meterSources); + shouldListenTo = instrument => regex.IsMatch(instrument.Meter.Name); } + else if (meterSources.Any()) + { + var meterSourcesToSubscribe = new HashSet(meterSources, StringComparer.OrdinalIgnoreCase); + shouldListenTo = instrument => meterSourcesToSubscribe.Contains(instrument.Meter.Name); + } + + this.listener = new MeterListener(); + var viewConfigCount = this.viewConfigs.Count; - this.listener = new MeterListener() + // We expect that all the readers to be added are provided before MeterProviderSdk is built. + // If there are no readers added, we do not enable measurements for the instruments. + if (viewConfigCount > 0) { - InstrumentPublished = (instrument, listener) => + this.listener.InstrumentPublished = (instrument, listener) => { - if (meterSourcesToSubscribe.ContainsKey(instrument.Meter.Name)) + if (!shouldListenTo(instrument)) { - var aggregatorStore = new AggregatorStore(instrument); + OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(instrument.Name, instrument.Meter.Name, "Instrument belongs to a Meter not subscribed by the provider.", "Use AddMeter to add the Meter to the provider."); + return; + } - // Lock to prevent new instrument (aggregatorstore) - // from being added while Collect is going on. - lock (this.collectLock) + try + { + // Creating list with initial capacity as the maximum + // possible size, to avoid any array resize/copy internally. + // There may be excess space wasted, but it'll eligible for + // GC right after this method. + var metricStreamConfigs = new List(viewConfigCount); + foreach (var viewConfig in this.viewConfigs) { - this.AggregatorStores.TryAdd(aggregatorStore, true); - listener.EnableMeasurementEvents(instrument, aggregatorStore); + var metricStreamConfig = viewConfig(instrument); + if (metricStreamConfig != null) + { + metricStreamConfigs.Add(metricStreamConfig); + } + } + + if (metricStreamConfigs.Count == 0) + { + // No views matched. Add null + // which will apply defaults. + // Users can turn off this default + // by adding a view like below as the last view. + // .AddView(instrumentName: "*", MetricStreamConfiguration.Drop) + metricStreamConfigs.Add(null); + } + + if (this.reader != null) + { + if (this.compositeMetricReader == null) + { + var metrics = this.reader.AddMetricsListWithViews(instrument, metricStreamConfigs); + if (metrics.Count > 0) + { + listener.EnableMeasurementEvents(instrument, metrics); + } + } + else + { + var metricsSuperList = this.compositeMetricReader.AddMetricsSuperListWithViews(instrument, metricStreamConfigs); + if (metricsSuperList.Any(metrics => metrics.Count > 0)) + { + listener.EnableMeasurementEvents(instrument, metricsSuperList); + } + } } } - }, - MeasurementsCompleted = (instrument, state) => this.MeasurementsCompleted(instrument, state), - }; + catch (Exception) + { + OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(instrument.Name, instrument.Meter.Name, "SDK internal error occurred.", "Contact SDK owners."); + } + }; + + // Everything double + this.listener.SetMeasurementEventCallback(this.MeasurementRecordedDouble); + this.listener.SetMeasurementEventCallback((instrument, value, tags, state) => this.MeasurementRecordedDouble(instrument, value, tags, state)); - // Everything double - this.listener.SetMeasurementEventCallback((i, m, l, c) => this.MeasurementRecorded(i, m, l, c)); - this.listener.SetMeasurementEventCallback((i, m, l, c) => this.MeasurementRecorded(i, (double)m, l, c)); + // Everything long + this.listener.SetMeasurementEventCallback(this.MeasurementRecordedLong); + this.listener.SetMeasurementEventCallback((instrument, value, tags, state) => this.MeasurementRecordedLong(instrument, value, tags, state)); + this.listener.SetMeasurementEventCallback((instrument, value, tags, state) => this.MeasurementRecordedLong(instrument, value, tags, state)); + this.listener.SetMeasurementEventCallback((instrument, value, tags, state) => this.MeasurementRecordedLong(instrument, value, tags, state)); - // Everything long - this.listener.SetMeasurementEventCallback((i, m, l, c) => this.MeasurementRecorded(i, m, l, c)); - this.listener.SetMeasurementEventCallback((i, m, l, c) => this.MeasurementRecorded(i, (long)m, l, c)); - this.listener.SetMeasurementEventCallback((i, m, l, c) => this.MeasurementRecorded(i, (long)m, l, c)); - this.listener.SetMeasurementEventCallback((i, m, l, c) => this.MeasurementRecorded(i, (long)m, l, c)); + this.listener.MeasurementsCompleted = (instrument, state) => this.MeasurementsCompleted(instrument, state); + } + else + { + this.listener.InstrumentPublished = (instrument, listener) => + { + if (!shouldListenTo(instrument)) + { + OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(instrument.Name, instrument.Meter.Name, "Instrument belongs to a Meter not subscribed by the provider.", "Use AddMeter to add the Meter to the provider."); + return; + } + + try + { + if (!MeterProviderBuilderSdk.IsValidInstrumentName(instrument.Name)) + { + OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored( + instrument.Name, + instrument.Meter.Name, + "Instrument name is invalid.", + "The name must comply with the OpenTelemetry specification"); + + return; + } + + if (this.reader != null) + { + if (this.compositeMetricReader == null) + { + var metric = this.reader.AddMetricWithNoViews(instrument); + if (metric != null) + { + listener.EnableMeasurementEvents(instrument, metric); + } + } + else + { + var metrics = this.compositeMetricReader.AddMetricsWithNoViews(instrument); + if (metrics.Any(metric => metric != null)) + { + listener.EnableMeasurementEvents(instrument, metrics); + } + } + } + } + catch (Exception) + { + OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(instrument.Name, instrument.Meter.Name, "SDK internal error occurred.", "Contact SDK owners."); + } + }; + + // Everything double + this.listener.SetMeasurementEventCallback(this.MeasurementRecordedDoubleSingleStream); + this.listener.SetMeasurementEventCallback((instrument, value, tags, state) => this.MeasurementRecordedDoubleSingleStream(instrument, value, tags, state)); + + // Everything long + this.listener.SetMeasurementEventCallback(this.MeasurementRecordedLongSingleStream); + this.listener.SetMeasurementEventCallback((instrument, value, tags, state) => this.MeasurementRecordedLongSingleStream(instrument, value, tags, state)); + this.listener.SetMeasurementEventCallback((instrument, value, tags, state) => this.MeasurementRecordedLongSingleStream(instrument, value, tags, state)); + this.listener.SetMeasurementEventCallback((instrument, value, tags, state) => this.MeasurementRecordedLongSingleStream(instrument, value, tags, state)); + + this.listener.MeasurementsCompleted = (instrument, state) => this.MeasurementsCompletedSingleStream(instrument, state); + } this.listener.Start(); + + static Regex GetWildcardRegex(IEnumerable collection) + { + var pattern = '^' + string.Join("|", from name in collection select "(?:" + Regex.Escape(name).Replace("\\*", ".*") + ')') + '$'; + return new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase); + } } internal Resource Resource { get; } + internal List Instrumentations => this.instrumentations; + + internal MetricReader Reader => this.reader; + + internal void MeasurementsCompletedSingleStream(Instrument instrument, object state) + { + Debug.Assert(instrument != null, "instrument must be non-null."); + + if (this.compositeMetricReader == null) + { + if (state is not Metric metric) + { + // TODO: log + return; + } + + this.reader.CompleteSingleStreamMeasurement(metric); + } + else + { + if (state is not List metrics) + { + // TODO: log + return; + } + + this.compositeMetricReader.CompleteSingleStreamMeasurements(metrics); + } + } + internal void MeasurementsCompleted(Instrument instrument, object state) { - Console.WriteLine($"Instrument {instrument.Meter.Name}:{instrument.Name} completed."); + Debug.Assert(instrument != null, "instrument must be non-null."); + + if (this.compositeMetricReader == null) + { + if (state is not List metrics) + { + // TODO: log + return; + } + + this.reader.CompleteMeasurement(metrics); + } + else + { + if (state is not List> metricsSuperList) + { + // TODO: log + return; + } + + this.compositeMetricReader.CompleteMesaurements(metricsSuperList); + } } - internal void MeasurementRecorded(Instrument instrument, T value, ReadOnlySpan> tagsRos, object state) - where T : struct + internal void MeasurementRecordedDouble(Instrument instrument, double value, ReadOnlySpan> tagsRos, object state) { - // Get Instrument State - var aggregatorStore = state as AggregatorStore; + Debug.Assert(instrument != null, "instrument must be non-null."); - if (instrument == null || aggregatorStore == null) + if (this.compositeMetricReader == null) { - // TODO: log - return; + if (state is not List metrics) + { + OpenTelemetrySdkEventSource.Log.MeasurementDropped(instrument.Name, "SDK internal error occurred.", "Contact SDK owners."); + return; + } + + this.reader.RecordDoubleMeasurement(metrics, value, tagsRos); } + else + { + if (state is not List> metricsSuperList) + { + OpenTelemetrySdkEventSource.Log.MeasurementDropped(instrument.Name, "SDK internal error occurred.", "Contact SDK owners."); + return; + } - aggregatorStore.Update(value, tagsRos); + this.compositeMetricReader.RecordDoubleMeasurements(metricsSuperList, value, tagsRos); + } } - protected override void Dispose(bool disposing) + internal void MeasurementRecordedLong(Instrument instrument, long value, ReadOnlySpan> tagsRos, object state) + { + Debug.Assert(instrument != null, "instrument must be non-null."); + + if (this.compositeMetricReader == null) + { + if (state is not List metrics) + { + OpenTelemetrySdkEventSource.Log.MeasurementDropped(instrument.Name, "SDK internal error occurred.", "Contact SDK owners."); + return; + } + + this.reader.RecordLongMeasurement(metrics, value, tagsRos); + } + else + { + if (state is not List> metricsSuperList) + { + OpenTelemetrySdkEventSource.Log.MeasurementDropped(instrument.Name, "SDK internal error occurred.", "Contact SDK owners."); + return; + } + + this.compositeMetricReader.RecordLongMeasurements(metricsSuperList, value, tagsRos); + } + } + + internal void MeasurementRecordedLongSingleStream(Instrument instrument, long value, ReadOnlySpan> tagsRos, object state) { - if (this.instrumentations != null) + Debug.Assert(instrument != null, "instrument must be non-null."); + + if (this.compositeMetricReader == null) + { + if (state is not Metric metric) + { + OpenTelemetrySdkEventSource.Log.MeasurementDropped(instrument.Name, "SDK internal error occurred.", "Contact SDK owners."); + return; + } + + this.reader.RecordSingleStreamLongMeasurement(metric, value, tagsRos); + } + else { - foreach (var item in this.instrumentations) + if (state is not List metrics) { - (item as IDisposable)?.Dispose(); + OpenTelemetrySdkEventSource.Log.MeasurementDropped(instrument.Name, "SDK internal error occurred.", "Contact SDK owners."); + return; } - this.instrumentations.Clear(); + this.compositeMetricReader.RecordSingleStreamLongMeasurements(metrics, value, tagsRos); } + } + + internal void MeasurementRecordedDoubleSingleStream(Instrument instrument, double value, ReadOnlySpan> tagsRos, object state) + { + Debug.Assert(instrument != null, "instrument must be non-null."); - foreach (var processor in this.metricProcessors) + if (this.compositeMetricReader == null) { - processor.Dispose(); + if (state is not Metric metric) + { + OpenTelemetrySdkEventSource.Log.MeasurementDropped(instrument.Name, "SDK internal error occurred.", "Contact SDK owners."); + return; + } + + this.reader.RecordSingleStreamDoubleMeasurement(metric, value, tagsRos); } + else + { + if (state is not List metrics) + { + OpenTelemetrySdkEventSource.Log.MeasurementDropped(instrument.Name, "SDK internal error occurred.", "Contact SDK owners."); + return; + } - this.listener.Dispose(); + this.compositeMetricReader.RecordSingleStreamDoubleMeasurements(metrics, value, tagsRos); + } } - private MetricItem Collect(bool isDelta) + internal void CollectObservableInstruments() { lock (this.collectLock) { - MetricItem metricItem = null; + // Record all observable instruments try { - // Record all observable instruments this.listener.RecordObservableInstruments(); - var dt = DateTimeOffset.UtcNow; - metricItem = new MetricItem(); - foreach (var kv in this.AggregatorStores) - { - var metrics = kv.Key.Collect(isDelta, dt); - metricItem.Metrics.AddRange(metrics); - } } - catch (Exception) + catch (Exception exception) + { + // TODO: + // It doesn't looks like we can find which instrument callback + // threw. + OpenTelemetrySdkEventSource.Log.MetricObserverCallbackException(exception); + } + } + } + + /// + /// Called by ForceFlush. This function should block the current + /// thread until flush completed or timed out. + /// + /// + /// The number (non-negative) of milliseconds to wait, or + /// Timeout.Infinite to wait indefinitely. + /// + /// + /// Returns true when flush succeeded; otherwise, false. + /// + /// + /// This function is called synchronously on the thread which made the + /// first call to ForceFlush. This function should not throw + /// exceptions. + /// + internal bool OnForceFlush(int timeoutMilliseconds) + { + return this.reader?.Collect(timeoutMilliseconds) ?? true; + } + + /// + /// Called by Shutdown. This function should block the current + /// thread until shutdown completed or timed out. + /// + /// + /// The number (non-negative) of milliseconds to wait, or + /// Timeout.Infinite to wait indefinitely. + /// + /// + /// Returns true when shutdown succeeded; otherwise, false. + /// + /// + /// This function is called synchronously on the thread which made the + /// first call to Shutdown. This function should not throw + /// exceptions. + /// + internal bool OnShutdown(int timeoutMilliseconds) + { + return this.reader?.Shutdown(timeoutMilliseconds) ?? true; + } + + protected override void Dispose(bool disposing) + { + if (!this.disposed) + { + if (disposing) { - // TODO: Log + if (this.instrumentations != null) + { + foreach (var item in this.instrumentations) + { + (item as IDisposable)?.Dispose(); + } + + this.instrumentations.Clear(); + } + + // Wait for up to 5 seconds grace period + this.reader?.Shutdown(5000); + this.reader?.Dispose(); + this.compositeMetricReader?.Dispose(); + + this.listener.Dispose(); } - return metricItem; + this.disposed = true; } + + base.Dispose(disposing); } } } diff --git a/src/OpenTelemetry/Metrics/Metric.cs b/src/OpenTelemetry/Metrics/Metric.cs new file mode 100644 index 00000000000..983976d5e9e --- /dev/null +++ b/src/OpenTelemetry/Metrics/Metric.cs @@ -0,0 +1,149 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using System.Diagnostics.Metrics; + +namespace OpenTelemetry.Metrics +{ + public sealed class Metric + { + internal static readonly double[] DefaultHistogramBounds = new double[] { 0, 5, 10, 25, 50, 75, 100, 250, 500, 1000 }; + + private readonly AggregatorStore aggStore; + + internal Metric( + Instrument instrument, + AggregationTemporality temporality, + string metricName, + string metricDescription, + int maxMetricPointsPerMetricStream, + double[] histogramBounds = null, + string[] tagKeysInteresting = null) + { + this.Name = metricName; + this.Description = metricDescription ?? string.Empty; + this.Unit = instrument.Unit ?? string.Empty; + this.Meter = instrument.Meter; + + AggregationType aggType; + if (instrument.GetType() == typeof(ObservableCounter) + || instrument.GetType() == typeof(ObservableCounter) + || instrument.GetType() == typeof(ObservableCounter) + || instrument.GetType() == typeof(ObservableCounter)) + { + aggType = AggregationType.LongSumIncomingCumulative; + this.MetricType = MetricType.LongSum; + } + else if (instrument.GetType() == typeof(Counter) + || instrument.GetType() == typeof(Counter) + || instrument.GetType() == typeof(Counter) + || instrument.GetType() == typeof(Counter)) + { + aggType = AggregationType.LongSumIncomingDelta; + this.MetricType = MetricType.LongSum; + } + else if (instrument.GetType() == typeof(Counter) + || instrument.GetType() == typeof(Counter)) + { + aggType = AggregationType.DoubleSumIncomingDelta; + this.MetricType = MetricType.DoubleSum; + } + else if (instrument.GetType() == typeof(ObservableCounter) + || instrument.GetType() == typeof(ObservableCounter)) + { + aggType = AggregationType.DoubleSumIncomingCumulative; + this.MetricType = MetricType.DoubleSum; + } + else if (instrument.GetType() == typeof(ObservableGauge) + || instrument.GetType() == typeof(ObservableGauge)) + { + aggType = AggregationType.DoubleGauge; + this.MetricType = MetricType.DoubleGauge; + } + else if (instrument.GetType() == typeof(ObservableGauge) + || instrument.GetType() == typeof(ObservableGauge) + || instrument.GetType() == typeof(ObservableGauge) + || instrument.GetType() == typeof(ObservableGauge)) + { + aggType = AggregationType.LongGauge; + this.MetricType = MetricType.LongGauge; + } + else if (instrument.GetType() == typeof(Histogram) + || instrument.GetType() == typeof(Histogram) + || instrument.GetType() == typeof(Histogram) + || instrument.GetType() == typeof(Histogram) + || instrument.GetType() == typeof(Histogram) + || instrument.GetType() == typeof(Histogram)) + { + this.MetricType = MetricType.Histogram; + + if (histogramBounds != null + && histogramBounds.Length == 0) + { + aggType = AggregationType.HistogramSumCount; + } + else + { + aggType = AggregationType.Histogram; + } + } + else + { + throw new NotSupportedException($"Unsupported Instrument Type: {instrument.GetType().FullName}"); + } + + this.aggStore = new AggregatorStore(metricName, aggType, temporality, maxMetricPointsPerMetricStream, histogramBounds ?? DefaultHistogramBounds, tagKeysInteresting); + this.Temporality = temporality; + this.InstrumentDisposed = false; + } + + public MetricType MetricType { get; private set; } + + public AggregationTemporality Temporality { get; private set; } + + public string Name { get; private set; } + + public string Description { get; private set; } + + public string Unit { get; private set; } + + public Meter Meter { get; private set; } + + internal bool InstrumentDisposed { get; set; } + + public MetricPointsAccessor GetMetricPoints() + { + return this.aggStore.GetMetricPoints(); + } + + internal void UpdateLong(long value, ReadOnlySpan> tags) + { + this.aggStore.Update(value, tags); + } + + internal void UpdateDouble(double value, ReadOnlySpan> tags) + { + this.aggStore.Update(value, tags); + } + + internal int Snapshot() + { + return this.aggStore.Snapshot(); + } + } +} diff --git a/src/OpenTelemetry/Metrics/MetricAggregators/GaugeMetricAggregator.cs b/src/OpenTelemetry/Metrics/MetricAggregators/GaugeMetricAggregator.cs deleted file mode 100644 index 2a7848a2b54..00000000000 --- a/src/OpenTelemetry/Metrics/MetricAggregators/GaugeMetricAggregator.cs +++ /dev/null @@ -1,89 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -using System; -using System.Collections.Generic; -using System.Diagnostics.Metrics; - -namespace OpenTelemetry.Metrics -{ - internal class GaugeMetricAggregator : IGaugeMetric, IAggregator - { - private readonly object lockUpdate = new object(); - private IDataValue value; - - internal GaugeMetricAggregator(string name, string description, string unit, Meter meter, DateTimeOffset startTimeExclusive, KeyValuePair[] attributes) - { - this.Name = name; - this.Description = description; - this.Unit = unit; - this.Meter = meter; - this.StartTimeExclusive = startTimeExclusive; - this.Attributes = attributes; - - // TODO: Split this class into two or leverage generic - this.MetricType = MetricType.LongGauge; - } - - public string Name { get; private set; } - - public string Description { get; private set; } - - public string Unit { get; private set; } - - public Meter Meter { get; private set; } - - public DateTimeOffset StartTimeExclusive { get; private set; } - - public DateTimeOffset EndTimeInclusive { get; private set; } - - public KeyValuePair[] Attributes { get; private set; } - - public IEnumerable Exemplars { get; private set; } = new List(); - - public IDataValue LastValue => this.value; - - public MetricType MetricType { get; private set; } - - public void Update(T value) - where T : struct - { - lock (this.lockUpdate) - { - this.value = new DataValue(value); - } - } - - public IMetric Collect(DateTimeOffset dt, bool isDelta) - { - var cloneItem = new GaugeMetricAggregator(this.Name, this.Description, this.Unit, this.Meter, this.StartTimeExclusive, this.Attributes); - - lock (this.lockUpdate) - { - cloneItem.Exemplars = this.Exemplars; - cloneItem.EndTimeInclusive = dt; - cloneItem.value = this.LastValue; - } - - return cloneItem; - } - - public string ToDisplayString() - { - return $"Last={this.LastValue.Value}"; - } - } -} diff --git a/src/OpenTelemetry/Metrics/MetricAggregators/HistogramMetric.cs b/src/OpenTelemetry/Metrics/MetricAggregators/HistogramMetric.cs deleted file mode 100644 index cf740093875..00000000000 --- a/src/OpenTelemetry/Metrics/MetricAggregators/HistogramMetric.cs +++ /dev/null @@ -1,70 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -using System; -using System.Collections.Generic; -using System.Diagnostics.Metrics; - -namespace OpenTelemetry.Metrics -{ - internal class HistogramMetric : IHistogramMetric - { - internal HistogramMetric(string name, string description, string unit, Meter meter, DateTimeOffset startTimeExclusive, KeyValuePair[] attributes, int bucketCount) - { - this.Name = name; - this.Description = description; - this.Unit = unit; - this.Meter = meter; - this.StartTimeExclusive = startTimeExclusive; - this.Attributes = attributes; - this.MetricType = MetricType.Histogram; - this.BucketsArray = new HistogramBucket[bucketCount]; - } - - public string Name { get; private set; } - - public string Description { get; private set; } - - public string Unit { get; private set; } - - public Meter Meter { get; private set; } - - public DateTimeOffset StartTimeExclusive { get; internal set; } - - public DateTimeOffset EndTimeInclusive { get; internal set; } - - public KeyValuePair[] Attributes { get; private set; } - - public bool IsDeltaTemporality { get; internal set; } - - public IEnumerable Exemplars { get; private set; } = new List(); - - public long PopulationCount { get; internal set; } - - public double PopulationSum { get; internal set; } - - public IEnumerable Buckets => this.BucketsArray; - - public MetricType MetricType { get; private set; } - - internal HistogramBucket[] BucketsArray { get; set; } - - public string ToDisplayString() - { - return $"Count={this.PopulationCount},Sum={this.PopulationSum}"; - } - } -} diff --git a/src/OpenTelemetry/Metrics/MetricAggregators/HistogramMetricAggregator.cs b/src/OpenTelemetry/Metrics/MetricAggregators/HistogramMetricAggregator.cs deleted file mode 100644 index 7b5229fdbd3..00000000000 --- a/src/OpenTelemetry/Metrics/MetricAggregators/HistogramMetricAggregator.cs +++ /dev/null @@ -1,176 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -using System; -using System.Collections.Generic; -using System.Diagnostics.Metrics; - -namespace OpenTelemetry.Metrics -{ - internal class HistogramMetricAggregator : IAggregator - { - private static readonly double[] DefaultBoundaries = new double[] { 0, 5, 10, 25, 50, 75, 100, 250, 500, 1000 }; - - private readonly object lockUpdate = new object(); - private HistogramBucket[] buckets; - private long populationCount; - private double populationSum; - private double[] boundaries; - private DateTimeOffset startTimeExclusive; - private HistogramMetric histogramMetric; - - internal HistogramMetricAggregator(string name, string description, string unit, Meter meter, DateTimeOffset startTimeExclusive, KeyValuePair[] attributes) - : this(name, description, unit, meter, startTimeExclusive, attributes, DefaultBoundaries) - { - } - - internal HistogramMetricAggregator(string name, string description, string unit, Meter meter, DateTimeOffset startTimeExclusive, KeyValuePair[] attributes, double[] boundaries) - { - this.startTimeExclusive = startTimeExclusive; - this.histogramMetric = new HistogramMetric(name, description, unit, meter, startTimeExclusive, attributes, boundaries.Length + 1); - - if (boundaries.Length == 0) - { - boundaries = DefaultBoundaries; - } - - this.boundaries = boundaries; - this.buckets = this.InitializeBucket(boundaries); - } - - public void Update(T value) - where T : struct - { - // promote value to be a double - - double val; - if (typeof(T) == typeof(long)) - { - val = (long)(object)value; - } - else if (typeof(T) == typeof(double)) - { - val = (double)(object)value; - } - else - { - throw new Exception("Unsupported Type!"); - } - - // Determine the bucket index - - int i; - for (i = 0; i < this.boundaries.Length; i++) - { - if (val < this.boundaries[i]) - { - break; - } - } - - lock (this.lockUpdate) - { - this.populationCount++; - this.populationSum += val; - this.buckets[i].Count++; - } - } - - public IMetric Collect(DateTimeOffset dt, bool isDelta) - { - if (this.populationCount == 0) - { - // TODO: Output stale markers - return null; - } - - lock (this.lockUpdate) - { - this.histogramMetric.StartTimeExclusive = this.startTimeExclusive; - this.histogramMetric.EndTimeInclusive = dt; - this.histogramMetric.PopulationCount = this.populationCount; - this.histogramMetric.PopulationSum = this.populationSum; - this.buckets.CopyTo(this.histogramMetric.BucketsArray, 0); - this.histogramMetric.IsDeltaTemporality = isDelta; - - if (isDelta) - { - this.startTimeExclusive = dt; - this.populationCount = 0; - this.populationSum = 0; - for (int i = 0; i < this.buckets.Length; i++) - { - this.buckets[i].Count = 0; - } - } - } - - // TODO: Confirm that this approach of - // re-using the same instance is correct. - // This avoids allocating a new instance. - // It is read only for Exporters, - // and also there is no parallel - // Collect allowed. - return this.histogramMetric; - } - - private HistogramBucket[] InitializeBucket(double[] boundaries) - { - var buckets = new HistogramBucket[boundaries.Length + 1]; - - var lastBoundary = boundaries[0]; - for (int i = 0; i < buckets.Length; i++) - { - if (i == 0) - { - // LowBoundary is inclusive - buckets[i].LowBoundary = double.NegativeInfinity; - - // HighBoundary is exclusive - buckets[i].HighBoundary = boundaries[i]; - } - else if (i < boundaries.Length) - { - // LowBoundary is inclusive - buckets[i].LowBoundary = lastBoundary; - - // HighBoundary is exclusive - buckets[i].HighBoundary = boundaries[i]; - } - else - { - // LowBoundary and HighBoundary are inclusive - buckets[i].LowBoundary = lastBoundary; - buckets[i].HighBoundary = double.PositiveInfinity; - } - - buckets[i].Count = 0; - - if (i < boundaries.Length) - { - if (boundaries[i] < lastBoundary) - { - throw new ArgumentException("Boundary values must be increasing.", nameof(boundaries)); - } - - lastBoundary = boundaries[i]; - } - } - - return buckets; - } - } -} diff --git a/src/OpenTelemetry/Metrics/MetricAggregators/IMetric.cs b/src/OpenTelemetry/Metrics/MetricAggregators/IMetric.cs deleted file mode 100644 index 079172dd6ce..00000000000 --- a/src/OpenTelemetry/Metrics/MetricAggregators/IMetric.cs +++ /dev/null @@ -1,43 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -using System; -using System.Collections.Generic; -using System.Diagnostics.Metrics; - -namespace OpenTelemetry.Metrics -{ - public interface IMetric - { - string Name { get; } - - string Description { get; } - - string Unit { get; } - - Meter Meter { get; } - - DateTimeOffset StartTimeExclusive { get; } - - DateTimeOffset EndTimeInclusive { get; } - - KeyValuePair[] Attributes { get; } - - MetricType MetricType { get; } - - string ToDisplayString(); - } -} diff --git a/src/OpenTelemetry/Metrics/MetricAggregators/ISumMetric.cs b/src/OpenTelemetry/Metrics/MetricAggregators/ISumMetric.cs deleted file mode 100644 index 36d2f4f648c..00000000000 --- a/src/OpenTelemetry/Metrics/MetricAggregators/ISumMetric.cs +++ /dev/null @@ -1,29 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -using System.Collections.Generic; - -namespace OpenTelemetry.Metrics -{ - public interface ISumMetric : IMetric - { - bool IsDeltaTemporality { get; } - - bool IsMonotonic { get; } - - IEnumerable Exemplars { get; } - } -} diff --git a/src/OpenTelemetry/Metrics/MetricAggregators/ISumMetricDouble.cs b/src/OpenTelemetry/Metrics/MetricAggregators/ISumMetricDouble.cs deleted file mode 100644 index 5b04e1d9db8..00000000000 --- a/src/OpenTelemetry/Metrics/MetricAggregators/ISumMetricDouble.cs +++ /dev/null @@ -1,23 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -namespace OpenTelemetry.Metrics -{ - public interface ISumMetricDouble : ISumMetric - { - double DoubleSum { get; } - } -} diff --git a/src/OpenTelemetry/Metrics/MetricAggregators/ISumMetricLong.cs b/src/OpenTelemetry/Metrics/MetricAggregators/ISumMetricLong.cs deleted file mode 100644 index 033956fdf83..00000000000 --- a/src/OpenTelemetry/Metrics/MetricAggregators/ISumMetricLong.cs +++ /dev/null @@ -1,23 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -namespace OpenTelemetry.Metrics -{ - public interface ISumMetricLong : ISumMetric - { - long LongSum { get; } - } -} diff --git a/src/OpenTelemetry/Metrics/MetricAggregators/SumMetricAggregatorDouble.cs b/src/OpenTelemetry/Metrics/MetricAggregators/SumMetricAggregatorDouble.cs deleted file mode 100644 index d266973c3ad..00000000000 --- a/src/OpenTelemetry/Metrics/MetricAggregators/SumMetricAggregatorDouble.cs +++ /dev/null @@ -1,93 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -using System; -using System.Collections.Generic; -using System.Diagnostics.Metrics; - -namespace OpenTelemetry.Metrics -{ - internal class SumMetricAggregatorDouble : IAggregator - { - private readonly object lockUpdate = new object(); - private double sumDouble = 0; - private SumMetricDouble sumMetricDouble; - private DateTimeOffset startTimeExclusive; - - internal SumMetricAggregatorDouble(string name, string description, string unit, Meter meter, DateTimeOffset startTimeExclusive, KeyValuePair[] attributes) - { - this.startTimeExclusive = startTimeExclusive; - this.sumMetricDouble = new SumMetricDouble(name, description, unit, meter, startTimeExclusive, attributes); - } - - public void Update(T value) - where T : struct - { - // TODO: Replace Lock with - // TryAdd..{Spin..TryAdd..Repeat} if "lost race to another thread" - lock (this.lockUpdate) - { - if (typeof(T) == typeof(double)) - { - // TODO: Confirm this doesn't cause boxing. - var val = (double)(object)value; - if (val < 0) - { - // TODO: log? - // Also, this validation can be done in earlier stage. - } - else - { - this.sumDouble += val; - } - } - else - { - throw new Exception("Unsupported Type"); - } - } - } - - public IMetric Collect(DateTimeOffset dt, bool isDelta) - { - lock (this.lockUpdate) - { - this.sumMetricDouble.StartTimeExclusive = this.startTimeExclusive; - this.sumMetricDouble.EndTimeInclusive = dt; - this.sumMetricDouble.DoubleSum = this.sumDouble; - this.sumMetricDouble.IsDeltaTemporality = isDelta; - if (isDelta) - { - this.startTimeExclusive = dt; - this.sumDouble = 0; - } - } - - // TODO: Confirm that this approach of - // re-using the same instance is correct. - // This avoids allocating a new instance. - // It is read only for Exporters, - // and also there is no parallel - // Collect allowed. - return this.sumMetricDouble; - } - - public string ToDisplayString() - { - return $"Sum={this.sumDouble}"; - } - } -} diff --git a/src/OpenTelemetry/Metrics/MetricAggregators/SumMetricAggregatorLong.cs b/src/OpenTelemetry/Metrics/MetricAggregators/SumMetricAggregatorLong.cs deleted file mode 100644 index 048cbc93a2c..00000000000 --- a/src/OpenTelemetry/Metrics/MetricAggregators/SumMetricAggregatorLong.cs +++ /dev/null @@ -1,92 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -using System; -using System.Collections.Generic; -using System.Diagnostics.Metrics; - -namespace OpenTelemetry.Metrics -{ - internal class SumMetricAggregatorLong : IAggregator - { - private readonly object lockUpdate = new object(); - private long sumLong = 0; - private SumMetricLong sumMetricLong; - private DateTimeOffset startTimeExclusive; - - internal SumMetricAggregatorLong(string name, string description, string unit, Meter meter, DateTimeOffset startTimeExclusive, KeyValuePair[] attributes) - { - this.startTimeExclusive = startTimeExclusive; - this.sumMetricLong = new SumMetricLong(name, description, unit, meter, startTimeExclusive, attributes); - } - - public void Update(T value) - where T : struct - { - // TODO: Replace Lock with Interlocked.Add - lock (this.lockUpdate) - { - if (typeof(T) == typeof(long)) - { - // TODO: Confirm this doesn't cause boxing. - var val = (long)(object)value; - if (val < 0) - { - // TODO: log? - // Also, this validation can be done in earlier stage. - } - else - { - this.sumLong += val; - } - } - else - { - throw new Exception("Unsupported Type"); - } - } - } - - public IMetric Collect(DateTimeOffset dt, bool isDelta) - { - lock (this.lockUpdate) - { - this.sumMetricLong.StartTimeExclusive = this.startTimeExclusive; - this.sumMetricLong.EndTimeInclusive = dt; - this.sumMetricLong.LongSum = this.sumLong; - this.sumMetricLong.IsDeltaTemporality = isDelta; - if (isDelta) - { - this.startTimeExclusive = dt; - this.sumLong = 0; - } - } - - // TODO: Confirm that this approach of - // re-using the same instance is correct. - // This avoids allocating a new instance. - // It is read only for Exporters, - // and also there is no parallel - // Collect allowed. - return this.sumMetricLong; - } - - public string ToDisplayString() - { - return $"Sum={this.sumLong}"; - } - } -} diff --git a/src/OpenTelemetry/Metrics/MetricAggregators/SumMetricDouble.cs b/src/OpenTelemetry/Metrics/MetricAggregators/SumMetricDouble.cs deleted file mode 100644 index 3b5486ad6d3..00000000000 --- a/src/OpenTelemetry/Metrics/MetricAggregators/SumMetricDouble.cs +++ /dev/null @@ -1,66 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -using System; -using System.Collections.Generic; -using System.Diagnostics.Metrics; - -namespace OpenTelemetry.Metrics -{ - internal class SumMetricDouble : ISumMetricDouble - { - internal SumMetricDouble(string name, string description, string unit, Meter meter, DateTimeOffset startTimeExclusive, KeyValuePair[] attributes) - { - this.Name = name; - this.Description = description; - this.Unit = unit; - this.Meter = meter; - this.StartTimeExclusive = startTimeExclusive; - this.Attributes = attributes; - this.IsMonotonic = true; - this.MetricType = MetricType.DoubleSum; - } - - public string Name { get; private set; } - - public string Description { get; private set; } - - public string Unit { get; private set; } - - public Meter Meter { get; private set; } - - public DateTimeOffset StartTimeExclusive { get; internal set; } - - public DateTimeOffset EndTimeInclusive { get; internal set; } - - public KeyValuePair[] Attributes { get; private set; } - - public bool IsDeltaTemporality { get; internal set; } - - public bool IsMonotonic { get; } - - public IEnumerable Exemplars { get; private set; } = new List(); - - public double DoubleSum { get; internal set; } - - public MetricType MetricType { get; private set; } - - public string ToDisplayString() - { - return $"Sum={this.DoubleSum}"; - } - } -} diff --git a/src/OpenTelemetry/Metrics/MetricAggregators/SumMetricLong.cs b/src/OpenTelemetry/Metrics/MetricAggregators/SumMetricLong.cs deleted file mode 100644 index 1398cb694af..00000000000 --- a/src/OpenTelemetry/Metrics/MetricAggregators/SumMetricLong.cs +++ /dev/null @@ -1,66 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -using System; -using System.Collections.Generic; -using System.Diagnostics.Metrics; - -namespace OpenTelemetry.Metrics -{ - internal class SumMetricLong : ISumMetricLong - { - internal SumMetricLong(string name, string description, string unit, Meter meter, DateTimeOffset startTimeExclusive, KeyValuePair[] attributes) - { - this.Name = name; - this.Description = description; - this.Unit = unit; - this.Meter = meter; - this.StartTimeExclusive = startTimeExclusive; - this.Attributes = attributes; - this.IsMonotonic = true; - this.MetricType = MetricType.LongSum; - } - - public string Name { get; private set; } - - public string Description { get; private set; } - - public string Unit { get; private set; } - - public Meter Meter { get; private set; } - - public DateTimeOffset StartTimeExclusive { get; internal set; } - - public DateTimeOffset EndTimeInclusive { get; internal set; } - - public KeyValuePair[] Attributes { get; private set; } - - public bool IsDeltaTemporality { get; internal set; } - - public bool IsMonotonic { get; } - - public IEnumerable Exemplars { get; private set; } = new List(); - - public long LongSum { get; internal set; } - - public MetricType MetricType { get; private set; } - - public string ToDisplayString() - { - return $"Sum={this.LongSum}"; - } - } -} diff --git a/src/OpenTelemetry/Metrics/MetricAggregators/SummaryMetricAggregator.cs b/src/OpenTelemetry/Metrics/MetricAggregators/SummaryMetricAggregator.cs deleted file mode 100644 index 0e7fe400636..00000000000 --- a/src/OpenTelemetry/Metrics/MetricAggregators/SummaryMetricAggregator.cs +++ /dev/null @@ -1,123 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -using System; -using System.Collections.Generic; -using System.Diagnostics.Metrics; - -namespace OpenTelemetry.Metrics -{ - internal class SummaryMetricAggregator : ISummaryMetric, IAggregator - { - private readonly object lockUpdate = new object(); - - private List quantiles = new List(); - - internal SummaryMetricAggregator(string name, string description, string unit, Meter meter, DateTimeOffset startTimeExclusive, KeyValuePair[] attributes, bool isMonotonic) - { - this.Name = name; - this.Description = description; - this.Unit = unit; - this.Meter = meter; - this.StartTimeExclusive = startTimeExclusive; - this.Attributes = attributes; - this.IsMonotonic = isMonotonic; - this.MetricType = MetricType.Summary; - } - - public string Name { get; private set; } - - public string Description { get; private set; } - - public string Unit { get; private set; } - - public Meter Meter { get; private set; } - - public DateTimeOffset StartTimeExclusive { get; private set; } - - public DateTimeOffset EndTimeInclusive { get; private set; } - - public KeyValuePair[] Attributes { get; private set; } - - public bool IsMonotonic { get; } - - public long PopulationCount { get; private set; } - - public double PopulationSum { get; private set; } - - public IEnumerable Quantiles => this.quantiles; - - public MetricType MetricType { get; private set; } - - public void Update(T value) - where T : struct - { - // TODO: Implement Summary! - - lock (this.lockUpdate) - { - if (typeof(T) == typeof(long)) - { - var val = (long)(object)value; - if (val > 0 || !this.IsMonotonic) - { - this.PopulationSum += (double)val; - this.PopulationCount++; - } - } - else if (typeof(T) == typeof(double)) - { - var val = (double)(object)value; - if (val > 0 || !this.IsMonotonic) - { - this.PopulationSum += (double)val; - this.PopulationCount++; - } - } - } - } - - public IMetric Collect(DateTimeOffset dt, bool isDelta) - { - if (this.PopulationCount == 0) - { - // TODO: Output stale markers - return null; - } - - var cloneItem = new SummaryMetricAggregator(this.Name, this.Description, this.Unit, this.Meter, this.StartTimeExclusive, this.Attributes, this.IsMonotonic); - - lock (this.lockUpdate) - { - cloneItem.EndTimeInclusive = dt; - cloneItem.PopulationCount = this.PopulationCount; - cloneItem.PopulationSum = this.PopulationSum; - cloneItem.quantiles = this.quantiles; - - this.StartTimeExclusive = dt; - this.PopulationCount = 0; - this.PopulationSum = 0; - } - - return cloneItem; - } - - public string ToDisplayString() - { - return $"Count={this.PopulationCount},Sum={this.PopulationSum}"; - } - } -} diff --git a/src/OpenTelemetry/Metrics/MetricAggregators/ValueAtQuantile.cs b/src/OpenTelemetry/Metrics/MetricAggregators/ValueAtQuantile.cs deleted file mode 100644 index 6b43f98e153..00000000000 --- a/src/OpenTelemetry/Metrics/MetricAggregators/ValueAtQuantile.cs +++ /dev/null @@ -1,24 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -namespace OpenTelemetry.Metrics -{ - public struct ValueAtQuantile - { - internal double Quantile; - internal double Value; - } -} diff --git a/src/OpenTelemetry/Metrics/MetricPoint.cs b/src/OpenTelemetry/Metrics/MetricPoint.cs new file mode 100644 index 00000000000..e91c1253ace --- /dev/null +++ b/src/OpenTelemetry/Metrics/MetricPoint.cs @@ -0,0 +1,514 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Threading; + +namespace OpenTelemetry.Metrics +{ + /// + /// Stores details about a metric data point. + /// + public struct MetricPoint + { + private readonly AggregationType aggType; + + private readonly HistogramBuckets histogramBuckets; + + // Represents temporality adjusted "value" for double/long metric types or "count" when histogram + private MetricPointValueStorage runningValue; + + // Represents either "value" for double/long metric types or "count" when histogram + private MetricPointValueStorage snapshotValue; + + private MetricPointValueStorage deltaLastValue; + + internal MetricPoint( + AggregationType aggType, + DateTimeOffset startTime, + string[] keys, + object[] values, + double[] histogramExplicitBounds) + { + Debug.Assert((keys?.Length ?? 0) == (values?.Length ?? 0), "Key and value array lengths did not match."); + Debug.Assert(histogramExplicitBounds != null, "Histogram explicit Bounds was null."); + + this.aggType = aggType; + this.StartTime = startTime; + this.Tags = new ReadOnlyTagCollection(keys, values); + this.EndTime = default; + this.runningValue = default; + this.snapshotValue = default; + this.deltaLastValue = default; + this.MetricPointStatus = MetricPointStatus.NoCollectPending; + + if (this.aggType == AggregationType.Histogram) + { + this.histogramBuckets = new HistogramBuckets(histogramExplicitBounds); + } + else if (this.aggType == AggregationType.HistogramSumCount) + { + this.histogramBuckets = new HistogramBuckets(null); + } + else + { + this.histogramBuckets = null; + } + } + + /// + /// Gets the tags associated with the metric point. + /// + public ReadOnlyTagCollection Tags + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + } + + /// + /// Gets the start time associated with the metric point. + /// + public DateTimeOffset StartTime + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal set; + } + + /// + /// Gets the end time associated with the metric point. + /// + public DateTimeOffset EndTime + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal set; + } + + internal MetricPointStatus MetricPointStatus + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private set; + } + + /// + /// Gets the sum long value associated with the metric point. + /// + /// + /// Applies to metric type. + /// + /// Long sum value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public long GetSumLong() + { + if (this.aggType != AggregationType.LongSumIncomingDelta && this.aggType != AggregationType.LongSumIncomingCumulative) + { + this.ThrowNotSupportedMetricTypeException(nameof(this.GetSumLong)); + } + + return this.snapshotValue.AsLong; + } + + /// + /// Gets the sum double value associated with the metric point. + /// + /// + /// Applies to metric type. + /// + /// Double sum value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public double GetSumDouble() + { + if (this.aggType != AggregationType.DoubleSumIncomingDelta && this.aggType != AggregationType.DoubleSumIncomingCumulative) + { + this.ThrowNotSupportedMetricTypeException(nameof(this.GetSumDouble)); + } + + return this.snapshotValue.AsDouble; + } + + /// + /// Gets the last long value of the gauge associated with the metric point. + /// + /// + /// Applies to metric type. + /// + /// Long gauge value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public long GetGaugeLastValueLong() + { + if (this.aggType != AggregationType.LongGauge) + { + this.ThrowNotSupportedMetricTypeException(nameof(this.GetGaugeLastValueLong)); + } + + return this.snapshotValue.AsLong; + } + + /// + /// Gets the last double value of the gauge associated with the metric point. + /// + /// + /// Applies to metric type. + /// + /// Double gauge value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public double GetGaugeLastValueDouble() + { + if (this.aggType != AggregationType.DoubleGauge) + { + this.ThrowNotSupportedMetricTypeException(nameof(this.GetGaugeLastValueDouble)); + } + + return this.snapshotValue.AsDouble; + } + + /// + /// Gets the count value of the histogram associated with the metric point. + /// + /// + /// Applies to metric type. + /// + /// Count value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public long GetHistogramCount() + { + if (this.aggType != AggregationType.Histogram && this.aggType != AggregationType.HistogramSumCount) + { + this.ThrowNotSupportedMetricTypeException(nameof(this.GetHistogramCount)); + } + + return this.snapshotValue.AsLong; + } + + /// + /// Gets the sum value of the histogram associated with the metric point. + /// + /// + /// Applies to metric type. + /// + /// Sum value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public double GetHistogramSum() + { + if (this.aggType != AggregationType.Histogram && this.aggType != AggregationType.HistogramSumCount) + { + this.ThrowNotSupportedMetricTypeException(nameof(this.GetHistogramSum)); + } + + return this.histogramBuckets.SnapshotSum; + } + + /// + /// Gets the buckets of the histogram associated with the metric point. + /// + /// + /// Applies to metric type. + /// + /// . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public HistogramBuckets GetHistogramBuckets() + { + if (this.aggType != AggregationType.Histogram && this.aggType != AggregationType.HistogramSumCount) + { + this.ThrowNotSupportedMetricTypeException(nameof(this.GetHistogramBuckets)); + } + + return this.histogramBuckets; + } + + internal void Update(long number) + { + switch (this.aggType) + { + case AggregationType.LongSumIncomingDelta: + { + Interlocked.Add(ref this.runningValue.AsLong, number); + break; + } + + case AggregationType.LongSumIncomingCumulative: + { + Interlocked.Exchange(ref this.runningValue.AsLong, number); + break; + } + + case AggregationType.LongGauge: + { + Interlocked.Exchange(ref this.runningValue.AsLong, number); + break; + } + + case AggregationType.Histogram: + case AggregationType.HistogramSumCount: + { + this.Update((double)number); + break; + } + } + + // There is a race with Snapshot: + // Update() updates the value + // Snapshot snapshots the value + // Snapshot sets status to NoCollectPending + // Update sets status to CollectPending -- this is not right as the Snapshot + // already included the updated value. + // In the absence of any new Update call until next Snapshot, + // this results in exporting an Update even though + // it had no update. + // TODO: For Delta, this can be mitigated + // by ignoring Zero points + this.MetricPointStatus = MetricPointStatus.CollectPending; + } + + internal void Update(double number) + { + switch (this.aggType) + { + case AggregationType.DoubleSumIncomingDelta: + { + double initValue, newValue; + do + { + initValue = this.runningValue.AsDouble; + newValue = initValue + number; + } + while (initValue != Interlocked.CompareExchange(ref this.runningValue.AsDouble, newValue, initValue)); + break; + } + + case AggregationType.DoubleSumIncomingCumulative: + { + Interlocked.Exchange(ref this.runningValue.AsDouble, number); + break; + } + + case AggregationType.DoubleGauge: + { + Interlocked.Exchange(ref this.runningValue.AsDouble, number); + break; + } + + case AggregationType.Histogram: + { + int i; + for (i = 0; i < this.histogramBuckets.ExplicitBounds.Length; i++) + { + // Upper bound is inclusive + if (number <= this.histogramBuckets.ExplicitBounds[i]) + { + break; + } + } + + lock (this.histogramBuckets.LockObject) + { + this.runningValue.AsLong++; + this.histogramBuckets.RunningSum += number; + this.histogramBuckets.RunningBucketCounts[i]++; + } + + break; + } + + case AggregationType.HistogramSumCount: + { + lock (this.histogramBuckets.LockObject) + { + this.runningValue.AsLong++; + this.histogramBuckets.RunningSum += number; + } + + break; + } + } + + // There is a race with Snapshot: + // Update() updates the value + // Snapshot snapshots the value + // Snapshot sets status to NoCollectPending + // Update sets status to CollectPending -- this is not right as the Snapshot + // already included the updated value. + // In the absence of any new Update call until next Snapshot, + // this results in exporting an Update even though + // it had no update. + // TODO: For Delta, this can be mitigated + // by ignoring Zero points + this.MetricPointStatus = MetricPointStatus.CollectPending; + } + + internal void TakeSnapshot(bool outputDelta) + { + switch (this.aggType) + { + case AggregationType.LongSumIncomingDelta: + case AggregationType.LongSumIncomingCumulative: + { + if (outputDelta) + { + long initValue = Interlocked.Read(ref this.runningValue.AsLong); + this.snapshotValue.AsLong = initValue - this.deltaLastValue.AsLong; + this.deltaLastValue.AsLong = initValue; + this.MetricPointStatus = MetricPointStatus.NoCollectPending; + + // Check again if value got updated, if yes reset status. + // This ensures no Updates get Lost. + if (initValue != Interlocked.Read(ref this.runningValue.AsLong)) + { + this.MetricPointStatus = MetricPointStatus.CollectPending; + } + } + else + { + this.snapshotValue.AsLong = Interlocked.Read(ref this.runningValue.AsLong); + } + + break; + } + + case AggregationType.DoubleSumIncomingDelta: + case AggregationType.DoubleSumIncomingCumulative: + { + if (outputDelta) + { + // TODO: + // Is this thread-safe way to read double? + // As long as the value is not -ve infinity, + // the exchange (to 0.0) will never occur, + // but we get the original value atomically. + double initValue = Interlocked.CompareExchange(ref this.runningValue.AsDouble, 0.0, double.NegativeInfinity); + this.snapshotValue.AsDouble = initValue - this.deltaLastValue.AsDouble; + this.deltaLastValue.AsDouble = initValue; + this.MetricPointStatus = MetricPointStatus.NoCollectPending; + + // Check again if value got updated, if yes reset status. + // This ensures no Updates get Lost. + if (initValue != Interlocked.CompareExchange(ref this.runningValue.AsDouble, 0.0, double.NegativeInfinity)) + { + this.MetricPointStatus = MetricPointStatus.CollectPending; + } + } + else + { + // TODO: + // Is this thread-safe way to read double? + // As long as the value is not -ve infinity, + // the exchange (to 0.0) will never occur, + // but we get the original value atomically. + this.snapshotValue.AsDouble = Interlocked.CompareExchange(ref this.runningValue.AsDouble, 0.0, double.NegativeInfinity); + } + + break; + } + + case AggregationType.LongGauge: + { + this.snapshotValue.AsLong = Interlocked.Read(ref this.runningValue.AsLong); + this.MetricPointStatus = MetricPointStatus.NoCollectPending; + + // Check again if value got updated, if yes reset status. + // This ensures no Updates get Lost. + if (this.snapshotValue.AsLong != Interlocked.Read(ref this.runningValue.AsLong)) + { + this.MetricPointStatus = MetricPointStatus.CollectPending; + } + + break; + } + + case AggregationType.DoubleGauge: + { + // TODO: + // Is this thread-safe way to read double? + // As long as the value is not -ve infinity, + // the exchange (to 0.0) will never occur, + // but we get the original value atomically. + this.snapshotValue.AsDouble = Interlocked.CompareExchange(ref this.runningValue.AsDouble, 0.0, double.NegativeInfinity); + this.MetricPointStatus = MetricPointStatus.NoCollectPending; + + // Check again if value got updated, if yes reset status. + // This ensures no Updates get Lost. + if (this.snapshotValue.AsDouble != Interlocked.CompareExchange(ref this.runningValue.AsDouble, 0.0, double.NegativeInfinity)) + { + this.MetricPointStatus = MetricPointStatus.CollectPending; + } + + break; + } + + case AggregationType.Histogram: + { + lock (this.histogramBuckets.LockObject) + { + this.snapshotValue.AsLong = this.runningValue.AsLong; + this.histogramBuckets.SnapshotSum = this.histogramBuckets.RunningSum; + if (outputDelta) + { + this.runningValue.AsLong = 0; + this.histogramBuckets.RunningSum = 0; + } + + for (int i = 0; i < this.histogramBuckets.RunningBucketCounts.Length; i++) + { + this.histogramBuckets.SnapshotBucketCounts[i] = this.histogramBuckets.RunningBucketCounts[i]; + if (outputDelta) + { + this.histogramBuckets.RunningBucketCounts[i] = 0; + } + } + + this.MetricPointStatus = MetricPointStatus.NoCollectPending; + } + + break; + } + + case AggregationType.HistogramSumCount: + { + lock (this.histogramBuckets.LockObject) + { + this.snapshotValue.AsLong = this.runningValue.AsLong; + this.histogramBuckets.SnapshotSum = this.histogramBuckets.RunningSum; + if (outputDelta) + { + this.runningValue.AsLong = 0; + this.histogramBuckets.RunningSum = 0; + } + + this.MetricPointStatus = MetricPointStatus.NoCollectPending; + } + + break; + } + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void ThrowNotSupportedMetricTypeException(string methodName) + { + throw new NotSupportedException($"{methodName} is not supported for this metric type."); + } + } +} diff --git a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/Internal/HttpParseResult.cs b/src/OpenTelemetry/Metrics/MetricPointStatus.cs similarity index 56% rename from src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/Internal/HttpParseResult.cs rename to src/OpenTelemetry/Metrics/MetricPointStatus.cs index 288e1faa070..0ecd2a0c062 100644 --- a/src/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule/Internal/HttpParseResult.cs +++ b/src/OpenTelemetry/Metrics/MetricPointStatus.cs @@ -1,4 +1,4 @@ -// +// // Copyright The OpenTelemetry Authors // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,24 +14,20 @@ // limitations under the License. // -namespace OpenTelemetry.Instrumentation.AspNet +namespace OpenTelemetry.Metrics { - // Adoptation of code from https://github.com/aspnet/HttpAbstractions/blob/07d115400e4f8c7a66ba239f230805f03a14ee3d/src/Microsoft.Net.Http.Headers/HttpParseResult.cs - internal enum HttpParseResult + internal enum MetricPointStatus { /// - /// Parsed successfully. + /// This status is applied to s with status after a Collect. + /// If an update occurs, status will be moved to . /// - Parsed, + NoCollectPending, /// - /// Was not parsed. + /// The has been updated since the previous Collect cycle. + /// Collect will move it to . /// - NotParsed, - - /// - /// Invalid format. - /// - InvalidFormat, + CollectPending, } } diff --git a/src/OpenTelemetry/Metrics/MetricAggregators/IGaugeMetric.cs b/src/OpenTelemetry/Metrics/MetricPointValueStorage.cs similarity index 67% rename from src/OpenTelemetry/Metrics/MetricAggregators/IGaugeMetric.cs rename to src/OpenTelemetry/Metrics/MetricPointValueStorage.cs index afb67d7959c..778e97885bb 100644 --- a/src/OpenTelemetry/Metrics/MetricAggregators/IGaugeMetric.cs +++ b/src/OpenTelemetry/Metrics/MetricPointValueStorage.cs @@ -1,4 +1,4 @@ -// +// // Copyright The OpenTelemetry Authors // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,14 +14,17 @@ // limitations under the License. // -using System.Collections.Generic; +using System.Runtime.InteropServices; namespace OpenTelemetry.Metrics { - public interface IGaugeMetric : IMetric + [StructLayout(LayoutKind.Explicit)] + internal struct MetricPointValueStorage { - IEnumerable Exemplars { get; } + [FieldOffset(0)] + public long AsLong; - IDataValue LastValue { get; } + [FieldOffset(0)] + public double AsDouble; } } diff --git a/src/OpenTelemetry/Metrics/MetricPointsAccessor.cs b/src/OpenTelemetry/Metrics/MetricPointsAccessor.cs new file mode 100644 index 00000000000..ef5c1dc0054 --- /dev/null +++ b/src/OpenTelemetry/Metrics/MetricPointsAccessor.cs @@ -0,0 +1,109 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Metrics +{ + /// + /// A struct for accessing the s collected for a + /// . + /// + public readonly struct MetricPointsAccessor + { + private readonly MetricPoint[] metricsPoints; + private readonly int[] metricPointsToProcess; + private readonly long targetCount; + private readonly DateTimeOffset start; + private readonly DateTimeOffset end; + + internal MetricPointsAccessor(MetricPoint[] metricsPoints, int[] metricPointsToProcess, long targetCount, DateTimeOffset start, DateTimeOffset end) + { + Guard.ThrowIfNull(metricsPoints, nameof(metricsPoints)); + + this.metricsPoints = metricsPoints; + this.metricPointsToProcess = metricPointsToProcess; + this.targetCount = targetCount; + this.start = start; + this.end = end; + } + + /// + /// Returns an enumerator that iterates through the . + /// + /// . + public Enumerator GetEnumerator() + { + return new Enumerator(this.metricsPoints, this.metricPointsToProcess, this.targetCount, this.start, this.end); + } + + /// + /// Enumerates the elements of a . + /// + public struct Enumerator + { + private readonly MetricPoint[] metricsPoints; + private readonly int[] metricPointsToProcess; + private readonly DateTimeOffset start; + private readonly DateTimeOffset end; + private readonly long targetCount; + private long index; + + internal Enumerator(MetricPoint[] metricsPoints, int[] metricPointsToProcess, long targetCount, DateTimeOffset start, DateTimeOffset end) + { + this.metricsPoints = metricsPoints; + this.metricPointsToProcess = metricPointsToProcess; + this.targetCount = targetCount; + this.index = -1; + this.start = start; + this.end = end; + } + + /// + /// Gets the at the current position of the enumerator. + /// + public ref readonly MetricPoint Current + { + get + { + return ref this.metricsPoints[this.metricPointsToProcess[this.index]]; + } + } + + /// + /// Advances the enumerator to the next element of the . + /// + /// if the enumerator was + /// successfully advanced to the next element; if the enumerator has passed the end of the + /// collection. + public bool MoveNext() + { + while (++this.index < this.targetCount) + { + ref var metricPoint = ref this.metricsPoints[this.metricPointsToProcess[this.index]]; + metricPoint.StartTime = this.start; + metricPoint.EndTime = this.end; + return true; + } + + return false; + } + } + } +} diff --git a/src/OpenTelemetry/Metrics/MetricReader.cs b/src/OpenTelemetry/Metrics/MetricReader.cs new file mode 100644 index 00000000000..759010a5ed9 --- /dev/null +++ b/src/OpenTelemetry/Metrics/MetricReader.cs @@ -0,0 +1,277 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Metrics +{ + /// + /// MetricReader which does not deal with individual metrics. + /// + public abstract partial class MetricReader : IDisposable + { + private const AggregationTemporality AggregationTemporalityUnspecified = (AggregationTemporality)0; + private readonly object newTaskLock = new object(); + private readonly object onCollectLock = new object(); + private readonly TaskCompletionSource shutdownTcs = new TaskCompletionSource(); + private AggregationTemporality temporality = AggregationTemporalityUnspecified; + private int shutdownCount; + private TaskCompletionSource collectionTcs; + + public BaseProvider ParentProvider { get; private set; } + + public AggregationTemporality Temporality + { + get + { + if (this.temporality == AggregationTemporalityUnspecified) + { + this.temporality = AggregationTemporality.Cumulative; + } + + return this.temporality; + } + + set + { + if (this.temporality != AggregationTemporalityUnspecified) + { + throw new NotSupportedException($"The temporality cannot be modified (the current value is {this.temporality})."); + } + + this.temporality = value; + } + } + + /// + /// Attempts to collect the metrics, blocks the current thread until + /// metrics collection completed, shutdown signaled or timed out. + /// + /// + /// The number (non-negative) of milliseconds to wait, or + /// Timeout.Infinite to wait indefinitely. + /// + /// + /// Returns true when metrics collection succeeded; otherwise, + /// false. + /// + /// + /// Thrown when the timeoutMilliseconds is smaller than -1. + /// + /// + /// This function guarantees thread-safety. If multiple calls occurred + /// simultaneously, they might get folded and result in less calls to + /// the OnCollect callback for improved performance, as long as + /// the semantic can be preserved. + /// + public bool Collect(int timeoutMilliseconds = Timeout.Infinite) + { + Guard.ThrowIfInvalidTimeout(timeoutMilliseconds, nameof(timeoutMilliseconds)); + + var shouldRunCollect = false; + var tcs = this.collectionTcs; + + if (tcs == null) + { + lock (this.newTaskLock) + { + tcs = this.collectionTcs; + + if (tcs == null) + { + shouldRunCollect = true; + tcs = new TaskCompletionSource(); + this.collectionTcs = tcs; + } + } + } + + if (!shouldRunCollect) + { + return Task.WaitAny(tcs.Task, this.shutdownTcs.Task, Task.Delay(timeoutMilliseconds)) == 0 ? tcs.Task.Result : false; + } + + var result = false; + try + { + lock (this.onCollectLock) + { + this.collectionTcs = null; + result = this.OnCollect(timeoutMilliseconds); + } + } + catch (Exception ex) + { + OpenTelemetrySdkEventSource.Log.MetricReaderException(nameof(this.Collect), ex); + } + + tcs.TrySetResult(result); + return result; + } + + /// + /// Attempts to shutdown the processor, blocks the current thread until + /// shutdown completed or timed out. + /// + /// + /// The number (non-negative) of milliseconds to wait, or + /// Timeout.Infinite to wait indefinitely. + /// + /// + /// Returns true when shutdown succeeded; otherwise, false. + /// + /// + /// Thrown when the timeoutMilliseconds is smaller than -1. + /// + /// + /// This function guarantees thread-safety. Only the first call will + /// win, subsequent calls will be no-op. + /// + public bool Shutdown(int timeoutMilliseconds = Timeout.Infinite) + { + Guard.ThrowIfInvalidTimeout(timeoutMilliseconds, nameof(timeoutMilliseconds)); + + if (Interlocked.CompareExchange(ref this.shutdownCount, 1, 0) != 0) + { + return false; // shutdown already called + } + + var result = false; + try + { + result = this.OnShutdown(timeoutMilliseconds); + } + catch (Exception ex) + { + OpenTelemetrySdkEventSource.Log.MetricReaderException(nameof(this.Shutdown), ex); + } + + this.shutdownTcs.TrySetResult(result); + return result; + } + + /// + public void Dispose() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + internal virtual void SetParentProvider(BaseProvider parentProvider) + { + this.ParentProvider = parentProvider; + } + + /// + /// Processes a batch of metrics. + /// + /// Batch of metrics to be processed. + /// + /// The number (non-negative) of milliseconds to wait, or + /// Timeout.Infinite to wait indefinitely. + /// + /// + /// Returns true when metrics processing succeeded; otherwise, + /// false. + /// + internal virtual bool ProcessMetrics(in Batch metrics, int timeoutMilliseconds) + { + return true; + } + + /// + /// Called by Collect. This function should block the current + /// thread until metrics collection completed, shutdown signaled or + /// timed out. + /// + /// + /// The number (non-negative) of milliseconds to wait, or + /// Timeout.Infinite to wait indefinitely. + /// + /// + /// Returns true when metrics collection succeeded; otherwise, + /// false. + /// + /// + /// This function is called synchronously on the threads which called + /// Collect. This function should not throw exceptions. + /// + protected virtual bool OnCollect(int timeoutMilliseconds) + { + var sw = timeoutMilliseconds == Timeout.Infinite + ? null + : Stopwatch.StartNew(); + + var collectObservableInstruments = this.ParentProvider.GetObservableInstrumentCollectCallback(); + collectObservableInstruments?.Invoke(); + + var metrics = this.GetMetricsBatch(); + + if (sw == null) + { + return this.ProcessMetrics(metrics, Timeout.Infinite); + } + else + { + var timeout = timeoutMilliseconds - sw.ElapsedMilliseconds; + + if (timeout <= 0) + { + return false; + } + + return this.ProcessMetrics(metrics, (int)timeout); + } + } + + /// + /// Called by Shutdown. This function should block the current + /// thread until shutdown completed or timed out. + /// + /// + /// The number (non-negative) of milliseconds to wait, or + /// Timeout.Infinite to wait indefinitely. + /// + /// + /// Returns true when shutdown succeeded; otherwise, false. + /// + /// + /// This function is called synchronously on the thread which made the + /// first call to Shutdown. This function should not throw + /// exceptions. + /// + protected virtual bool OnShutdown(int timeoutMilliseconds) + { + return this.Collect(timeoutMilliseconds); + } + + /// + /// Releases the unmanaged resources used by this class and optionally + /// releases the managed resources. + /// + /// + /// to release both managed and unmanaged resources; + /// to release only unmanaged resources. + /// + protected virtual void Dispose(bool disposing) + { + } + } +} diff --git a/src/OpenTelemetry/Metrics/MetricReaderExt.cs b/src/OpenTelemetry/Metrics/MetricReaderExt.cs new file mode 100644 index 00000000000..c963a9b9cb3 --- /dev/null +++ b/src/OpenTelemetry/Metrics/MetricReaderExt.cs @@ -0,0 +1,241 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using System.Diagnostics.Metrics; +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Metrics +{ + /// + /// MetricReader which processes individual metrics. + /// + public abstract partial class MetricReader + { + private readonly HashSet metricStreamNames = new HashSet(StringComparer.OrdinalIgnoreCase); + private readonly object instrumentCreationLock = new object(); + private int maxMetricStreams; + private int maxMetricPointsPerMetricStream; + private Metric[] metrics; + private Metric[] metricsCurrentBatch; + private int metricIndex = -1; + + internal Metric AddMetricWithNoViews(Instrument instrument) + { + var meterName = instrument.Meter.Name; + var metricName = instrument.Name; + var metricStreamName = $"{meterName}.{metricName}"; + lock (this.instrumentCreationLock) + { + if (this.metricStreamNames.Contains(metricStreamName)) + { + OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(metricName, instrument.Meter.Name, "Metric name conflicting with existing name.", "Either change the name of the instrument or change name using View."); + return null; + } + + var index = ++this.metricIndex; + if (index >= this.maxMetricStreams) + { + OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(metricName, instrument.Meter.Name, "Maximum allowed Metric streams for the provider exceeded.", "Use MeterProviderBuilder.AddView to drop unused instruments. Or use MeterProviderBuilder.SetMaxMetricStreams to configure MeterProvider to allow higher limit."); + return null; + } + else + { + var metric = new Metric(instrument, this.Temporality, metricName, instrument.Description, this.maxMetricPointsPerMetricStream); + this.metrics[index] = metric; + this.metricStreamNames.Add(metricStreamName); + return metric; + } + } + } + + internal void RecordSingleStreamLongMeasurement(Metric metric, long value, ReadOnlySpan> tags) + { + metric.UpdateLong(value, tags); + } + + internal void RecordSingleStreamDoubleMeasurement(Metric metric, double value, ReadOnlySpan> tags) + { + metric.UpdateDouble(value, tags); + } + + internal List AddMetricsListWithViews(Instrument instrument, List metricStreamConfigs) + { + var maxCountMetricsToBeCreated = metricStreamConfigs.Count; + + // Create list with initial capacity as the max metric count. + // Due to duplicate/max limit, we may not end up using them + // all, and that memory is wasted until Meter disposed. + // TODO: Revisit to see if we need to do metrics.TrimExcess() + var metrics = new List(maxCountMetricsToBeCreated); + lock (this.instrumentCreationLock) + { + for (int i = 0; i < maxCountMetricsToBeCreated; i++) + { + var metricStreamConfig = metricStreamConfigs[i]; + var meterName = instrument.Meter.Name; + var metricName = metricStreamConfig?.Name ?? instrument.Name; + var metricStreamName = $"{meterName}.{metricName}"; + + if (!MeterProviderBuilderSdk.IsValidInstrumentName(metricName)) + { + OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored( + metricName, + instrument.Meter.Name, + "Metric name is invalid.", + "The name must comply with the OpenTelemetry specification."); + + continue; + } + + if (this.metricStreamNames.Contains(metricStreamName)) + { + OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(metricName, instrument.Meter.Name, "Metric name conflicting with existing name.", "Either change the name of the instrument or change name using MeterProviderBuilder.AddView."); + continue; + } + + if (metricStreamConfig?.Aggregation == Aggregation.Drop) + { + OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(metricName, instrument.Meter.Name, "View configuration asks to drop this instrument.", "Modify view configuration to allow this instrument, if desired."); + continue; + } + + var index = ++this.metricIndex; + if (index >= this.maxMetricStreams) + { + OpenTelemetrySdkEventSource.Log.MetricInstrumentIgnored(metricName, instrument.Meter.Name, "Maximum allowed Metric streams for the provider exceeded.", "Use MeterProviderBuilder.AddView to drop unused instruments. Or use MeterProviderBuilder.SetMaxMetricStreams to configure MeterProvider to allow higher limit."); + } + else + { + Metric metric; + var metricDescription = metricStreamConfig?.Description ?? instrument.Description; + string[] tagKeysInteresting = metricStreamConfig?.TagKeys; + double[] histogramBucketBounds = (metricStreamConfig is ExplicitBucketHistogramConfiguration histogramConfig + && histogramConfig.Boundaries != null) ? histogramConfig.Boundaries : null; + metric = new Metric(instrument, this.Temporality, metricName, metricDescription, this.maxMetricPointsPerMetricStream, histogramBucketBounds, tagKeysInteresting); + + this.metrics[index] = metric; + metrics.Add(metric); + this.metricStreamNames.Add(metricStreamName); + } + } + + return metrics; + } + } + + internal void RecordLongMeasurement(List metrics, long value, ReadOnlySpan> tags) + { + if (metrics.Count == 1) + { + // special casing the common path + // as this is faster than the + // foreach, when count is 1. + metrics[0].UpdateLong(value, tags); + } + else + { + foreach (var metric in metrics) + { + metric.UpdateLong(value, tags); + } + } + } + + internal void RecordDoubleMeasurement(List metrics, double value, ReadOnlySpan> tags) + { + if (metrics.Count == 1) + { + // special casing the common path + // as this is faster than the + // foreach, when count is 1. + metrics[0].UpdateDouble(value, tags); + } + else + { + foreach (var metric in metrics) + { + metric.UpdateDouble(value, tags); + } + } + } + + internal void CompleteSingleStreamMeasurement(Metric metric) + { + metric.InstrumentDisposed = true; + } + + internal void CompleteMeasurement(List metrics) + { + foreach (var metric in metrics) + { + metric.InstrumentDisposed = true; + } + } + + internal void SetMaxMetricStreams(int maxMetricStreams) + { + this.maxMetricStreams = maxMetricStreams; + this.metrics = new Metric[maxMetricStreams]; + this.metricsCurrentBatch = new Metric[maxMetricStreams]; + } + + internal void SetMaxMetricPointsPerMetricStream(int maxMetricPointsPerMetricStream) + { + this.maxMetricPointsPerMetricStream = maxMetricPointsPerMetricStream; + } + + private Batch GetMetricsBatch() + { + try + { + var indexSnapshot = Math.Min(this.metricIndex, this.maxMetricStreams - 1); + var target = indexSnapshot + 1; + int metricCountCurrentBatch = 0; + for (int i = 0; i < target; i++) + { + var metric = this.metrics[i]; + int metricPointSize = 0; + if (metric != null) + { + if (metric.InstrumentDisposed) + { + metricPointSize = metric.Snapshot(); + this.metrics[i] = null; + } + else + { + metricPointSize = metric.Snapshot(); + } + + if (metricPointSize > 0) + { + this.metricsCurrentBatch[metricCountCurrentBatch++] = metric; + } + } + } + + return (metricCountCurrentBatch > 0) ? new Batch(this.metricsCurrentBatch, metricCountCurrentBatch) : default; + } + catch (Exception ex) + { + OpenTelemetrySdkEventSource.Log.MetricReaderException(nameof(this.GetMetricsBatch), ex); + return default; + } + } + } +} diff --git a/src/OpenTelemetry/Metrics/MetricAggregators/IHistogramMetric.cs b/src/OpenTelemetry/Metrics/MetricReaderType.cs similarity index 52% rename from src/OpenTelemetry/Metrics/MetricAggregators/IHistogramMetric.cs rename to src/OpenTelemetry/Metrics/MetricReaderType.cs index ad79e52db88..4c20574f552 100644 --- a/src/OpenTelemetry/Metrics/MetricAggregators/IHistogramMetric.cs +++ b/src/OpenTelemetry/Metrics/MetricReaderType.cs @@ -1,4 +1,4 @@ -// +// // Copyright The OpenTelemetry Authors // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,20 +14,23 @@ // limitations under the License. // -using System.Collections.Generic; - namespace OpenTelemetry.Metrics { - public interface IHistogramMetric : IMetric + /// + /// Type of to be used. + /// + public enum MetricReaderType { - bool IsDeltaTemporality { get; } - - IEnumerable Exemplars { get; } - - long PopulationCount { get; } - - double PopulationSum { get; } + /// + /// Use the . + /// This requires manually invoking MetricReader.Collect() to export metrics. + /// + Manual, - IEnumerable Buckets { get; } + /// + /// Use the . + /// MetricReader.Collect() will be invoked on a defined interval. + /// + Periodic, } } diff --git a/src/OpenTelemetry/Metrics/MetricStreamConfiguration.cs b/src/OpenTelemetry/Metrics/MetricStreamConfiguration.cs new file mode 100644 index 00000000000..ccc68f56cac --- /dev/null +++ b/src/OpenTelemetry/Metrics/MetricStreamConfiguration.cs @@ -0,0 +1,54 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace OpenTelemetry.Metrics +{ + // TODO: can be optimized like MetricType + internal enum Aggregation + { +#pragma warning disable SA1602 // Enumeration items should be documented + Default, + Drop, + Sum, + LastValue, + Histogram, +#pragma warning restore SA1602 // Enumeration items should be documented + } + + public class MetricStreamConfiguration + { + public static readonly MetricStreamConfiguration Drop = new DropConfiguration(); + + public string Name { get; set; } + + public string Description { get; set; } + + public string[] TagKeys { get; set; } + + internal virtual Aggregation Aggregation { get; set; } + + // TODO: MetricPoints caps can be configured here + + private sealed class DropConfiguration : MetricStreamConfiguration + { + internal override Aggregation Aggregation + { + get => Aggregation.Drop; + set { } + } + } + } +} diff --git a/src/OpenTelemetry/Metrics/MetricType.cs b/src/OpenTelemetry/Metrics/MetricType.cs new file mode 100644 index 00000000000..bf0ebd604be --- /dev/null +++ b/src/OpenTelemetry/Metrics/MetricType.cs @@ -0,0 +1,73 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; + +namespace OpenTelemetry.Metrics +{ + [Flags] + public enum MetricType : byte + { + /* + Type: + 0x10: Sum + 0x20: Gauge + 0x30: Summary (reserved) + 0x40: Histogram + 0x50: HistogramWithMinMax (reserved) + 0x60: ExponentialHistogram (reserved) + 0x70: ExponentialHistogramWithMinMax (reserved) + 0x80: Reserved + + Point kind: + 0x04: I1 (signed 1-byte integer) + 0x05: U1 (unsigned 1-byte integer) + 0x06: I2 (signed 2-byte integer) + 0x07: U2 (unsigned 2-byte integer) + 0x08: I4 (signed 4-byte integer) + 0x09: U4 (unsigned 4-byte integer) + 0x0a: I8 (signed 8-byte integer) + 0x0b: U8 (unsigned 8-byte integer) + 0x0c: R4 (4-byte floating point) + 0x0d: R8 (8-byte floating point) + */ + + /// + /// Sum of Long type. + /// + LongSum = 0x1a, + + /// + /// Sum of Double type. + /// + DoubleSum = 0x1d, + + /// + /// Gauge of Long type. + /// + LongGauge = 0x2a, + + /// + /// Gauge of Double type. + /// + DoubleGauge = 0x2d, + + /// + /// Histogram. + /// + Histogram = 0x40, + } +} diff --git a/src/OpenTelemetry/Metrics/MetricTypeExtensions.cs b/src/OpenTelemetry/Metrics/MetricTypeExtensions.cs new file mode 100644 index 00000000000..393a5cfe790 --- /dev/null +++ b/src/OpenTelemetry/Metrics/MetricTypeExtensions.cs @@ -0,0 +1,77 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Runtime.CompilerServices; + +namespace OpenTelemetry.Metrics +{ + public static class MetricTypeExtensions + { +#pragma warning disable SA1310 // field should not contain an underscore + + internal const MetricType METRIC_TYPE_MASK = (MetricType)0xf0; + + internal const MetricType METRIC_TYPE_SUM = (MetricType)0x10; + internal const MetricType METRIC_TYPE_GAUGE = (MetricType)0x20; + /* internal const byte METRIC_TYPE_SUMMARY = 0x30; // not used */ + internal const MetricType METRIC_TYPE_HISTOGRAM = (MetricType)0x40; + + internal const MetricType POINT_KIND_MASK = (MetricType)0x0f; + + internal const MetricType POINT_KIND_I1 = (MetricType)0x04; // signed 1-byte integer + internal const MetricType POINT_KIND_U1 = (MetricType)0x05; // unsigned 1-byte integer + internal const MetricType POINT_KIND_I2 = (MetricType)0x06; // signed 2-byte integer + internal const MetricType POINT_KIND_U2 = (MetricType)0x07; // unsigned 2-byte integer + internal const MetricType POINT_KIND_I4 = (MetricType)0x08; // signed 4-byte integer + internal const MetricType POINT_KIND_U4 = (MetricType)0x09; // unsigned 4-byte integer + internal const MetricType POINT_KIND_I8 = (MetricType)0x0a; // signed 8-byte integer + internal const MetricType POINT_KIND_U8 = (MetricType)0x0b; // unsigned 8-byte integer + internal const MetricType POINT_KIND_R4 = (MetricType)0x0c; // 4-byte floating point + internal const MetricType POINT_KIND_R8 = (MetricType)0x0d; // 8-byte floating point + +#pragma warning restore SA1310 // field should not contain an underscore + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsSum(this MetricType self) + { + return (self & METRIC_TYPE_MASK) == METRIC_TYPE_SUM; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsGauge(this MetricType self) + { + return (self & METRIC_TYPE_MASK) == METRIC_TYPE_GAUGE; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsHistogram(this MetricType self) + { + return self.HasFlag(METRIC_TYPE_HISTOGRAM); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsDouble(this MetricType self) + { + return (self & POINT_KIND_MASK) == POINT_KIND_R8; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsLong(this MetricType self) + { + return (self & POINT_KIND_MASK) == POINT_KIND_I8; + } + } +} diff --git a/src/OpenTelemetry/Metrics/PeriodicExportingMetricReader.cs b/src/OpenTelemetry/Metrics/PeriodicExportingMetricReader.cs new file mode 100644 index 00000000000..f6cdcdff155 --- /dev/null +++ b/src/OpenTelemetry/Metrics/PeriodicExportingMetricReader.cs @@ -0,0 +1,136 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Diagnostics; +using System.Threading; +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Metrics +{ + public class PeriodicExportingMetricReader : BaseExportingMetricReader + { + internal const int DefaultExportIntervalMilliseconds = 60000; + internal const int DefaultExportTimeoutMilliseconds = 30000; + + private readonly int exportIntervalMilliseconds; + private readonly int exportTimeoutMilliseconds; + private readonly Thread exporterThread; + private readonly AutoResetEvent exportTrigger = new AutoResetEvent(false); + private readonly ManualResetEvent shutdownTrigger = new ManualResetEvent(false); + private bool disposed; + + public PeriodicExportingMetricReader( + BaseExporter exporter, + int exportIntervalMilliseconds = DefaultExportIntervalMilliseconds, + int exportTimeoutMilliseconds = DefaultExportTimeoutMilliseconds) + : base(exporter) + { + Guard.ThrowIfOutOfRange(exportIntervalMilliseconds, nameof(exportIntervalMilliseconds), min: 1); + Guard.ThrowIfOutOfRange(exportTimeoutMilliseconds, nameof(exportTimeoutMilliseconds), min: 0); + + if ((this.SupportedExportModes & ExportModes.Push) != ExportModes.Push) + { + throw new InvalidOperationException($"The '{nameof(exporter)}' does not support '{nameof(ExportModes)}.{nameof(ExportModes.Push)}'"); + } + + this.exportIntervalMilliseconds = exportIntervalMilliseconds; + this.exportTimeoutMilliseconds = exportTimeoutMilliseconds; + + this.exporterThread = new Thread(new ThreadStart(this.ExporterProc)) + { + IsBackground = true, + Name = $"OpenTelemetry-{nameof(PeriodicExportingMetricReader)}-{exporter.GetType().Name}", + }; + this.exporterThread.Start(); + } + + /// + protected override bool OnShutdown(int timeoutMilliseconds) + { + var result = true; + + this.shutdownTrigger.Set(); + + if (timeoutMilliseconds == Timeout.Infinite) + { + this.exporterThread.Join(); + result = this.exporter.Shutdown() && result; + } + else + { + var sw = Stopwatch.StartNew(); + result = this.exporterThread.Join(timeoutMilliseconds) && result; + var timeout = timeoutMilliseconds - sw.ElapsedMilliseconds; + result = this.exporter.Shutdown((int)Math.Max(timeout, 0)) && result; + } + + return result; + } + + /// + protected override void Dispose(bool disposing) + { + if (!this.disposed) + { + if (disposing) + { + this.exportTrigger.Dispose(); + this.shutdownTrigger.Dispose(); + } + + this.disposed = true; + } + + base.Dispose(disposing); + } + + private void ExporterProc() + { + var sw = Stopwatch.StartNew(); + var triggers = new WaitHandle[] { this.exportTrigger, this.shutdownTrigger }; + + while (true) + { + var timeout = (int)(this.exportIntervalMilliseconds - (sw.ElapsedMilliseconds % this.exportIntervalMilliseconds)); + + int index; + + try + { + index = WaitHandle.WaitAny(triggers, timeout); + } + catch (ObjectDisposedException) + { + return; + } + + switch (index) + { + case 0: // export + this.Collect(this.exportTimeoutMilliseconds); + break; + case 1: // shutdown + this.Collect(this.exportTimeoutMilliseconds); // TODO: do we want to use the shutdown timeout here? + return; + case WaitHandle.WaitTimeout: // timer + this.Collect(this.exportTimeoutMilliseconds); + break; + } + } + } + } +} diff --git a/src/OpenTelemetry/Metrics/DataPoint/IDataValue.cs b/src/OpenTelemetry/Metrics/PeriodicExportingMetricReaderOptions.cs similarity index 64% rename from src/OpenTelemetry/Metrics/DataPoint/IDataValue.cs rename to src/OpenTelemetry/Metrics/PeriodicExportingMetricReaderOptions.cs index 62503b58ef7..af1df534cdc 100644 --- a/src/OpenTelemetry/Metrics/DataPoint/IDataValue.cs +++ b/src/OpenTelemetry/Metrics/PeriodicExportingMetricReaderOptions.cs @@ -1,4 +1,4 @@ -// +// // Copyright The OpenTelemetry Authors // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,8 +16,11 @@ namespace OpenTelemetry.Metrics { - public interface IDataValue + public class PeriodicExportingMetricReaderOptions { - object Value { get; } + /// + /// Gets or sets the metric export interval in milliseconds. The default value is 60000. + /// + public int ExportIntervalMilliseconds { get; set; } = 60000; } } diff --git a/src/OpenTelemetry/Metrics/Processors/MetricProcessor.cs b/src/OpenTelemetry/Metrics/Processors/MetricProcessor.cs deleted file mode 100644 index fae82ef274f..00000000000 --- a/src/OpenTelemetry/Metrics/Processors/MetricProcessor.cs +++ /dev/null @@ -1,41 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -using System; - -namespace OpenTelemetry.Metrics -{ - public abstract class MetricProcessor : BaseProcessor - { - protected readonly BaseExporter exporter; - - protected MetricProcessor(BaseExporter exporter) - { - this.exporter = exporter ?? throw new ArgumentNullException(nameof(exporter)); - } - - // GetMetric or GetMemoryState or GetAggregatedMetrics.. - // ...or some other names - public abstract void SetGetMetricFunction(Func getMetrics); - - internal override void SetParentProvider(BaseProvider parentProvider) - { - base.SetParentProvider(parentProvider); - - this.exporter.ParentProvider = parentProvider; - } - } -} diff --git a/src/OpenTelemetry/Metrics/Processors/PullMetricProcessor.cs b/src/OpenTelemetry/Metrics/Processors/PullMetricProcessor.cs deleted file mode 100644 index c0383278dc2..00000000000 --- a/src/OpenTelemetry/Metrics/Processors/PullMetricProcessor.cs +++ /dev/null @@ -1,71 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -using System; - -namespace OpenTelemetry.Metrics -{ - public class PullMetricProcessor : MetricProcessor, IDisposable - { - private Func getMetrics; - private bool disposed; - private bool isDelta; - - public PullMetricProcessor(BaseExporter exporter, bool isDelta) - : base(exporter) - { - this.isDelta = isDelta; - } - - public override void SetGetMetricFunction(Func getMetrics) - { - this.getMetrics = getMetrics; - } - - public void PullRequest() - { - if (this.getMetrics != null) - { - var metricsToExport = this.getMetrics(this.isDelta); - if (metricsToExport != null && metricsToExport.Metrics.Count > 0) - { - Batch batch = new Batch(metricsToExport); - this.exporter.Export(batch); - } - } - } - - /// - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - - if (disposing && !this.disposed) - { - try - { - this.exporter.Dispose(); - } - catch (Exception) - { - // TODO: Log - } - - this.disposed = true; - } - } - } -} diff --git a/src/OpenTelemetry/Metrics/Processors/PushMetricProcessor.cs b/src/OpenTelemetry/Metrics/Processors/PushMetricProcessor.cs deleted file mode 100644 index 33bc8c5959f..00000000000 --- a/src/OpenTelemetry/Metrics/Processors/PushMetricProcessor.cs +++ /dev/null @@ -1,88 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace OpenTelemetry.Metrics -{ - public class PushMetricProcessor : MetricProcessor, IDisposable - { - private Task exportTask; - private CancellationTokenSource token; - private int exportIntervalMs; - private Func getMetrics; - private bool disposed; - - public PushMetricProcessor(BaseExporter exporter, int exportIntervalMs, bool isDelta) - : base(exporter) - { - this.exportIntervalMs = exportIntervalMs; - this.token = new CancellationTokenSource(); - this.exportTask = new Task(() => - { - while (!this.token.IsCancellationRequested) - { - Task.Delay(this.exportIntervalMs).Wait(); - this.Export(isDelta); - } - }); - - this.exportTask.Start(); - } - - public override void SetGetMetricFunction(Func getMetrics) - { - this.getMetrics = getMetrics; - } - - /// - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - - if (disposing && !this.disposed) - { - try - { - this.token.Cancel(); - this.exporter.Dispose(); - this.exportTask.Wait(); - } - catch (Exception) - { - // TODO: Log - } - - this.disposed = true; - } - } - - private void Export(bool isDelta) - { - if (this.getMetrics != null) - { - var metricsToExport = this.getMetrics(isDelta); - if (metricsToExport != null && metricsToExport.Metrics.Count > 0) - { - Batch batch = new Batch(metricsToExport); - this.exporter.Export(batch); - } - } - } - } -} diff --git a/src/OpenTelemetry/Metrics/PullMetricScope.cs b/src/OpenTelemetry/Metrics/PullMetricScope.cs new file mode 100644 index 00000000000..dd00e844bad --- /dev/null +++ b/src/OpenTelemetry/Metrics/PullMetricScope.cs @@ -0,0 +1,52 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using OpenTelemetry.Context; + +namespace OpenTelemetry.Metrics +{ + internal sealed class PullMetricScope : IDisposable + { + private static readonly RuntimeContextSlot Slot = RuntimeContext.RegisterSlot("otel.pull_metric"); + + private readonly bool previousValue; + private bool disposed; + + internal PullMetricScope(bool value = true) + { + this.previousValue = Slot.Get(); + Slot.Set(value); + } + + internal static bool IsPullAllowed => Slot.Get(); + + public static IDisposable Begin(bool value = true) + { + return new PullMetricScope(value); + } + + /// + public void Dispose() + { + if (!this.disposed) + { + Slot.Set(this.previousValue); + this.disposed = true; + } + } + } +} diff --git a/src/OpenTelemetry/Metrics/ThreadStaticStorage.cs b/src/OpenTelemetry/Metrics/ThreadStaticStorage.cs index d0c109b5ea1..af193f7f7ad 100644 --- a/src/OpenTelemetry/Metrics/ThreadStaticStorage.cs +++ b/src/OpenTelemetry/Metrics/ThreadStaticStorage.cs @@ -17,75 +17,124 @@ using System; using System.Collections.Generic; using System.Runtime.CompilerServices; +using OpenTelemetry.Internal; namespace OpenTelemetry.Metrics { internal class ThreadStaticStorage { - private const int MaxTagCacheSize = 3; + private const int MaxTagCacheSize = 8; [ThreadStatic] private static ThreadStaticStorage storage; - private readonly TagStorage[] tagStorage = new TagStorage[MaxTagCacheSize + 1]; + private readonly TagStorage[] tagStorage = new TagStorage[MaxTagCacheSize]; private ThreadStaticStorage() { - for (int i = 0; i <= MaxTagCacheSize; i++) + for (int i = 0; i < MaxTagCacheSize; i++) { - this.tagStorage[i] = new TagStorage(i); + this.tagStorage[i] = new TagStorage(i + 1); } } [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static ThreadStaticStorage GetStorage() { - if (ThreadStaticStorage.storage == null) + if (storage == null) { - ThreadStaticStorage.storage = new ThreadStaticStorage(); + storage = new ThreadStaticStorage(); } - return ThreadStaticStorage.storage; + return storage; } [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal void SplitToKeysAndValues(ReadOnlySpan> tags, out string[] tagKeys, out object[] tagValues) + internal void SplitToKeysAndValues(ReadOnlySpan> tags, int tagLength, out string[] tagKeys, out object[] tagValues) { - var len = tags.Length; + Guard.ThrowIfZero(tagLength, $"There must be at least one tag to use {nameof(ThreadStaticStorage)}", $"{nameof(tagLength)}"); - if (len <= MaxTagCacheSize) + if (tagLength <= MaxTagCacheSize) { - tagKeys = this.tagStorage[len].TagKey; - tagValues = this.tagStorage[len].TagValue; + tagKeys = this.tagStorage[tagLength - 1].TagKeys; + tagValues = this.tagStorage[tagLength - 1].TagValues; } else { - tagKeys = new string[len]; - tagValues = new object[len]; + tagKeys = new string[tagLength]; + tagValues = new object[tagLength]; } - for (var n = 0; n < len; n++) + for (var n = 0; n < tagLength; n++) { tagKeys[n] = tags[n].Key; tagValues[n] = tags[n].Value; } } - internal class TagStorage + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal void SplitToKeysAndValues(ReadOnlySpan> tags, int tagLength, HashSet tagKeysInteresting, out string[] tagKeys, out object[] tagValues, out int actualLength) { - // Used to copy ReadOnlySpan from API - internal readonly KeyValuePair[] Tags; + // Iterate over tags to find the exact length. + int i = 0; + for (var n = 0; n < tagLength; n++) + { + if (tagKeysInteresting.Contains(tags[n].Key)) + { + i++; + } + } - // Used to split into Key sequence, Value sequence, and KVPs for Aggregator Processor - internal readonly string[] TagKey; - internal readonly object[] TagValue; + actualLength = i; - internal TagStorage(int n) + if (actualLength == 0) + { + tagKeys = null; + tagValues = null; + } + else if (actualLength <= MaxTagCacheSize) + { + tagKeys = this.tagStorage[actualLength - 1].TagKeys; + tagValues = this.tagStorage[actualLength - 1].TagValues; + } + else { - this.Tags = new KeyValuePair[n]; + tagKeys = new string[actualLength]; + tagValues = new object[actualLength]; + } + + // Iterate again (!) to assign the actual value. + // TODO: The dual iteration over tags might be + // avoidable if we change the tagKey and tagObject + // to be a different type (eg: List). + // It might lead to some wasted memory. + // Also, it requires changes to the Dictionary + // used for lookup. + // The TODO here is to make that change + // separately, after benchmarking. + i = 0; + for (var n = 0; n < tagLength; n++) + { + var tag = tags[n]; + if (tagKeysInteresting.Contains(tag.Key)) + { + tagKeys[i] = tag.Key; + tagValues[i] = tag.Value; + i++; + } + } + } - this.TagKey = new string[n]; - this.TagValue = new object[n]; + internal class TagStorage + { + // Used to split into Key sequence, Value sequence. + internal readonly string[] TagKeys; + internal readonly object[] TagValues; + + internal TagStorage(int n) + { + this.TagKeys = new string[n]; + this.TagValues = new object[n]; } } } diff --git a/src/OpenTelemetry/ProviderExtensions.cs b/src/OpenTelemetry/ProviderExtensions.cs index 4928daf4a23..1ae3a85ecfc 100644 --- a/src/OpenTelemetry/ProviderExtensions.cs +++ b/src/OpenTelemetry/ProviderExtensions.cs @@ -14,6 +14,7 @@ // limitations under the License. // +using System; using OpenTelemetry.Logs; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; @@ -58,5 +59,15 @@ public static Resource GetDefaultResource(this BaseProvider baseProvider) { return ResourceBuilder.CreateDefault().Build(); } + + internal static Action GetObservableInstrumentCollectCallback(this BaseProvider baseProvider) + { + if (baseProvider is MeterProviderSdk meterProviderSdk) + { + return meterProviderSdk.CollectObservableInstruments; + } + + return null; + } } } diff --git a/src/OpenTelemetry/README.md b/src/OpenTelemetry/README.md index d7b17455c97..57eacf89671 100644 --- a/src/OpenTelemetry/README.md +++ b/src/OpenTelemetry/README.md @@ -137,7 +137,7 @@ using var tracerProvider = Sdk.CreateTracerProviderBuilder() // named "MyCompany.MyProduct.MyLibrary" only. .AddSource("MyCompany.MyProduct.MyLibrary") // The following subscribes to activities from all Activity Sources - // whose name starts with "ABCCompany.XYZProduct.". + // whose name starts with "ABCCompany.XYZProduct.". .AddSource("ABCCompany.XYZProduct.*") .Build(); ``` @@ -202,10 +202,27 @@ purposes, the SDK provides the following built-in processors: * [BatchExportProcessor<T>](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#batching-processor) : This is an exporting processor which batches the telemetry before sending to the configured exporter. + + The following environment variables can be used to override the default + values of the `BatchExportActivityProcessorOptions`. + + + | Environment variable | `BatchExportActivityProcessorOptions` property | + | -------------------------------- | ---------------------------------------------- | + | `OTEL_BSP_SCHEDULE_DELAY` | `ScheduledDelayMilliseconds` | + | `OTEL_BSP_EXPORT_TIMEOUT` | `ExporterTimeoutMilliseconds` | + | `OTEL_BSP_MAX_QUEUE_SIZE` | `MaxQueueSize` | + | `OTEL_BSP_MAX_EXPORT_BATCH_SIZE` | `MaxExportBatchSizeEnvVarKey` | + + + `FormatException` is thrown in case of an invalid value for any of the + supported environment variables. + * [CompositeProcessor<T>](../../src/OpenTelemetry/CompositeProcessor.cs) : This is a processor which can be composed from multiple processors. This is typically used to construct multiple processing pipelines, each ending with its own exporter. + * [SimpleExportProcessor<T>](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/sdk.md#simple-processor) : This is an exporting processor which passes telemetry to the configured exporter without any batching. @@ -312,8 +329,11 @@ this SDK also ships a "self-diagnostics feature", which helps troubleshooting. When enabled, internal events generated by OpenTelemetry will be written to a log file. -The self-diagnostics feature can be enabled/disabled while the process -is running. +The self-diagnostics feature can be enabled/changed/disabled while the process +is running. The SDK will attempt to read the configuration file every `10` seconds +in non-exclusive read-only mode. The SDK will create or overwrite a file +with new logs according to the configuration. This file will not exceed the +configured max size and will be overwritten in a circular way. To enable self-diagnostics, go to the [current working directory](https://en.wikipedia.org/wiki/Working_directory) of @@ -345,7 +365,10 @@ You can also find the exact directory by calling these methods from your code. can be an absolute path or a relative path to the current directory. 2. `FileSize` is a positive integer, which specifies the log file size in - [KiB](https://en.wikipedia.org/wiki/Kibibyte). + [KiB](https://en.wikipedia.org/wiki/Kibibyte). This value must be between 1 MiB + and 128 MiB (inclusive), or it will be rounded to the closest upper or lower + limit. The log file will never exceed this configured size, and will be + overwritten in a circular way. 3. `LogLevel` is the lowest level of the events to be captured. It has to be one of the @@ -362,19 +385,19 @@ A `FileSize`-KiB log file named as `ExecutableName.ProcessId.log` (e.g. `foobar.exe.12345.log`) will be generated at the specified directory `LogDirectory`, into which logs are written to. -The SDK will attempt to open the configuration file in non-exclusive read-only -mode, read the file and parse it as the configuration file every 10 seconds. If -the SDK fails to parse the `LogDirectory`, `FileSize` or `LogLevel` fields as +If the SDK fails to parse the `LogDirectory`, `FileSize` or `LogLevel` fields as the specified format, the configuration file will be treated as invalid and no -log file would be generated. Otherwise, it will create or overwrite the log file -as described above. - -Note that the `FileSize` has to be between 1 MiB and 128 MiB (inclusive), or it -will be rounded to the closest upper or lower limit. When the `LogDirectory` or -`FileSize` is found to be changed, the SDK will create or overwrite a file with -new logs according to the new configuration. The configuration file has to be no -more than 4 KiB. In case the file is larger than 4 KiB, only the first 4 KiB of -content will be read. +log file would be generated. + +When the `LogDirectory` or `FileSize` is found to be changed, the SDK will create +or overwrite a file with new logs according to the new configuration. The +configuration file has to be no more than 4 KiB. In case the file is larger than +4 KiB, only the first 4 KiB of content will be read. + +The log file might not be a proper text file format to achieve the goal of having +minimal overhead and bounded resource usage: it may have trailing `NUL`s if log +text is less than configured size; once write operation reaches the end, it will +start from beginning and overwrite existing text. ## References diff --git a/src/OpenTelemetry/ReadOnlyTagCollection.cs b/src/OpenTelemetry/ReadOnlyTagCollection.cs new file mode 100644 index 00000000000..fdbfeeaa7de --- /dev/null +++ b/src/OpenTelemetry/ReadOnlyTagCollection.cs @@ -0,0 +1,95 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Collections.Generic; + +namespace OpenTelemetry +{ + /// + /// A read-only collection of tag key/value pairs. + /// + // Note: Does not implement IReadOnlyCollection<> or IEnumerable<> to + // prevent accidental boxing. + public readonly struct ReadOnlyTagCollection + { + private readonly string[] keys; + private readonly object[] values; + + internal ReadOnlyTagCollection(string[] keys, object[] values) + { + this.keys = keys; + this.values = values; + } + + /// + /// Gets the number of tags in the collection. + /// + public int Count => this.keys?.Length ?? 0; + + /// + /// Returns an enumerator that iterates through the tags. + /// + /// . + public Enumerator GetEnumerator() => new(this); + + /// + /// Enumerates the elements of a . + /// + // Note: Does not implement IEnumerator<> to prevent accidental boxing. + public struct Enumerator + { + private readonly ReadOnlyTagCollection source; + private int index; + + internal Enumerator(ReadOnlyTagCollection source) + { + this.source = source; + this.index = 0; + this.Current = default; + } + + /// + /// Gets the tag at the current position of the enumerator. + /// + public KeyValuePair Current { get; private set; } + + /// + /// Advances the enumerator to the next element of the . + /// + /// if the enumerator was + /// successfully advanced to the next element; if the enumerator has passed the end of the + /// collection. + public bool MoveNext() + { + int index = this.index; + + if (index < this.source.Count) + { + this.Current = new KeyValuePair( + this.source.keys[index], + this.source.values[index]); + + this.index++; + return true; + } + + return false; + } + } + } +} diff --git a/src/OpenTelemetry/Resources/OtelEnvResourceDetector.cs b/src/OpenTelemetry/Resources/OtelEnvResourceDetector.cs index cd424e2d034..fd78b3eba4f 100644 --- a/src/OpenTelemetry/Resources/OtelEnvResourceDetector.cs +++ b/src/OpenTelemetry/Resources/OtelEnvResourceDetector.cs @@ -14,9 +14,7 @@ // limitations under the License. // -using System; using System.Collections.Generic; -using System.Security; using OpenTelemetry.Internal; namespace OpenTelemetry.Resources @@ -31,18 +29,10 @@ public Resource Detect() { var resource = Resource.Empty; - try + if (EnvironmentVariableHelper.LoadString(EnvVarKey, out string envResourceAttributeValue)) { - string envResourceAttributeValue = Environment.GetEnvironmentVariable(EnvVarKey); - if (!string.IsNullOrEmpty(envResourceAttributeValue)) - { - var attributes = ParseResourceAttributes(envResourceAttributeValue); - resource = new Resource(attributes); - } - } - catch (SecurityException ex) - { - OpenTelemetrySdkEventSource.Log.ResourceDetectorFailed(nameof(OtelEnvResourceDetector), ex.Message); + var attributes = ParseResourceAttributes(envResourceAttributeValue); + resource = new Resource(attributes); } return resource; diff --git a/src/OpenTelemetry/Resources/OtelServiceNameEnvVarDetector.cs b/src/OpenTelemetry/Resources/OtelServiceNameEnvVarDetector.cs index 806ced7c793..5233f2004a4 100644 --- a/src/OpenTelemetry/Resources/OtelServiceNameEnvVarDetector.cs +++ b/src/OpenTelemetry/Resources/OtelServiceNameEnvVarDetector.cs @@ -14,9 +14,7 @@ // limitations under the License. // -using System; using System.Collections.Generic; -using System.Security; using OpenTelemetry.Internal; namespace OpenTelemetry.Resources @@ -29,20 +27,12 @@ public Resource Detect() { var resource = Resource.Empty; - try + if (EnvironmentVariableHelper.LoadString(EnvVarKey, out string envResourceAttributeValue)) { - string envResourceAttributeValue = Environment.GetEnvironmentVariable(EnvVarKey); - if (!string.IsNullOrEmpty(envResourceAttributeValue)) + resource = new Resource(new Dictionary { - resource = new Resource(new Dictionary - { - [ResourceSemanticConventions.AttributeServiceName] = envResourceAttributeValue, - }); - } - } - catch (SecurityException ex) - { - OpenTelemetrySdkEventSource.Log.ResourceDetectorFailed(nameof(OtelServiceNameEnvVarDetector), ex.Message); + [ResourceSemanticConventions.AttributeServiceName] = envResourceAttributeValue, + }); } return resource; diff --git a/src/OpenTelemetry/Resources/Resource.cs b/src/OpenTelemetry/Resources/Resource.cs index 11099497812..72cb6cab4e4 100644 --- a/src/OpenTelemetry/Resources/Resource.cs +++ b/src/OpenTelemetry/Resources/Resource.cs @@ -14,7 +14,9 @@ // limitations under the License. // +using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using OpenTelemetry.Internal; @@ -101,64 +103,32 @@ private static KeyValuePair SanitizeAttribute(KeyValuePair(sanitizedKey, sanitizedValue); } private static object SanitizeValue(object value, string keyName) { - if (value != null) - { - if (value is string || value is bool || value is double || value is long) - { - return value; - } - - if (value is string[] || value is bool[] || value is double[] || value is long[]) - { - return value; - } - - if (value is int || value is short) - { - return System.Convert.ToInt64(value); - } + Guard.ThrowIfNull(keyName, nameof(keyName)); - if (value is float) - { - return System.Convert.ToDouble(value, System.Globalization.CultureInfo.InvariantCulture); - } - - if (value is int[] || value is short[]) - { - long[] convertedArr = new long[((System.Array)value).Length]; - int i = 0; - foreach (var val in (System.Array)value) - { - convertedArr[i] = System.Convert.ToInt64(val); - i++; - } - - return convertedArr; - } - - if (value is float[]) - { - double[] convertedArr = new double[((float[])value).Length]; - int i = 0; - foreach (float val in (float[])value) - { - convertedArr[i] = System.Convert.ToDouble(val, System.Globalization.CultureInfo.InvariantCulture); - i++; - } - - return convertedArr; - } - - throw new System.ArgumentException("Attribute value type is not an accepted primitive", keyName); - } - - throw new System.ArgumentException("Attribute value is null", keyName); + return value switch + { + string => value, + bool => value, + double => value, + long => value, + string[] => value, + bool[] => value, + double[] => value, + long[] => value, + int => Convert.ToInt64(value), + short => Convert.ToInt64(value), + float => Convert.ToDouble(value, CultureInfo.InvariantCulture), + int[] v => Array.ConvertAll(v, Convert.ToInt64), + short[] v => Array.ConvertAll(v, Convert.ToInt64), + float[] v => Array.ConvertAll(v, f => Convert.ToDouble(f, CultureInfo.InvariantCulture)), + _ => throw new ArgumentException("Attribute value type is not an accepted primitive", keyName), + }; } } } diff --git a/src/OpenTelemetry/Resources/ResourceBuilder.cs b/src/OpenTelemetry/Resources/ResourceBuilder.cs index 99aff474daf..3ae97aef000 100644 --- a/src/OpenTelemetry/Resources/ResourceBuilder.cs +++ b/src/OpenTelemetry/Resources/ResourceBuilder.cs @@ -14,8 +14,8 @@ // limitations under the License. // -using System; using System.Collections.Generic; +using OpenTelemetry.Internal; namespace OpenTelemetry.Resources { @@ -90,10 +90,7 @@ public Resource Build() // https://github.com/open-telemetry/oteps/blob/master/text/0111-auto-resource-detection.md internal ResourceBuilder AddDetector(IResourceDetector resourceDetector) { - if (resourceDetector == null) - { - throw new ArgumentNullException(nameof(resourceDetector)); - } + Guard.ThrowIfNull(resourceDetector, nameof(resourceDetector)); Resource resource = resourceDetector.Detect(); @@ -107,10 +104,7 @@ internal ResourceBuilder AddDetector(IResourceDetector resourceDetector) internal ResourceBuilder AddResource(Resource resource) { - if (resource == null) - { - throw new ArgumentNullException(nameof(resource)); - } + Guard.ThrowIfNull(resource, nameof(resource)); this.resources.Add(resource); diff --git a/src/OpenTelemetry/Resources/ResourceBuilderExtensions.cs b/src/OpenTelemetry/Resources/ResourceBuilderExtensions.cs index 9dd30fc1236..5f38c4cafef 100644 --- a/src/OpenTelemetry/Resources/ResourceBuilderExtensions.cs +++ b/src/OpenTelemetry/Resources/ResourceBuilderExtensions.cs @@ -17,6 +17,7 @@ using System; using System.Collections.Generic; using System.Reflection; +using OpenTelemetry.Internal; namespace OpenTelemetry.Resources { @@ -55,10 +56,7 @@ public static ResourceBuilder AddService( { Dictionary resourceAttributes = new Dictionary(); - if (string.IsNullOrEmpty(serviceName)) - { - throw new ArgumentNullException(nameof(serviceName)); - } + Guard.ThrowIfNullOrEmpty(serviceName, nameof(serviceName)); resourceAttributes.Add(ResourceSemanticConventions.AttributeServiceName, serviceName); diff --git a/src/OpenTelemetry/Trace/BatchExportActivityProcessorOptions.cs b/src/OpenTelemetry/Trace/BatchExportActivityProcessorOptions.cs new file mode 100644 index 00000000000..5b78df251cb --- /dev/null +++ b/src/OpenTelemetry/Trace/BatchExportActivityProcessorOptions.cs @@ -0,0 +1,67 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Diagnostics; +using OpenTelemetry.Internal; + +namespace OpenTelemetry.Trace +{ + /// + /// Batch span processor options. + /// OTEL_BSP_MAX_QUEUE_SIZE, OTEL_BSP_MAX_EXPORT_BATCH_SIZE, OTEL_BSP_EXPORT_TIMEOUT, OTEL_BSP_SCHEDULE_DELAY + /// environment variables are parsed during object construction. + /// + /// + /// The constructor throws if it fails to parse + /// any of the supported environment variables. + /// + public class BatchExportActivityProcessorOptions : BatchExportProcessorOptions + { + internal const string MaxQueueSizeEnvVarKey = "OTEL_BSP_MAX_QUEUE_SIZE"; + + internal const string MaxExportBatchSizeEnvVarKey = "OTEL_BSP_MAX_EXPORT_BATCH_SIZE"; + + internal const string ExporterTimeoutEnvVarKey = "OTEL_BSP_EXPORT_TIMEOUT"; + + internal const string ScheduledDelayEnvVarKey = "OTEL_BSP_SCHEDULE_DELAY"; + + public BatchExportActivityProcessorOptions() + { + int value; + + if (EnvironmentVariableHelper.LoadNumeric(ExporterTimeoutEnvVarKey, out value)) + { + this.ExporterTimeoutMilliseconds = value; + } + + if (EnvironmentVariableHelper.LoadNumeric(MaxExportBatchSizeEnvVarKey, out value)) + { + this.MaxExportBatchSize = value; + } + + if (EnvironmentVariableHelper.LoadNumeric(MaxQueueSizeEnvVarKey, out value)) + { + this.MaxQueueSize = value; + } + + if (EnvironmentVariableHelper.LoadNumeric(ScheduledDelayEnvVarKey, out value)) + { + this.ScheduledDelayMilliseconds = value; + } + } + } +} diff --git a/src/OpenTelemetry/Trace/ExceptionProcessor.cs b/src/OpenTelemetry/Trace/ExceptionProcessor.cs index 16ebcfa14b9..e8e1e9d6c6c 100644 --- a/src/OpenTelemetry/Trace/ExceptionProcessor.cs +++ b/src/OpenTelemetry/Trace/ExceptionProcessor.cs @@ -22,7 +22,7 @@ namespace OpenTelemetry.Trace { - internal class ExceptionProcessor : BaseProcessor + internal sealed class ExceptionProcessor : BaseProcessor { private const string ExceptionPointersKey = "otel.exception_pointers"; @@ -39,7 +39,7 @@ public ExceptionProcessor() } catch (Exception ex) { - throw new NotSupportedException("System.Runtime.InteropServices.Marshal.GetExceptionPointers is not supported.", ex); + throw new NotSupportedException($"'{typeof(Marshal).FullName}.GetExceptionPointers' is not supported", ex); } } diff --git a/src/OpenTelemetry/Trace/ParentBasedSampler.cs b/src/OpenTelemetry/Trace/ParentBasedSampler.cs index a88aad4082c..e83e94b8d12 100644 --- a/src/OpenTelemetry/Trace/ParentBasedSampler.cs +++ b/src/OpenTelemetry/Trace/ParentBasedSampler.cs @@ -13,8 +13,9 @@ // See the License for the specific language governing permissions and // limitations under the License. // -using System; + using System.Diagnostics; +using OpenTelemetry.Internal; namespace OpenTelemetry.Trace { @@ -42,8 +43,9 @@ public sealed class ParentBasedSampler : Sampler /// The to be called for root span/activity. public ParentBasedSampler(Sampler rootSampler) { - this.rootSampler = rootSampler ?? throw new ArgumentNullException(nameof(rootSampler)); + Guard.ThrowIfNull(rootSampler, nameof(rootSampler)); + this.rootSampler = rootSampler; this.Description = $"ParentBased{{{rootSampler.Description}}}"; this.remoteParentSampled = new AlwaysOnSampler(); diff --git a/src/OpenTelemetry/Trace/TraceIdRatioBasedSampler.cs b/src/OpenTelemetry/Trace/TraceIdRatioBasedSampler.cs index 5a5523610d4..049c620e48d 100644 --- a/src/OpenTelemetry/Trace/TraceIdRatioBasedSampler.cs +++ b/src/OpenTelemetry/Trace/TraceIdRatioBasedSampler.cs @@ -16,6 +16,7 @@ using System; using System.Globalization; +using OpenTelemetry.Internal; namespace OpenTelemetry.Trace { @@ -36,10 +37,7 @@ public sealed class TraceIdRatioBasedSampler /// public TraceIdRatioBasedSampler(double probability) { - if (probability < 0.0 || probability > 1.0) - { - throw new ArgumentOutOfRangeException(nameof(probability), probability, "Probability must be in range [0.0, 1.0]"); - } + Guard.ThrowIfOutOfRange(probability, nameof(probability), min: 0.0, max: 1.0); this.probability = probability; diff --git a/src/OpenTelemetry/Trace/TracerProviderBuilderBase.cs b/src/OpenTelemetry/Trace/TracerProviderBuilderBase.cs index 0b752dd15cd..1502dd4e18f 100644 --- a/src/OpenTelemetry/Trace/TracerProviderBuilderBase.cs +++ b/src/OpenTelemetry/Trace/TracerProviderBuilderBase.cs @@ -17,6 +17,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using OpenTelemetry.Internal; using OpenTelemetry.Resources; namespace OpenTelemetry.Trace @@ -29,7 +30,7 @@ public abstract class TracerProviderBuilderBase : TracerProviderBuilder private readonly List instrumentationFactories = new List(); private readonly List> processors = new List>(); private readonly List sources = new List(); - private readonly Dictionary legacyActivityOperationNames = new Dictionary(StringComparer.OrdinalIgnoreCase); + private readonly HashSet legacyActivityOperationNames = new HashSet(StringComparer.OrdinalIgnoreCase); private ResourceBuilder resourceBuilder = ResourceBuilder.CreateDefault(); private Sampler sampler = new ParentBasedSampler(new AlwaysOnSampler()); @@ -42,10 +43,7 @@ public override TracerProviderBuilder AddInstrumentation( Func instrumentationFactory) where TInstrumentation : class { - if (instrumentationFactory == null) - { - throw new ArgumentNullException(nameof(instrumentationFactory)); - } + Guard.ThrowIfNull(instrumentationFactory, nameof(instrumentationFactory)); this.instrumentationFactories.Add( new InstrumentationFactory( @@ -59,17 +57,11 @@ public override TracerProviderBuilder AddInstrumentation( /// public override TracerProviderBuilder AddSource(params string[] names) { - if (names == null) - { - throw new ArgumentNullException(nameof(names)); - } + Guard.ThrowIfNull(names, nameof(names)); foreach (var name in names) { - if (string.IsNullOrWhiteSpace(name)) - { - throw new ArgumentException($"{nameof(names)} contains null or whitespace string."); - } + Guard.ThrowIfNullOrWhitespace(name, nameof(name)); // TODO: We need to fix the listening model. // Today it ignores version. @@ -82,18 +74,15 @@ public override TracerProviderBuilder AddSource(params string[] names) /// public override TracerProviderBuilder AddLegacySource(string operationName) { - if (string.IsNullOrWhiteSpace(operationName)) - { - throw new ArgumentException($"{nameof(operationName)} contains null or whitespace string."); - } + Guard.ThrowIfNullOrWhitespace(operationName, nameof(operationName)); - this.legacyActivityOperationNames[operationName] = true; + this.legacyActivityOperationNames.Add(operationName); return this; } /// - /// Sets whether the status of + /// Sets whether the status of /// should be set to Status.Error when it ended abnormally due to an unhandled exception. /// /// Enabled or not. @@ -117,7 +106,7 @@ internal TracerProviderBuilder SetErrorStatusOnException(bool enabled) } catch (Exception ex) { - throw new NotSupportedException("SetErrorStatusOnException is not supported on this platform.", ex); + throw new NotSupportedException($"'{nameof(this.SetErrorStatusOnException)}' is not supported on this platform", ex); } } } @@ -140,7 +129,9 @@ internal TracerProviderBuilder SetErrorStatusOnException(bool enabled) /// Returns for chaining. internal TracerProviderBuilder SetSampler(Sampler sampler) { - this.sampler = sampler ?? throw new ArgumentNullException(nameof(sampler)); + Guard.ThrowIfNull(sampler, nameof(sampler)); + + this.sampler = sampler; return this; } @@ -152,7 +143,9 @@ internal TracerProviderBuilder SetSampler(Sampler sampler) /// Returns for chaining. internal TracerProviderBuilder SetResourceBuilder(ResourceBuilder resourceBuilder) { - this.resourceBuilder = resourceBuilder ?? throw new ArgumentNullException(nameof(resourceBuilder)); + Guard.ThrowIfNull(resourceBuilder, nameof(resourceBuilder)); + + this.resourceBuilder = resourceBuilder; return this; } @@ -163,10 +156,7 @@ internal TracerProviderBuilder SetResourceBuilder(ResourceBuilder resourceBuilde /// Returns for chaining. internal TracerProviderBuilder AddProcessor(BaseProcessor processor) { - if (processor == null) - { - throw new ArgumentNullException(nameof(processor)); - } + Guard.ThrowIfNull(processor, nameof(processor)); this.processors.Add(processor); diff --git a/src/OpenTelemetry/Trace/TracerProviderBuilderExtensions.cs b/src/OpenTelemetry/Trace/TracerProviderBuilderExtensions.cs index f6b261e12cf..2c1bef0ef5e 100644 --- a/src/OpenTelemetry/Trace/TracerProviderBuilderExtensions.cs +++ b/src/OpenTelemetry/Trace/TracerProviderBuilderExtensions.cs @@ -26,7 +26,7 @@ namespace OpenTelemetry.Trace public static class TracerProviderBuilderExtensions { /// - /// Sets whether the status of + /// Sets whether the status of /// should be set to Status.Error when it ended abnormally due to an unhandled exception. /// /// TracerProviderBuilder instance. @@ -100,7 +100,7 @@ public static TracerProvider Build(this TracerProviderBuilder tracerProviderBuil { if (tracerProviderBuilder is IDeferredTracerProviderBuilder) { - throw new NotSupportedException("DeferredTracerBuilder requires a ServiceProvider to build."); + throw new NotSupportedException($"'{nameof(TracerProviderBuilder)}' requires a '{nameof(IServiceProvider)}' to build"); } if (tracerProviderBuilder is TracerProviderBuilderSdk tracerProviderBuilderSdk) diff --git a/src/OpenTelemetry/Trace/TracerProviderBuilderSdk.cs b/src/OpenTelemetry/Trace/TracerProviderBuilderSdk.cs index 18dae6e2ffe..ba9291e5851 100644 --- a/src/OpenTelemetry/Trace/TracerProviderBuilderSdk.cs +++ b/src/OpenTelemetry/Trace/TracerProviderBuilderSdk.cs @@ -16,7 +16,7 @@ namespace OpenTelemetry.Trace { - internal class TracerProviderBuilderSdk : TracerProviderBuilderBase + internal sealed class TracerProviderBuilderSdk : TracerProviderBuilderBase { internal TracerProvider BuildSdk() => this.Build(); } diff --git a/src/OpenTelemetry/Trace/TracerProviderExtensions.cs b/src/OpenTelemetry/Trace/TracerProviderExtensions.cs index cfa331e179d..555c1cfe6e1 100644 --- a/src/OpenTelemetry/Trace/TracerProviderExtensions.cs +++ b/src/OpenTelemetry/Trace/TracerProviderExtensions.cs @@ -25,15 +25,8 @@ public static class TracerProviderExtensions { public static TracerProvider AddProcessor(this TracerProvider provider, BaseProcessor processor) { - if (provider == null) - { - throw new ArgumentNullException(nameof(provider)); - } - - if (processor == null) - { - throw new ArgumentNullException(nameof(processor)); - } + Guard.ThrowIfNull(provider, nameof(provider)); + Guard.ThrowIfNull(processor, nameof(processor)); if (provider is TracerProviderSdk tracerProviderSdk) { @@ -44,18 +37,18 @@ public static TracerProvider AddProcessor(this TracerProvider provider, BaseProc } /// - /// Flushes the all the processors at TracerProviderSdk, blocks the current thread until flush - /// completed, shutdown signaled or timed out. + /// Flushes all the processors registered under TracerProviderSdk, blocks the current thread + /// until flush completed, shutdown signaled or timed out. /// /// TracerProviderSdk instance on which ForceFlush will be called. /// - /// The number of milliseconds to wait, or Timeout.Infinite to - /// wait indefinitely. + /// The number (non-negative) of milliseconds to wait, or + /// Timeout.Infinite to wait indefinitely. /// /// /// Returns true when force flush succeeded; otherwise, false. /// - /// + /// /// Thrown when the timeoutMilliseconds is smaller than -1. /// /// @@ -63,18 +56,11 @@ public static TracerProvider AddProcessor(this TracerProvider provider, BaseProc /// public static bool ForceFlush(this TracerProvider provider, int timeoutMilliseconds = Timeout.Infinite) { - if (provider == null) - { - throw new ArgumentNullException(nameof(provider)); - } + Guard.ThrowIfNull(provider, nameof(provider)); + Guard.ThrowIfInvalidTimeout(timeoutMilliseconds, nameof(timeoutMilliseconds)); if (provider is TracerProviderSdk tracerProviderSdk) { - if (timeoutMilliseconds < 0 && timeoutMilliseconds != Timeout.Infinite) - { - throw new ArgumentOutOfRangeException(nameof(timeoutMilliseconds), timeoutMilliseconds, "timeoutMilliseconds should be non-negative."); - } - try { return tracerProviderSdk.OnForceFlush(timeoutMilliseconds); @@ -95,13 +81,13 @@ public static bool ForceFlush(this TracerProvider provider, int timeoutMilliseco /// /// TracerProviderSdk instance on which Shutdown will be called. /// - /// The number of milliseconds to wait, or Timeout.Infinite to - /// wait indefinitely. + /// The number (non-negative) of milliseconds to wait, or + /// Timeout.Infinite to wait indefinitely. /// /// /// Returns true when shutdown succeeded; otherwise, false. /// - /// + /// /// Thrown when the timeoutMilliseconds is smaller than -1. /// /// @@ -110,18 +96,11 @@ public static bool ForceFlush(this TracerProvider provider, int timeoutMilliseco /// public static bool Shutdown(this TracerProvider provider, int timeoutMilliseconds = Timeout.Infinite) { - if (provider == null) - { - throw new ArgumentNullException(nameof(provider)); - } + Guard.ThrowIfNull(provider, nameof(provider)); + Guard.ThrowIfInvalidTimeout(timeoutMilliseconds, nameof(timeoutMilliseconds)); if (provider is TracerProviderSdk tracerProviderSdk) { - if (timeoutMilliseconds < 0 && timeoutMilliseconds != Timeout.Infinite) - { - throw new ArgumentOutOfRangeException(nameof(timeoutMilliseconds), timeoutMilliseconds, "timeoutMilliseconds should be non-negative."); - } - if (Interlocked.Increment(ref tracerProviderSdk.ShutdownCount) > 1) { return false; // shutdown already called diff --git a/src/OpenTelemetry/Trace/TracerProviderSdk.cs b/src/OpenTelemetry/Trace/TracerProviderSdk.cs index 898e8b0a894..46cc99892bd 100644 --- a/src/OpenTelemetry/Trace/TracerProviderSdk.cs +++ b/src/OpenTelemetry/Trace/TracerProviderSdk.cs @@ -25,7 +25,7 @@ namespace OpenTelemetry.Trace { - internal class TracerProviderSdk : TracerProvider + internal sealed class TracerProviderSdk : TracerProvider { internal int ShutdownCount; @@ -35,6 +35,7 @@ internal class TracerProviderSdk : TracerProvider private readonly Action getRequestedDataAction; private readonly bool supportLegacyActivity; private BaseProcessor processor; + private bool disposed; internal TracerProviderSdk( Resource resource, @@ -42,12 +43,24 @@ internal TracerProviderSdk( IEnumerable instrumentationFactories, Sampler sampler, List> processors, - Dictionary legacyActivityOperationNames) + HashSet legacyActivityOperationNames) { this.Resource = resource; this.sampler = sampler; this.supportLegacyActivity = legacyActivityOperationNames.Count > 0; + bool legacyActivityWildcardMode = false; + Regex legacyActivityWildcardModeRegex = null; + foreach (var legacyName in legacyActivityOperationNames) + { + if (legacyName.Contains('*')) + { + legacyActivityWildcardMode = true; + legacyActivityWildcardModeRegex = GetWildcardRegex(legacyActivityOperationNames); + break; + } + } + foreach (var processor in processors) { this.AddProcessor(processor); @@ -61,17 +74,27 @@ internal TracerProviderSdk( } } - var listener = new ActivityListener + var listener = new ActivityListener(); + + if (this.supportLegacyActivity) { - // Callback when Activity is started. - ActivityStarted = (activity) => + Func legacyActivityPredicate = null; + if (legacyActivityWildcardMode) + { + legacyActivityPredicate = activity => legacyActivityWildcardModeRegex.IsMatch(activity.OperationName); + } + else + { + legacyActivityPredicate = activity => legacyActivityOperationNames.Contains(activity.OperationName); + } + + listener.ActivityStarted = activity => { OpenTelemetrySdkEventSource.Log.ActivityStarted(activity); - if (this.supportLegacyActivity && string.IsNullOrEmpty(activity.Source.Name)) + if (string.IsNullOrEmpty(activity.Source.Name)) { - // We have a legacy activity in hand now - if (legacyActivityOperationNames.ContainsKey(activity.OperationName)) + if (legacyActivityPredicate(activity)) { // Legacy activity matches the user configured list. // Call sampler for the legacy activity @@ -101,21 +124,16 @@ internal TracerProviderSdk( { this.processor?.OnStart(activity); } - }, + }; - // Callback when Activity is stopped. - ActivityStopped = (activity) => + listener.ActivityStopped = activity => { OpenTelemetrySdkEventSource.Log.ActivityStopped(activity); - if (this.supportLegacyActivity && string.IsNullOrEmpty(activity.Source.Name)) + if (string.IsNullOrEmpty(activity.Source.Name) && !legacyActivityPredicate(activity)) { - // We have a legacy activity in hand now - if (!legacyActivityOperationNames.ContainsKey(activity.OperationName)) - { - // Legacy activity doesn't match the user configured list. No need to proceed further. - return; - } + // Legacy activity doesn't match the user configured list. No need to proceed further. + return; } if (!activity.IsAllDataRequested) @@ -135,8 +153,43 @@ internal TracerProviderSdk( { this.processor?.OnEnd(activity); } - }, - }; + }; + } + else + { + listener.ActivityStarted = activity => + { + OpenTelemetrySdkEventSource.Log.ActivityStarted(activity); + + if (activity.IsAllDataRequested && SuppressInstrumentationScope.IncrementIfTriggered() == 0) + { + this.processor?.OnStart(activity); + } + }; + + listener.ActivityStopped = activity => + { + OpenTelemetrySdkEventSource.Log.ActivityStopped(activity); + + if (!activity.IsAllDataRequested) + { + return; + } + + // Spec says IsRecording must be false once span ends. + // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md#isrecording + // However, Activity has slightly different semantic + // than Span and we don't have strong reason to do this + // now, as Activity anyway allows read/write always. + // Intentionally commenting the following line. + // activity.IsAllDataRequested = false; + + if (SuppressInstrumentationScope.DecrementIfTriggered() == 0) + { + this.processor?.OnEnd(activity); + } + }; + } if (sampler is AlwaysOnSampler) { @@ -172,13 +225,13 @@ internal TracerProviderSdk( if (name.Contains('*')) { wildcardMode = true; + break; } } if (wildcardMode) { - var pattern = "^(" + string.Join("|", from name in sources select '(' + Regex.Escape(name).Replace("\\*", ".*") + ')') + ")$"; - var regex = new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase); + var regex = GetWildcardRegex(sources); // Function which takes ActivitySource and returns true/false to indicate if it should be subscribed to // or not. @@ -189,21 +242,16 @@ internal TracerProviderSdk( } else { - var activitySources = new Dictionary(StringComparer.OrdinalIgnoreCase); - - foreach (var name in sources) - { - activitySources[name] = true; - } + var activitySources = new HashSet(sources, StringComparer.OrdinalIgnoreCase); if (this.supportLegacyActivity) { - activitySources[string.Empty] = true; + activitySources.Add(string.Empty); } // Function which takes ActivitySource and returns true/false to indicate if it should be subscribed to // or not. - listener.ShouldListenTo = (activitySource) => activitySources.ContainsKey(activitySource.Name); + listener.ShouldListenTo = (activitySource) => activitySources.Contains(activitySource.Name); } } else @@ -216,6 +264,12 @@ internal TracerProviderSdk( ActivitySource.AddActivityListener(listener); this.listener = listener; + + Regex GetWildcardRegex(IEnumerable collection) + { + var pattern = '^' + string.Join("|", from name in collection select "(?:" + Regex.Escape(name).Replace("\\*", ".*") + ')') + '$'; + return new Regex(pattern, RegexOptions.Compiled | RegexOptions.IgnoreCase); + } } internal Resource Resource { get; } @@ -228,10 +282,7 @@ internal TracerProviderSdk( internal TracerProviderSdk AddProcessor(BaseProcessor processor) { - if (processor == null) - { - throw new ArgumentNullException(nameof(processor)); - } + Guard.ThrowIfNull(processor, nameof(processor)); processor.SetParentProvider(this); @@ -265,8 +316,8 @@ internal bool OnForceFlush(int timeoutMilliseconds) /// thread until shutdown completed or timed out. /// /// - /// The number of milliseconds to wait, or Timeout.Infinite to - /// wait indefinitely. + /// The number (non-negative) of milliseconds to wait, or + /// Timeout.Infinite to wait indefinitely. /// /// /// Returns true when shutdown succeeded; otherwise, false. @@ -297,26 +348,34 @@ internal bool OnShutdown(int timeoutMilliseconds) protected override void Dispose(bool disposing) { - if (this.instrumentations != null) + if (!this.disposed) { - foreach (var item in this.instrumentations) + if (disposing) { - (item as IDisposable)?.Dispose(); - } + if (this.instrumentations != null) + { + foreach (var item in this.instrumentations) + { + (item as IDisposable)?.Dispose(); + } - this.instrumentations.Clear(); - } + this.instrumentations.Clear(); + } - (this.sampler as IDisposable)?.Dispose(); + (this.sampler as IDisposable)?.Dispose(); - // Wait for up to 5 seconds grace period - this.processor?.Shutdown(5000); - this.processor?.Dispose(); + // Wait for up to 5 seconds grace period + this.processor?.Shutdown(5000); + this.processor?.Dispose(); - // Shutdown the listener last so that anything created while instrumentation cleans up will still be processed. - // Redis instrumentation, for example, flushes during dispose which creates Activity objects for any profiling - // sessions that were open. - this.listener?.Dispose(); + // Shutdown the listener last so that anything created while instrumentation cleans up will still be processed. + // Redis instrumentation, for example, flushes during dispose which creates Activity objects for any profiling + // sessions that were open. + this.listener?.Dispose(); + } + + this.disposed = true; + } base.Dispose(disposing); } @@ -339,7 +398,7 @@ private static ActivitySamplingResult ComputeActivitySamplingResult( { SamplingDecision.RecordAndSample => ActivitySamplingResult.AllDataAndRecorded, SamplingDecision.RecordOnly => ActivitySamplingResult.AllData, - _ => ActivitySamplingResult.PropagationData + _ => ActivitySamplingResult.PropagationData, }; if (activitySamplingResult != ActivitySamplingResult.PropagationData) diff --git a/test/Benchmarks/Benchmarks.csproj b/test/Benchmarks/Benchmarks.csproj index d92339811ec..282f5305fe0 100644 --- a/test/Benchmarks/Benchmarks.csproj +++ b/test/Benchmarks/Benchmarks.csproj @@ -1,33 +1,34 @@ - + Exe - netcoreapp3.1;net5.0;net462 + netcoreapp3.1;net5.0;net6.0;net462 false + + + + + + + - - + + + + + - - - - - - - - - diff --git a/test/Benchmarks/Exporter/JaegerExporterBenchmarks.cs b/test/Benchmarks/Exporter/JaegerExporterBenchmarks.cs index cf452c2cb74..b19fe98e79b 100644 --- a/test/Benchmarks/Exporter/JaegerExporterBenchmarks.cs +++ b/test/Benchmarks/Exporter/JaegerExporterBenchmarks.cs @@ -14,13 +14,16 @@ // limitations under the License. // +extern alias Jaeger; + using System.Diagnostics; using BenchmarkDotNet.Attributes; using Benchmarks.Helper; +using Jaeger::OpenTelemetry.Exporter; +using Jaeger::OpenTelemetry.Exporter.Jaeger.Implementation; +using Jaeger::Thrift.Protocol; using OpenTelemetry; -using OpenTelemetry.Exporter; using OpenTelemetry.Internal; -using Thrift.Transport; namespace Benchmarks.Exporter { @@ -48,9 +51,10 @@ public void JaegerExporter_Batching() { using JaegerExporter exporter = new JaegerExporter( new JaegerExporterOptions(), - new BlackHoleTransport()) + new TCompactProtocol.Factory(), + new NoopJaegerClient()) { - Process = new OpenTelemetry.Exporter.Jaeger.Implementation.Process("TestService"), + Process = new Jaeger::OpenTelemetry.Exporter.Jaeger.Implementation.Process("TestService"), }; for (int i = 0; i < this.NumberOfBatches; i++) @@ -66,26 +70,25 @@ public void JaegerExporter_Batching() exporter.Shutdown(); } - private class BlackHoleTransport : TTransport + private sealed class NoopJaegerClient : IJaegerClient { - public override bool IsOpen => true; + public bool Connected => true; - public override void Close() + public void Close() { - // do nothing } - public override void Write(byte[] buffer, int offset, int length) + public void Connect() { } - public override int Flush() + public void Dispose() { - return 0; } - protected override void Dispose(bool disposing) + public int Send(byte[] buffer, int offset, int count) { + return count; } } } diff --git a/test/Benchmarks/Exporter/OtlpExporterBenchmarks.cs b/test/Benchmarks/Exporter/OtlpGrpcExporterBenchmarks.cs similarity index 80% rename from test/Benchmarks/Exporter/OtlpExporterBenchmarks.cs rename to test/Benchmarks/Exporter/OtlpGrpcExporterBenchmarks.cs index e3b17203de7..f30469e31e7 100644 --- a/test/Benchmarks/Exporter/OtlpExporterBenchmarks.cs +++ b/test/Benchmarks/Exporter/OtlpGrpcExporterBenchmarks.cs @@ -1,4 +1,4 @@ -// +// // Copyright The OpenTelemetry Authors // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,6 +14,8 @@ // limitations under the License. // +extern alias OpenTelemetryProtocol; + using System; using System.Diagnostics; using System.Threading; @@ -21,14 +23,15 @@ using Benchmarks.Helper; using Grpc.Core; using OpenTelemetry; -using OpenTelemetry.Exporter; using OpenTelemetry.Internal; -using OtlpCollector = Opentelemetry.Proto.Collector.Trace.V1; +using OpenTelemetryProtocol::OpenTelemetry.Exporter; +using OpenTelemetryProtocol::OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; +using OtlpCollector = OpenTelemetryProtocol::Opentelemetry.Proto.Collector.Trace.V1; namespace Benchmarks.Exporter { [MemoryDiagnoser] - public class OtlpExporterBenchmarks + public class OtlpGrpcExporterBenchmarks { private OtlpTraceExporter exporter; private Activity activity; @@ -43,9 +46,11 @@ public class OtlpExporterBenchmarks [GlobalSetup] public void GlobalSetup() { + var options = new OtlpExporterOptions(); this.exporter = new OtlpTraceExporter( - new OtlpExporterOptions(), - new NoopTraceServiceClient()); + options, + new OtlpGrpcTraceExportClient(options, new NoopTraceServiceClient())); + this.activity = ActivityHelper.CreateTestActivity(); this.activityBatch = new CircularBuffer(this.NumberOfSpans); } diff --git a/test/Benchmarks/Exporter/OtlpHttpExporterBenchmarks.cs b/test/Benchmarks/Exporter/OtlpHttpExporterBenchmarks.cs new file mode 100644 index 00000000000..4bd8e5b0942 --- /dev/null +++ b/test/Benchmarks/Exporter/OtlpHttpExporterBenchmarks.cs @@ -0,0 +1,106 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +extern alias OpenTelemetryProtocol; + +using System; +using System.Diagnostics; +using System.IO; +using BenchmarkDotNet.Attributes; +using Benchmarks.Helper; +using OpenTelemetry; +using OpenTelemetry.Internal; +using OpenTelemetry.Tests; +using OpenTelemetryProtocol::OpenTelemetry.Exporter; +using OpenTelemetryProtocol::OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; + +namespace Benchmarks.Exporter +{ + [MemoryDiagnoser] + public class OtlpHttpExporterBenchmarks + { + private readonly byte[] buffer = new byte[1024 * 1024]; + private IDisposable server; + private string serverHost; + private int serverPort; + private OtlpTraceExporter exporter; + private Activity activity; + private CircularBuffer activityBatch; + + [Params(1, 10, 100)] + public int NumberOfBatches { get; set; } + + [Params(10000)] + public int NumberOfSpans { get; set; } + + [GlobalSetup] + public void GlobalSetup() + { + this.server = TestHttpServer.RunServer( + (ctx) => + { + using (Stream receiveStream = ctx.Request.InputStream) + { + while (true) + { + if (receiveStream.Read(this.buffer, 0, this.buffer.Length) == 0) + { + break; + } + } + } + + ctx.Response.StatusCode = 200; + ctx.Response.OutputStream.Close(); + }, + out this.serverHost, + out this.serverPort); + + var options = new OtlpExporterOptions + { + Endpoint = new Uri($"http://{this.serverHost}:{this.serverPort}"), + }; + this.exporter = new OtlpTraceExporter( + options, + new OtlpHttpTraceExportClient(options, options.HttpClientFactory())); + + this.activity = ActivityHelper.CreateTestActivity(); + this.activityBatch = new CircularBuffer(this.NumberOfSpans); + } + + [GlobalCleanup] + public void GlobalCleanup() + { + this.exporter.Shutdown(); + this.exporter.Dispose(); + this.server.Dispose(); + } + + [Benchmark] + public void OtlpExporter_Batching() + { + for (int i = 0; i < this.NumberOfBatches; i++) + { + for (int c = 0; c < this.NumberOfSpans; c++) + { + this.activityBatch.Add(this.activity); + } + + this.exporter.Export(new Batch(this.activityBatch, this.NumberOfSpans)); + } + } + } +} diff --git a/test/Benchmarks/Exporter/PrometheusSerializerBenchmarks.cs b/test/Benchmarks/Exporter/PrometheusSerializerBenchmarks.cs new file mode 100644 index 00000000000..05126d5b092 --- /dev/null +++ b/test/Benchmarks/Exporter/PrometheusSerializerBenchmarks.cs @@ -0,0 +1,81 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +extern alias Prometheus; + +using System.Collections.Generic; +using System.Diagnostics.Metrics; +using BenchmarkDotNet.Attributes; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Tests; +using Prometheus::OpenTelemetry.Exporter.Prometheus; + +namespace Benchmarks.Exporter +{ + [MemoryDiagnoser] + public class PrometheusSerializerBenchmarks + { + private Meter meter; + private MeterProvider meterProvider; + private List metrics = new List(); + private byte[] buffer = new byte[85000]; + + [Params(1, 1000, 10000)] + public int NumberOfSerializeCalls { get; set; } + + [GlobalSetup] + public void GlobalSetup() + { + this.meter = new Meter(Utils.GetCurrentMethodName()); + this.meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(this.meter.Name) + .AddInMemoryExporter(this.metrics) + .Build(); + + var counter = this.meter.CreateCounter("counter_name_1", "long", "counter_name_1_description"); + counter.Add(18, new("label1", "value1"), new("label2", "value2")); + + var gauge = this.meter.CreateObservableGauge("gauge_name_1", () => 18.0D, "long", "gauge_name_1_description"); + + var histogram = this.meter.CreateHistogram("histogram_name_1", "long", "histogram_name_1_description"); + histogram.Record(100, new("label1", "value1"), new("label2", "value2")); + + this.meterProvider.ForceFlush(); + } + + [GlobalCleanup] + public void GlobalCleanup() + { + this.meter?.Dispose(); + this.meterProvider?.Dispose(); + } + + // TODO: this has a dependency on https://github.com/open-telemetry/opentelemetry-dotnet/issues/2361 + [Benchmark] + public void WriteMetric() + { + for (int i = 0; i < this.NumberOfSerializeCalls; i++) + { + int cursor = 0; + foreach (var metric in this.metrics) + { + cursor = PrometheusSerializer.WriteMetric(this.buffer, cursor, metric); + } + } + } + } +} diff --git a/test/Benchmarks/Exporter/ZipkinExporterBenchmarks.cs b/test/Benchmarks/Exporter/ZipkinExporterBenchmarks.cs index 9d86b1522d2..22612497395 100644 --- a/test/Benchmarks/Exporter/ZipkinExporterBenchmarks.cs +++ b/test/Benchmarks/Exporter/ZipkinExporterBenchmarks.cs @@ -14,15 +14,17 @@ // limitations under the License. // +extern alias Zipkin; + using System; using System.Diagnostics; using System.IO; using BenchmarkDotNet.Attributes; using Benchmarks.Helper; using OpenTelemetry; -using OpenTelemetry.Exporter; using OpenTelemetry.Internal; using OpenTelemetry.Tests; +using Zipkin::OpenTelemetry.Exporter; namespace Benchmarks.Exporter { diff --git a/src/OpenTelemetry/Metrics/DataPoint/DataValue{T}.cs b/test/Benchmarks/Logs/Food.cs similarity index 60% rename from src/OpenTelemetry/Metrics/DataPoint/DataValue{T}.cs rename to test/Benchmarks/Logs/Food.cs index d3733e08f82..f1fabbe06f4 100644 --- a/src/OpenTelemetry/Metrics/DataPoint/DataValue{T}.cs +++ b/test/Benchmarks/Logs/Food.cs @@ -1,4 +1,4 @@ -// +// // Copyright The OpenTelemetry Authors // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,18 +14,17 @@ // limitations under the License. // -namespace OpenTelemetry.Metrics +using Microsoft.Extensions.Logging; + +namespace Benchmarks.Logs { - internal readonly struct DataValue : IDataValue - where T : struct + public static partial class Food { - private readonly T value; - - internal DataValue(T value) - { - this.value = value; - } - - public object Value => (object)this.value; + [LoggerMessage( + EventId = 0, + Level = LogLevel.Information, + Message = "Hello from {food} {price}.")] + public static partial void SayHello( + ILogger logger, string food, double price); } } diff --git a/test/Benchmarks/Logs/LogBenchmarks.cs b/test/Benchmarks/Logs/LogBenchmarks.cs index 00273762675..6c9c1e95303 100644 --- a/test/Benchmarks/Logs/LogBenchmarks.cs +++ b/test/Benchmarks/Logs/LogBenchmarks.cs @@ -14,12 +14,31 @@ // limitations under the License. // -#if NETCOREAPP3_1 using BenchmarkDotNet.Attributes; using Microsoft.Extensions.Logging; using OpenTelemetry; using OpenTelemetry.Logs; +/* +// * Summary * + +BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19044.1466 (21H2) +Intel Core i7-4790 CPU 3.60GHz (Haswell), 1 CPU, 8 logical and 4 physical cores +.NET SDK=6.0.101 + [Host] : .NET 6.0.1 (6.0.121.56705), X64 RyuJIT + DefaultJob : .NET 6.0.1 (6.0.121.56705), X64 RyuJIT + + +| Method | Mean | Error | StdDev | Gen 0 | Allocated | +|--------------------------------------- |-----------:|----------:|----------:|-------:|----------:| +| NoListener | 72.365 ns | 0.9425 ns | 0.8817 ns | 0.0153 | 64 B | +| NoListenerWithLoggerMessageGenerator | 4.769 ns | 0.0161 ns | 0.0142 ns | - | - | +| OneProcessor | 168.330 ns | 0.6198 ns | 0.5494 ns | 0.0553 | 232 B | +| OneProcessorWithLoggerMessageGenerator | 142.898 ns | 0.5233 ns | 0.4086 ns | 0.0401 | 168 B | +| TwoProcessors | 173.727 ns | 0.5978 ns | 0.4992 ns | 0.0553 | 232 B | +| ThreeProcessors | 174.295 ns | 0.7697 ns | 0.7200 ns | 0.0553 | 232 B | +*/ + namespace Benchmarks.Logs { [MemoryDiagnoser] @@ -63,25 +82,37 @@ public LogBenchmarks() [Benchmark] public void NoListener() { - this.loggerWithNoListener.LogInformation("Hello, World!"); + this.loggerWithNoListener.LogInformation("Hello from {name} {price}.", "tomato", 2.99); + } + + [Benchmark] + public void NoListenerWithLoggerMessageGenerator() + { + Food.SayHello(this.loggerWithNoListener, "tomato", 2.99); } [Benchmark] public void OneProcessor() { - this.loggerWithOneProcessor.LogInformation("Hello, World!"); + this.loggerWithOneProcessor.LogInformation("Hello from {name} {price}.", "tomato", 2.99); + } + + [Benchmark] + public void OneProcessorWithLoggerMessageGenerator() + { + Food.SayHello(this.loggerWithOneProcessor, "tomato", 2.99); } [Benchmark] public void TwoProcessors() { - this.loggerWithTwoProcessors.LogInformation("Hello, World!"); + this.loggerWithTwoProcessors.LogInformation("Hello from {name} {price}.", "tomato", 2.99); } [Benchmark] public void ThreeProcessors() { - this.loggerWithThreeProcessors.LogInformation("Hello, World!"); + this.loggerWithThreeProcessors.LogInformation("Hello from {name} {price}.", "tomato", 2.99); } internal class DummyLogProcessor : BaseProcessor @@ -89,4 +120,3 @@ internal class DummyLogProcessor : BaseProcessor } } } -#endif diff --git a/test/Benchmarks/Logs/LogScopeBenchmarks.cs b/test/Benchmarks/Logs/LogScopeBenchmarks.cs index 5906cb03a76..80e57f4daaa 100644 --- a/test/Benchmarks/Logs/LogScopeBenchmarks.cs +++ b/test/Benchmarks/Logs/LogScopeBenchmarks.cs @@ -14,7 +14,6 @@ // limitations under the License. // -#if NETCOREAPP3_1 using System; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -77,4 +76,3 @@ public void ForEachScope() } } } -#endif diff --git a/test/Benchmarks/Metrics/MetricCollectBenchmarks.cs b/test/Benchmarks/Metrics/MetricCollectBenchmarks.cs new file mode 100644 index 00000000000..11cbb0fdf0c --- /dev/null +++ b/test/Benchmarks/Metrics/MetricCollectBenchmarks.cs @@ -0,0 +1,132 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using System.Diagnostics.Metrics; +using System.Threading; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Tests; + +/* +BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19043 +Intel Core i7-8650U CPU 1.90GHz (Kaby Lake R), 1 CPU, 8 logical and 4 physical cores +.NET Core SDK=5.0.400 + [Host] : .NET Core 3.1.18 (CoreCLR 4.700.21.35901, CoreFX 4.700.21.36305), X64 RyuJIT + DefaultJob : .NET Core 3.1.18 (CoreCLR 4.700.21.35901, CoreFX 4.700.21.36305), X64 RyuJIT + + +| Method | UseWithRef | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated | +|-------- |----------- |---------:|---------:|---------:|------:|------:|------:|----------:| +| Collect | False | 51.38 us | 1.027 us | 1.261 us | - | - | - | 136 B | +| Collect | True | 33.86 us | 0.716 us | 2.088 us | - | - | - | 136 B | +*/ + +namespace Benchmarks.Metrics +{ + [MemoryDiagnoser] + public class MetricCollectBenchmarks + { + private Counter counter; + private MeterProvider provider; + private Meter meter; + private CancellationTokenSource token; + private BaseExportingMetricReader reader; + private Task writeMetricTask; + private string[] dimensionValues = new string[] { "DimVal1", "DimVal2", "DimVal3", "DimVal4", "DimVal5", "DimVal6", "DimVal7", "DimVal8", "DimVal9", "DimVal10" }; + + // TODO: Confirm if this needs to be thread-safe + private Random random = new Random(); + + [Params(false, true)] + public bool UseWithRef { get; set; } + + [GlobalSetup] + public void Setup() + { + var metricExporter = new TestExporter(ProcessExport); + void ProcessExport(Batch batch) + { + double sum = 0; + foreach (var metric in batch) + { + if (this.UseWithRef) + { + // The performant way of iterating. + foreach (ref readonly var metricPoint in metric.GetMetricPoints()) + { + sum += metricPoint.GetSumDouble(); + } + } + else + { + // The non-performant way of iterating. + // This is still "correct", but less performant. + foreach (var metricPoint in metric.GetMetricPoints()) + { + sum += metricPoint.GetSumDouble(); + } + } + } + } + + this.reader = new BaseExportingMetricReader(metricExporter) + { + Temporality = AggregationTemporality.Cumulative, + }; + + this.meter = new Meter(Utils.GetCurrentMethodName()); + + this.provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(this.meter.Name) + .AddReader(this.reader) + .Build(); + + this.counter = this.meter.CreateCounter("counter"); + this.token = new CancellationTokenSource(); + this.writeMetricTask = new Task(() => + { + while (!this.token.IsCancellationRequested) + { + var tag1 = new KeyValuePair("DimName1", this.dimensionValues[this.random.Next(0, 10)]); + var tag2 = new KeyValuePair("DimName2", this.dimensionValues[this.random.Next(0, 10)]); + var tag3 = new KeyValuePair("DimName3", this.dimensionValues[this.random.Next(0, 10)]); + this.counter.Add(100.00, tag1, tag2, tag3); + } + }); + this.writeMetricTask.Start(); + } + + [GlobalCleanup] + public void Cleanup() + { + this.token.Cancel(); + this.token.Dispose(); + this.writeMetricTask.Wait(); + this.meter.Dispose(); + this.provider.Dispose(); + } + + [Benchmark] + public void Collect() + { + this.reader.Collect(); + } + } +} diff --git a/test/Benchmarks/Metrics/MetricsBenchmarks.cs b/test/Benchmarks/Metrics/MetricsBenchmarks.cs index 64ba7d5294d..0fbef5a11b7 100644 --- a/test/Benchmarks/Metrics/MetricsBenchmarks.cs +++ b/test/Benchmarks/Metrics/MetricsBenchmarks.cs @@ -16,29 +16,39 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.Metrics; using BenchmarkDotNet.Attributes; using OpenTelemetry; +using OpenTelemetry.Exporter; using OpenTelemetry.Metrics; +using OpenTelemetry.Tests; /* -BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19043 -Intel Core i7-8650U CPU 1.90GHz (Kaby Lake R), 1 CPU, 8 logical and 4 physical cores -.NET Core SDK=5.0.302 - [Host] : .NET Core 3.1.17 (CoreCLR 4.700.21.31506, CoreFX 4.700.21.31502), X64 RyuJIT - DefaultJob : .NET Core 3.1.17 (CoreCLR 4.700.21.31506, CoreFX 4.700.21.31502), X64 RyuJIT - - -| Method | WithSDK | Mean | Error | StdDev | Median | Gen 0 | Gen 1 | Gen 2 | Allocated | -|-------------------------- |-------- |----------:|----------:|-----------:|----------:|-------:|------:|------:|----------:| -| CounterHotPath | False | 18.48 ns | 0.366 ns | 0.570 ns | 18.52 ns | - | - | - | - | -| CounterWith1LabelsHotPath | False | 30.25 ns | 1.274 ns | 3.530 ns | 29.14 ns | - | - | - | - | -| CounterWith3LabelsHotPath | False | 82.93 ns | 2.586 ns | 7.124 ns | 81.79 ns | - | - | - | - | -| CounterWith5LabelsHotPath | False | 134.94 ns | 4.756 ns | 13.491 ns | 132.45 ns | 0.0248 | - | - | 104 B | -| CounterHotPath | True | 68.58 ns | 1.417 ns | 3.228 ns | 68.40 ns | - | - | - | - | -| CounterWith1LabelsHotPath | True | 192.19 ns | 8.114 ns | 23.151 ns | 184.06 ns | - | - | - | - | -| CounterWith3LabelsHotPath | True | 799.33 ns | 47.442 ns | 136.882 ns | 757.73 ns | - | - | - | - | -| CounterWith5LabelsHotPath | True | 972.16 ns | 45.809 ns | 133.626 ns | 939.95 ns | 0.0553 | - | - | 232 B | +// * Summary * + +BenchmarkDotNet=v0.13.1, OS=Windows 10.0.19043.1288 (21H1/May2021Update) +Intel Xeon CPU E5-1650 v4 3.60GHz, 1 CPU, 12 logical and 6 physical cores +.NET SDK=6.0.100 + [Host] : .NET 6.0.0 (6.0.21.52210), X64 RyuJIT + DefaultJob : .NET 6.0.0 (6.0.21.52210), X64 RyuJIT + + +| Method | AggregationTemporality | Mean | Error | StdDev | Median | Allocated | +|-------------------------- |----------------------- |----------:|----------:|----------:|----------:|----------:| +| CounterHotPath | Cumulative | 19.35 ns | 0.419 ns | 0.946 ns | 19.25 ns | - | +| CounterWith1LabelsHotPath | Cumulative | 97.25 ns | 1.973 ns | 3.657 ns | 96.57 ns | - | +| CounterWith3LabelsHotPath | Cumulative | 467.93 ns | 9.265 ns | 16.228 ns | 466.28 ns | - | +| CounterWith5LabelsHotPath | Cumulative | 746.34 ns | 14.804 ns | 34.014 ns | 749.77 ns | - | +| CounterWith6LabelsHotPath | Cumulative | 858.71 ns | 17.180 ns | 37.711 ns | 855.80 ns | - | +| CounterWith7LabelsHotPath | Cumulative | 972.73 ns | 19.371 ns | 39.130 ns | 970.10 ns | - | +| CounterHotPath | Delta | 20.27 ns | 0.415 ns | 0.912 ns | 20.36 ns | - | +| CounterWith1LabelsHotPath | Delta | 98.39 ns | 1.979 ns | 4.891 ns | 98.67 ns | - | +| CounterWith3LabelsHotPath | Delta | 483.07 ns | 9.694 ns | 22.850 ns | 478.88 ns | - | +| CounterWith5LabelsHotPath | Delta | 723.44 ns | 14.472 ns | 24.574 ns | 722.89 ns | - | +| CounterWith6LabelsHotPath | Delta | 850.73 ns | 16.661 ns | 19.187 ns | 850.21 ns | - | +| CounterWith7LabelsHotPath | Delta | 946.01 ns | 18.713 ns | 43.742 ns | 930.80 ns | - | + */ namespace Benchmarks.Metrics @@ -52,20 +62,24 @@ public class MetricsBenchmarks private Random random = new Random(); private string[] dimensionValues = new string[] { "DimVal1", "DimVal2", "DimVal3", "DimVal4", "DimVal5", "DimVal6", "DimVal7", "DimVal8", "DimVal9", "DimVal10" }; - [Params(false, true)] - public bool WithSDK { get; set; } + [Params(AggregationTemporality.Cumulative, AggregationTemporality.Delta)] + public AggregationTemporality AggregationTemporality { get; set; } [GlobalSetup] public void Setup() { - if (this.WithSDK) + this.meter = new Meter(Utils.GetCurrentMethodName()); + + var exportedItems = new List(); + var reader = new PeriodicExportingMetricReader(new InMemoryExporter(exportedItems), 1000) { - this.provider = Sdk.CreateMeterProviderBuilder() - .AddSource("TestMeter") // All instruments from this meter are enabled. - .Build(); - } + Temporality = this.AggregationTemporality, + }; + this.provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(this.meter.Name) // All instruments from this meter are enabled. + .AddReader(reader) + .Build(); - this.meter = new Meter("TestMeter"); this.counter = this.meter.CreateCounter("counter"); } @@ -101,12 +115,46 @@ public void CounterWith3LabelsHotPath() [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("DimName4", this.dimensionValues[this.random.Next(0, 10)]); - this.counter?.Add(100, tag1, tag2, tag3, tag4, tag5); + var tags = new TagList + { + { "DimName1", this.dimensionValues[this.random.Next(0, 2)] }, + { "DimName2", this.dimensionValues[this.random.Next(0, 2)] }, + { "DimName3", this.dimensionValues[this.random.Next(0, 5)] }, + { "DimName4", this.dimensionValues[this.random.Next(0, 5)] }, + { "DimName5", this.dimensionValues[this.random.Next(0, 10)] }, + }; + this.counter?.Add(100, tags); + } + + [Benchmark] + public void CounterWith6LabelsHotPath() + { + var tags = new TagList + { + { "DimName1", this.dimensionValues[this.random.Next(0, 2)] }, + { "DimName2", this.dimensionValues[this.random.Next(0, 2)] }, + { "DimName3", this.dimensionValues[this.random.Next(0, 5)] }, + { "DimName4", this.dimensionValues[this.random.Next(0, 5)] }, + { "DimName5", this.dimensionValues[this.random.Next(0, 5)] }, + { "DimName6", this.dimensionValues[this.random.Next(0, 2)] }, + }; + this.counter?.Add(100, tags); + } + + [Benchmark] + public void CounterWith7LabelsHotPath() + { + var tags = new TagList + { + { "DimName1", this.dimensionValues[this.random.Next(0, 2)] }, + { "DimName2", this.dimensionValues[this.random.Next(0, 2)] }, + { "DimName3", this.dimensionValues[this.random.Next(0, 5)] }, + { "DimName4", this.dimensionValues[this.random.Next(0, 5)] }, + { "DimName5", this.dimensionValues[this.random.Next(0, 5)] }, + { "DimName6", this.dimensionValues[this.random.Next(0, 2)] }, + { "DimName7", this.dimensionValues[this.random.Next(0, 1)] }, + }; + this.counter?.Add(100, tags); } } } diff --git a/test/Benchmarks/Metrics/MetricsViewBenchmarks.cs b/test/Benchmarks/Metrics/MetricsViewBenchmarks.cs new file mode 100644 index 00000000000..0bf3d40d506 --- /dev/null +++ b/test/Benchmarks/Metrics/MetricsViewBenchmarks.cs @@ -0,0 +1,131 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using System.Diagnostics.Metrics; +using System.Threading; +using BenchmarkDotNet.Attributes; +using OpenTelemetry; +using OpenTelemetry.Exporter; +using OpenTelemetry.Metrics; +using OpenTelemetry.Tests; + +/* +BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19043 +Intel Core i7-8650U CPU 1.90GHz (Kaby Lake R), 1 CPU, 8 logical and 4 physical cores +.NET Core SDK=5.0.401 + [Host] : .NET Core 5.0.10 (CoreCLR 5.0.1021.41214, CoreFX 5.0.1021.41214), X64 RyuJIT + DefaultJob : .NET Core 5.0.10 (CoreCLR 5.0.1021.41214, CoreFX 5.0.1021.41214), X64 RyuJIT + + +| Method | ViewConfig | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated | +|----------------------------------------- |--------------------- |---------:|---------:|---------:|------:|------:|------:|----------:| +| CounterMeasurementRecordingWithThreeTags | NoView | 503.2 ns | 8.36 ns | 7.82 ns | - | - | - | - | +| CounterMeasurementRecordingWithThreeTags | ViewNoInstrSelect | 552.7 ns | 10.98 ns | 19.24 ns | - | - | - | - | +| CounterMeasurementRecordingWithThreeTags | ViewSelectsInstr | 556.0 ns | 11.12 ns | 24.18 ns | - | - | - | - | +*/ + +namespace Benchmarks.Metrics +{ + [MemoryDiagnoser] + public class MetricsViewBenchmarks + { + private static readonly ThreadLocal ThreadLocalRandom = new ThreadLocal(() => new Random()); + private static readonly string[] DimensionValues = new string[] { "DimVal1", "DimVal2", "DimVal3", "DimVal4", "DimVal5", "DimVal6", "DimVal7", "DimVal8", "DimVal9", "DimVal10" }; + private static readonly int DimensionsValuesLength = DimensionValues.Length; + private List metrics; + private Counter counter; + private MeterProvider provider; + private Meter meter; + + public enum ViewConfiguration + { + /// + /// No views registered in the provider. + /// + NoView, + + /// + /// Provider has view registered, but it doesn't select the instrument. + /// This tests the perf impact View has on hot path, for those + /// instruments not participating in View feature. + /// + ViewNoInstrSelect, + + /// + /// Provider has view registered and it does select the instrument. + /// + ViewSelectsInstr, + } + + [Params( + ViewConfiguration.NoView, + ViewConfiguration.ViewNoInstrSelect, + ViewConfiguration.ViewSelectsInstr)] + public ViewConfiguration ViewConfig { get; set; } + + [GlobalSetup] + public void Setup() + { + this.meter = new Meter(Utils.GetCurrentMethodName()); + this.counter = this.meter.CreateCounter("counter"); + this.metrics = new List(); + + if (this.ViewConfig == ViewConfiguration.NoView) + { + this.provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(this.meter.Name) + .AddReader(new BaseExportingMetricReader(new InMemoryExporter(this.metrics))) + .Build(); + } + else if (this.ViewConfig == ViewConfiguration.ViewNoInstrSelect) + { + this.provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(this.meter.Name) + .AddView("nomatch", new MetricStreamConfiguration() { TagKeys = new string[] { "DimName1", "DimName2", "DimName3" } }) + .AddReader(new BaseExportingMetricReader(new InMemoryExporter(this.metrics))) + .Build(); + } + else if (this.ViewConfig == ViewConfiguration.ViewSelectsInstr) + { + this.provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(this.meter.Name) + .AddView(this.counter.Name, new MetricStreamConfiguration() { TagKeys = new string[] { "DimName1", "DimName2", "DimName3" } }) + .AddReader(new BaseExportingMetricReader(new InMemoryExporter(this.metrics))) + .Build(); + } + } + + [GlobalCleanup] + public void Cleanup() + { + this.meter?.Dispose(); + this.provider?.Dispose(); + } + + [Benchmark] + public void CounterMeasurementRecordingWithThreeTags() + { + var random = ThreadLocalRandom.Value; + this.counter?.Add( + 100, + new KeyValuePair("DimName1", DimensionValues[random.Next(0, DimensionsValuesLength)]), + new KeyValuePair("DimName2", DimensionValues[random.Next(0, DimensionsValuesLength)]), + new KeyValuePair("DimName3", DimensionValues[random.Next(0, DimensionsValuesLength)])); + } + } +} diff --git a/test/Benchmarks/Program.cs b/test/Benchmarks/Program.cs index fe6760ea9f6..bc6cd0d3b4f 100644 --- a/test/Benchmarks/Program.cs +++ b/test/Benchmarks/Program.cs @@ -13,6 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. // + using BenchmarkDotNet.Running; namespace OpenTelemetry.Benchmarks diff --git a/test/Benchmarks/README.md b/test/Benchmarks/README.md index 844e2f7d006..2dac7ab5cd0 100644 --- a/test/Benchmarks/README.md +++ b/test/Benchmarks/README.md @@ -5,5 +5,8 @@ Use the following example to run Benchmarks from command line: Navigate to `./test/Benchmarks` directory and run the following command: -`dotnet run --framework netcoreapp3.1 --configuration Release --filter -*TraceBenchmarks*` + +```sh +dotnet run --framework net6.0 --configuration Release --filter *TraceBenchmarks* +``` + diff --git a/test/Benchmarks/Trace/TraceBenchmarks.cs b/test/Benchmarks/Trace/TraceBenchmarks.cs index e6e5041e953..bef76949936 100644 --- a/test/Benchmarks/Trace/TraceBenchmarks.cs +++ b/test/Benchmarks/Trace/TraceBenchmarks.cs @@ -97,6 +97,18 @@ public TraceBenchmarks() .AddLegacySource("TestOperationName2") .AddProcessor(new DummyActivityProcessor()) .Build(); + + Sdk.CreateTracerProviderBuilder() + .SetSampler(new AlwaysOnSampler()) + .AddLegacySource("ExactMatch.OperationName1") + .AddProcessor(new DummyActivityProcessor()) + .Build(); + + Sdk.CreateTracerProviderBuilder() + .SetSampler(new AlwaysOnSampler()) + .AddLegacySource("WildcardMatch.*") + .AddProcessor(new DummyActivityProcessor()) + .Build(); } [Benchmark] @@ -175,6 +187,22 @@ public void TwoInstrumentations() } } + [Benchmark] + public void LegacyActivity_ExactMatchMode() + { + using (var activity = new Activity("ExactMatch.OperationName1").Start()) + { + } + } + + [Benchmark] + public void LegacyActivity_WildcardMatchMode() + { + using (var activity = new Activity("WildcardMatch.OperationName1").Start()) + { + } + } + internal class DummyActivityProcessor : BaseProcessor { } diff --git a/test/OpenTelemetry.Exporter.Jaeger.Tests/Implementation/Int128Test.cs b/test/OpenTelemetry.Exporter.Jaeger.Tests/Implementation/Int128Test.cs index 09a2cf8ccc8..68ddc6323c9 100644 --- a/test/OpenTelemetry.Exporter.Jaeger.Tests/Implementation/Int128Test.cs +++ b/test/OpenTelemetry.Exporter.Jaeger.Tests/Implementation/Int128Test.cs @@ -27,8 +27,8 @@ public void Int128ConversionWorksAsExpected() var id = ActivityTraceId.CreateFromBytes(new byte[] { 0x1a, 0x0f, 0x54, 0x63, 0x25, 0xa8, 0x56, 0x43, 0x1a, 0x4c, 0x24, 0xea, 0xa8, 0x60, 0xb0, 0xe8 }); var int128 = new Int128(id); - Assert.Equal(unchecked(0x1a0f546325a85643), int128.High); - Assert.Equal(unchecked(0x1a4c24eaa860b0e8), int128.Low); + Assert.Equal(unchecked(0x1a0f546325a85643), int128.High); + Assert.Equal(unchecked(0x1a4c24eaa860b0e8), int128.Low); } } } diff --git a/test/OpenTelemetry.Exporter.Jaeger.Tests/Implementation/JaegerActivityConversionTest.cs b/test/OpenTelemetry.Exporter.Jaeger.Tests/Implementation/JaegerActivityConversionTest.cs index 1670e37876b..3da21d1d589 100644 --- a/test/OpenTelemetry.Exporter.Jaeger.Tests/Implementation/JaegerActivityConversionTest.cs +++ b/test/OpenTelemetry.Exporter.Jaeger.Tests/Implementation/JaegerActivityConversionTest.cs @@ -71,7 +71,7 @@ public void JaegerActivityConverterTest_ConvertActivityToJaegerSpan_AllPropertie Assert.Equal(0x1, jaegerSpan.Flags); Assert.Equal(activity.StartTimeUtc.ToEpochMicroseconds(), jaegerSpan.StartTime); - Assert.Equal((long)(activity.Duration.TotalMilliseconds * 1000), jaegerSpan.Duration); + Assert.Equal(this.TimeSpanToMicroseconds(activity.Duration), jaegerSpan.Duration); var tags = jaegerSpan.Tags.ToArray(); var tag = tags[0]; @@ -159,7 +159,7 @@ public void JaegerActivityConverterTest_ConvertActivityToJaegerSpan_NoAttributes Assert.Equal(0x1, jaegerSpan.Flags); Assert.Equal(activity.StartTimeUtc.ToEpochMicroseconds(), jaegerSpan.StartTime); - Assert.Equal((long)(activity.Duration.TotalMilliseconds * 1000), jaegerSpan.Duration); + Assert.Equal(this.TimeSpanToMicroseconds(activity.Duration), jaegerSpan.Duration); // 2 tags: span.kind & library.name. Assert.Equal(2, jaegerSpan.Tags.Count); @@ -218,7 +218,7 @@ public void JaegerActivityConverterTest_ConvertActivityToJaegerSpan_NoEvents() Assert.Equal(0x1, jaegerSpan.Flags); Assert.Equal(activity.StartTimeUtc.ToEpochMicroseconds(), jaegerSpan.StartTime); - Assert.Equal(activity.Duration.TotalMilliseconds * 1000, jaegerSpan.Duration); + Assert.Equal(this.TimeSpanToMicroseconds(activity.Duration), jaegerSpan.Duration); var tags = jaegerSpan.Tags.ToArray(); var tag = tags[0]; @@ -250,7 +250,7 @@ public void JaegerActivityConverterTest_ConvertActivityToJaegerSpan_NoEvents() [Fact] public void JaegerActivityConverterTest_ConvertActivityToJaegerSpan_NoLinks() { - var activity = CreateTestActivity(addLinks: false); + var activity = CreateTestActivity(addLinks: false, ticksToAdd: 8000); var traceIdAsInt = new Int128(activity.Context.TraceId); var spanIdAsInt = new Int128(activity.Context.SpanId); @@ -269,7 +269,7 @@ public void JaegerActivityConverterTest_ConvertActivityToJaegerSpan_NoLinks() Assert.Equal(0x1, jaegerSpan.Flags); Assert.Equal(activity.StartTimeUtc.ToEpochMicroseconds(), jaegerSpan.StartTime); - Assert.Equal(activity.Duration.TotalMilliseconds * 1000, jaegerSpan.Duration); + Assert.Equal(this.TimeSpanToMicroseconds(activity.Duration), jaegerSpan.Duration); var tags = jaegerSpan.Tags.ToArray(); var tag = tags[0]; @@ -480,10 +480,11 @@ internal static Activity CreateTestActivity( Resource resource = null, ActivityKind kind = ActivityKind.Client, bool isRootSpan = false, - Status? status = null) + Status? status = null, + long ticksToAdd = 60 * TimeSpan.TicksPerSecond) { var startTimestamp = DateTime.UtcNow; - var endTimestamp = startTimestamp.AddSeconds(60); + var endTimestamp = startTimestamp.AddTicks(ticksToAdd); var eventTimestamp = DateTime.UtcNow; var traceId = ActivityTraceId.CreateFromString("e8ea7e9ac72de94e91fabc613f9686b2".AsSpan()); @@ -573,6 +574,11 @@ internal static Activity CreateTestActivity( return activity; } + private long TimeSpanToMicroseconds(TimeSpan timeSpan) + { + return timeSpan.Ticks / (TimeSpan.TicksPerMillisecond / 1000); + } + public class RemoteEndpointPriorityTestCase { public string Name { get; set; } diff --git a/test/OpenTelemetry.Exporter.Jaeger.Tests/Implementation/ThriftUdpClientTransportTests.cs b/test/OpenTelemetry.Exporter.Jaeger.Tests/Implementation/ThriftUdpClientTransportTests.cs deleted file mode 100644 index 01764860d3d..00000000000 --- a/test/OpenTelemetry.Exporter.Jaeger.Tests/Implementation/ThriftUdpClientTransportTests.cs +++ /dev/null @@ -1,151 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -using System; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using Moq; -using Thrift.Transport; -using Xunit; - -namespace OpenTelemetry.Exporter.Jaeger.Implementation.Tests -{ - public class ThriftUdpClientTransportTests : IDisposable - { - private readonly Mock mockClient = new Mock(); - private MemoryStream testingMemoryStream = new MemoryStream(); - - public void Dispose() - { - this.testingMemoryStream?.Dispose(); - } - - [Fact] - public void Constructor_ShouldConnectClient() - { - var host = "host, yo"; - var port = 4528; - - new JaegerThriftClientTransport(host, port, this.testingMemoryStream, this.mockClient.Object); - - this.mockClient.Verify(t => t.Connect(host, port), Times.Once); - } - - [Fact] - public void Close_ShouldCloseClient() - { - var host = "host, yo"; - var port = 4528; - - var transport = new JaegerThriftClientTransport(host, port, this.testingMemoryStream, this.mockClient.Object); - transport.Close(); - - this.mockClient.Verify(t => t.Close(), Times.Once); - } - - [Fact] - public async Task Write_ShouldWriteToMemoryStream() - { - var host = "host, yo"; - var port = 4528; - var writeBuffer = new byte[] { 0x20, 0x10, 0x40, 0x30, 0x18, 0x14, 0x10, 0x28 }; - var readBuffer = new byte[8]; - - var transport = new JaegerThriftClientTransport(host, port, this.testingMemoryStream, this.mockClient.Object); - - transport.Write(writeBuffer); - this.testingMemoryStream.Seek(0, SeekOrigin.Begin); - var size = await this.testingMemoryStream.ReadAsync(readBuffer, 0, 8, CancellationToken.None); - - Assert.Equal(8, size); - Assert.Equal(writeBuffer, readBuffer); - } - - [Fact] - public void Flush_ShouldReturnWhenNothingIsInTheStream() - { - var host = "host, yo"; - var port = 4528; - - var transport = new JaegerThriftClientTransport(host, port, this.testingMemoryStream, this.mockClient.Object); - var tInfo = transport.Flush(); - - this.mockClient.Verify(t => t.Send(It.IsAny()), Times.Never); - } - - [Fact] - public void Flush_ShouldSendStreamBytes() - { - var host = "host, yo"; - var port = 4528; - var streamBytes = new byte[] { 0x20, 0x10, 0x40, 0x30, 0x18, 0x14, 0x10, 0x28 }; - this.testingMemoryStream = new MemoryStream(streamBytes); - - var transport = new JaegerThriftClientTransport(host, port, this.testingMemoryStream, this.mockClient.Object); - var tInfo = transport.Flush(); - - this.mockClient.Verify(t => t.Send(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); - } - - [Fact] - public void Flush_ShouldThrowWhenClientDoes() - { - var host = "host, yo"; - var port = 4528; - var streamBytes = new byte[] { 0x20, 0x10, 0x40, 0x30, 0x18, 0x14, 0x10, 0x28 }; - this.testingMemoryStream = new MemoryStream(streamBytes); - - this.mockClient.Setup(t => t.Send(It.IsAny(), It.IsAny(), It.IsAny())).Throws(new Exception("message, yo")); - - var transport = new JaegerThriftClientTransport(host, port, this.testingMemoryStream, this.mockClient.Object); - - var ex = Assert.Throws(() => transport.Flush()); - - Assert.Equal("Cannot flush closed transport. message, yo", ex.Message); - } - - [Fact] - public void Dispose_ShouldCloseClientAndDisposeMemoryStream() - { - var host = "host, yo"; - var port = 4528; - - var transport = new JaegerThriftClientTransport(host, port, this.testingMemoryStream, this.mockClient.Object); - transport.Dispose(); - - this.mockClient.Verify(t => t.Dispose(), Times.Once); - Assert.False(this.testingMemoryStream.CanRead); - Assert.False(this.testingMemoryStream.CanSeek); - Assert.False(this.testingMemoryStream.CanWrite); - } - - [Fact] - public void Dispose_ShouldNotTryToDisposeResourcesMoreThanOnce() - { - var host = "host, yo"; - var port = 4528; - - var transport = new JaegerThriftClientTransport(host, port, this.testingMemoryStream, this.mockClient.Object); - transport.Dispose(); - transport.Dispose(); - - this.mockClient.Verify(t => t.Dispose(), Times.Once); - Assert.False(this.testingMemoryStream.CanRead); - Assert.False(this.testingMemoryStream.CanSeek); - Assert.False(this.testingMemoryStream.CanWrite); - } - } -} diff --git a/test/OpenTelemetry.Exporter.Jaeger.Tests/JaegerExporterOptionsTests.cs b/test/OpenTelemetry.Exporter.Jaeger.Tests/JaegerExporterOptionsTests.cs index 4716f975a2f..2aad340124e 100644 --- a/test/OpenTelemetry.Exporter.Jaeger.Tests/JaegerExporterOptionsTests.cs +++ b/test/OpenTelemetry.Exporter.Jaeger.Tests/JaegerExporterOptionsTests.cs @@ -59,9 +59,7 @@ public void JaegerExporterOptions_InvalidPortEnvironmentVariableOverride() { Environment.SetEnvironmentVariable(JaegerExporterOptions.OTelAgentPortEnvVarKey, "invalid"); - var options = new JaegerExporterOptions(); - - Assert.Equal(6831, options.AgentPort); // use default + Assert.Throws(() => new JaegerExporterOptions()); } [Fact] diff --git a/test/OpenTelemetry.Exporter.Jaeger.Tests/JaegerExporterTests.cs b/test/OpenTelemetry.Exporter.Jaeger.Tests/JaegerExporterTests.cs index 2fef9caf334..2ce448297a3 100644 --- a/test/OpenTelemetry.Exporter.Jaeger.Tests/JaegerExporterTests.cs +++ b/test/OpenTelemetry.Exporter.Jaeger.Tests/JaegerExporterTests.cs @@ -18,6 +18,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Exporter.Jaeger.Implementation; using OpenTelemetry.Exporter.Jaeger.Implementation.Tests; using OpenTelemetry.Resources; @@ -43,6 +44,71 @@ public void JaegerTraceExporter_ctor_NullServiceNameAllowed() Assert.NotNull(jaegerTraceExporter); } + [Fact] + public void UserHttpFactoryCalled() + { + JaegerExporterOptions options = new JaegerExporterOptions(); + + var defaultFactory = options.HttpClientFactory; + + int invocations = 0; + options.Protocol = JaegerExportProtocol.HttpBinaryThrift; + options.HttpClientFactory = () => + { + invocations++; + return defaultFactory(); + }; + + using (var exporter = new JaegerExporter(options)) + { + Assert.Equal(1, invocations); + } + + using (var provider = Sdk.CreateTracerProviderBuilder() + .AddJaegerExporter(o => + { + o.Protocol = JaegerExportProtocol.HttpBinaryThrift; + o.HttpClientFactory = options.HttpClientFactory; + }) + .Build()) + { + Assert.Equal(2, invocations); + } + + options.HttpClientFactory = null; + Assert.Throws(() => + { + using var exporter = new JaegerExporter(options); + }); + + options.HttpClientFactory = () => null; + Assert.Throws(() => + { + using var exporter = new JaegerExporter(options); + }); + } + + [Fact] + public void ServiceProviderHttpClientFactoryInvoked() + { + IServiceCollection services = new ServiceCollection(); + + services.AddHttpClient(); + + int invocations = 0; + + services.AddHttpClient("JaegerExporter", configureClient: (client) => invocations++); + + services.AddOpenTelemetryTracing(builder => builder.AddJaegerExporter( + o => o.Protocol = JaegerExportProtocol.HttpBinaryThrift)); + + using var serviceProvider = services.BuildServiceProvider(); + + var tracerProvider = serviceProvider.GetRequiredService(); + + Assert.Equal(1, invocations); + } + [Fact] public void JaegerTraceExporter_SetResource_UpdatesServiceName() { @@ -127,21 +193,26 @@ public void JaegerTraceExporter_BuildBatchesToTransmit_FlushedBatch() jaegerExporter.AppendSpan(CreateTestJaegerSpan()); // Assert - Assert.Equal(1, jaegerExporter.Batch.Count); + Assert.Equal(1U, jaegerExporter.NumberOfSpansInCurrentBatch); } - [Fact] - public void JaegerTraceExporter_SpansSplitToBatches_SpansIncludedInBatches() + [Theory] + [InlineData("Compact", 1500)] + [InlineData("Binary", 2200)] + public void JaegerTraceExporter_SpansSplitToBatches_SpansIncludedInBatches(string protocolType, int maxPayloadSizeInBytes) { + TProtocolFactory protocolFactory = protocolType == "Compact" + ? new TCompactProtocol.Factory() + : new TBinaryProtocol.Factory(); + var client = new TestJaegerClient(); + // Arrange - var memoryTransport = new InMemoryTransport(); using var jaegerExporter = new JaegerExporter( - new JaegerExporterOptions { MaxPayloadSizeInBytes = 1500 }, memoryTransport); + new JaegerExporterOptions { MaxPayloadSizeInBytes = maxPayloadSizeInBytes }, + protocolFactory, + client); jaegerExporter.SetResourceAndInitializeBatch(Resource.Empty); - var tempTransport = new InMemoryTransport(initialCapacity: 3000); - var protocol = new TCompactProtocol(tempTransport); - // Create six spans, each taking more space than the previous one var spans = new JaegerSpan[6]; for (int i = 0; i < 6; i++) @@ -153,10 +224,13 @@ public void JaegerTraceExporter_SpansSplitToBatches_SpansIncludedInBatches() }); } + var protocol = protocolFactory.GetProtocol(); var serializedSpans = spans.Select(s => { s.Write(protocol); - return tempTransport.ToArray(); + var data = protocol.WrittenData.ToArray(); + protocol.Clear(); + return data; }).ToArray(); // Act @@ -164,10 +238,11 @@ public void JaegerTraceExporter_SpansSplitToBatches_SpansIncludedInBatches() foreach (var span in spans) { jaegerExporter.AppendSpan(span); - var sentBatch = memoryTransport.ToArray(); - if (sentBatch.Length > 0) + var sentBatch = client.LastWrittenData; + if (sentBatch != null) { sentBatches.Add(sentBatch); + client.LastWrittenData = null; } } @@ -190,9 +265,10 @@ public void JaegerTraceExporter_SpansSplitToBatches_SpansIncludedInBatches() "Expected span data not found in sent batch"); // jaegerExporter.Batch should contain the two remaining spans - Assert.Equal(2, jaegerExporter.Batch.Count); - jaegerExporter.Batch.Write(protocol); - var serializedBatch = tempTransport.ToArray(); + Assert.Equal(2U, jaegerExporter.NumberOfSpansInCurrentBatch); + jaegerExporter.SendCurrentBatch(); + Assert.True(client.LastWrittenData != null); + var serializedBatch = client.LastWrittenData; Assert.True( ContainsSequence(serializedBatch, serializedSpans[4]), "Expected span data not found in unsent batch"); @@ -227,5 +303,30 @@ private static bool ContainsSequence(byte[] source, byte[] pattern) return false; } + + private sealed class TestJaegerClient : IJaegerClient + { + public bool Connected => true; + + public byte[] LastWrittenData { get; set; } + + public void Close() + { + } + + public void Connect() + { + } + + public void Dispose() + { + } + + public int Send(byte[] buffer, int offset, int count) + { + this.LastWrittenData = new ArraySegment(buffer, offset, count).ToArray(); + return count; + } + } } } diff --git a/test/OpenTelemetry.Exporter.Jaeger.Tests/OpenTelemetry.Exporter.Jaeger.Tests.csproj b/test/OpenTelemetry.Exporter.Jaeger.Tests/OpenTelemetry.Exporter.Jaeger.Tests.csproj index 0caeb6bd025..4f3af4b3d1c 100644 --- a/test/OpenTelemetry.Exporter.Jaeger.Tests/OpenTelemetry.Exporter.Jaeger.Tests.csproj +++ b/test/OpenTelemetry.Exporter.Jaeger.Tests/OpenTelemetry.Exporter.Jaeger.Tests.csproj @@ -1,7 +1,7 @@  Unit test project for Jaeger Exporter for OpenTelemetry - netcoreapp3.1;net5.0 + netcoreapp3.1;net5.0;net6.0 $(TargetFrameworks);net461 false @@ -16,10 +16,13 @@ runtime; build; native; contentfiles; analyzers + + + diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Dockerfile b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Dockerfile index 57c45f3cb55..855cac4a51b 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Dockerfile +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Dockerfile @@ -1,11 +1,11 @@ -# Create a container for running the OpenTelemetry Collector integration tests. +# Create a container for running the OpenTelemetry Collector integration tests. # This should be run from the root of the repo: # docker build --file test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Dockerfile -ARG SDK_VERSION=5.0 -FROM mcr.microsoft.com/dotnet/sdk:5.0-focal AS build +ARG SDK_VERSION=6.0 +FROM mcr.microsoft.com/dotnet/sdk:6.0-focal AS build ARG PUBLISH_CONFIGURATION=Release -ARG PUBLISH_FRAMEWORK=net5.0 +ARG PUBLISH_FRAMEWORK=net6.0 WORKDIR /repo COPY . ./ WORKDIR "/repo/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests" diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/ExporterClientValidationTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/ExporterClientValidationTests.cs new file mode 100644 index 00000000000..5667385271f --- /dev/null +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/ExporterClientValidationTests.cs @@ -0,0 +1,77 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; +using Xunit; + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests +{ + public class ExporterClientValidationTests : Http2UnencryptedSupportTests + { + private const string HttpEndpoint = "http://localhost:4173"; + private const string HttpsEndpoint = "https://localhost:4173"; + + [Fact] + public void ExporterClientValidation_FlagIsEnabledForHttpEndpoint() + { + var options = new OtlpExporterOptions + { + Endpoint = new Uri(HttpEndpoint), + }; + + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + + var exception = Record.Exception(() => ExporterClientValidation.EnsureUnencryptedSupportIsEnabled(options)); + Assert.Null(exception); + } + + [Fact] + public void ExporterClientValidation_FlagIsNotEnabledForHttpEndpoint() + { + var options = new OtlpExporterOptions + { + Endpoint = new Uri(HttpEndpoint), + }; + + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", false); + + var exception = Record.Exception(() => ExporterClientValidation.EnsureUnencryptedSupportIsEnabled(options)); + + if (Environment.Version.Major == 3) + { + Assert.NotNull(exception); + Assert.IsType(exception); + } + else + { + Assert.Null(exception); + } + } + + [Fact] + public void ExporterClientValidation_FlagIsNotEnabledForHttpsEndpoint() + { + var options = new OtlpExporterOptions + { + Endpoint = new Uri(HttpsEndpoint), + }; + + var exception = Record.Exception(() => ExporterClientValidation.EnsureUnencryptedSupportIsEnabled(options)); + Assert.Null(exception); + } + } +} diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Http2UnencryptedSupportTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Http2UnencryptedSupportTests.cs new file mode 100644 index 00000000000..907905ff672 --- /dev/null +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Http2UnencryptedSupportTests.cs @@ -0,0 +1,45 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests +{ + public class Http2UnencryptedSupportTests : IDisposable + { + private readonly bool initialFlagStatus; + + public Http2UnencryptedSupportTests() + { + this.initialFlagStatus = this.DetermineInitialFlagStatus(); + } + + public void Dispose() + { + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", this.initialFlagStatus); + } + + private bool DetermineInitialFlagStatus() + { + if (AppContext.TryGetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", out var flag)) + { + return flag; + } + + return false; + } + } +} diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/ExportClient/OtlpHttpTraceExportClientTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/ExportClient/OtlpHttpTraceExportClientTests.cs new file mode 100644 index 00000000000..fe5b8d7b5a4 --- /dev/null +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Implementation/ExportClient/OtlpHttpTraceExportClientTests.cs @@ -0,0 +1,209 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Net.Http; +using System.Threading; +#if !NET5_0_OR_GREATER +using System.Threading.Tasks; +#endif +using Moq; +using Moq.Protected; +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; +using Xunit; +using OtlpCollector = Opentelemetry.Proto.Collector.Trace.V1; + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests.Implementation.ExportClient +{ + public class OtlpHttpTraceExportClientTests + { + static OtlpHttpTraceExportClientTests() + { + Activity.DefaultIdFormat = ActivityIdFormat.W3C; + Activity.ForceDefaultIdFormat = true; + + var listener = new ActivityListener + { + ShouldListenTo = _ => true, + Sample = (ref ActivityCreationOptions options) => ActivitySamplingResult.AllData, + }; + + ActivitySource.AddActivityListener(listener); + } + + [Fact] + public void NewOtlpHttpTraceExportClient_OtlpExporterOptions_ExporterHasCorrectProperties() + { + var header1 = new { Name = "hdr1", Value = "val1" }; + var header2 = new { Name = "hdr2", Value = "val2" }; + + var options = new OtlpExporterOptions + { + Headers = $"{header1.Name}={header1.Value}, {header2.Name} = {header2.Value}", + }; + + var client = new OtlpHttpTraceExportClient(options, options.HttpClientFactory()); + + Assert.NotNull(client.HttpClient); + + Assert.Equal(2, client.Headers.Count); + Assert.Contains(client.Headers, kvp => kvp.Key == header1.Name && kvp.Value == header1.Value); + Assert.Contains(client.Headers, kvp => kvp.Key == header2.Name && kvp.Value == header2.Value); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void SendExportRequest_ExportTraceServiceRequest_SendsCorrectHttpRequest(bool includeServiceNameInResource) + { + // Arrange + var evenTags = new[] { new KeyValuePair("k0", "v0") }; + var oddTags = new[] { new KeyValuePair("k1", "v1") }; + var sources = new[] + { + new ActivitySource("even", "2.4.6"), + new ActivitySource("odd", "1.3.5"), + }; + var header1 = new { Name = "hdr1", Value = "val1" }; + var header2 = new { Name = "hdr2", Value = "val2" }; + + var options = new OtlpExporterOptions + { + Endpoint = new Uri("http://localhost:4317"), + Headers = $"{header1.Name}={header1.Value}, {header2.Name} = {header2.Value}", + }; + + var httpHandlerMock = new Mock(); + + HttpRequestMessage httpRequest = null; + var httpRequestContent = Array.Empty(); + + httpHandlerMock.Protected() +#if NET5_0_OR_GREATER + .Setup("Send", ItExpr.IsAny(), ItExpr.IsAny()) + .Returns((HttpRequestMessage request, CancellationToken token) => + { + return new HttpResponseMessage(); + }) + .Callback((r, ct) => + { + httpRequest = r; + + // We have to capture content as it can't be accessed after request is disposed inside of SendExportRequest method + httpRequestContent = r.Content.ReadAsByteArrayAsync()?.Result; + }) +#else + .Setup>("SendAsync", ItExpr.IsAny(), ItExpr.IsAny()) + .ReturnsAsync((HttpRequestMessage request, CancellationToken token) => + { + return new HttpResponseMessage(); + }) + .Callback(async (r, ct) => + { + httpRequest = r; + + // We have to capture content as it can't be accessed after request is disposed inside of SendExportRequest method + httpRequestContent = await r.Content.ReadAsByteArrayAsync(); + }) +#endif + .Verifiable(); + + var exportClient = new OtlpHttpTraceExportClient(options, new HttpClient(httpHandlerMock.Object)); + + var resourceBuilder = ResourceBuilder.CreateEmpty(); + if (includeServiceNameInResource) + { + resourceBuilder.AddAttributes( + new List> + { + new KeyValuePair(ResourceSemanticConventions.AttributeServiceName, "service_name"), + new KeyValuePair(ResourceSemanticConventions.AttributeServiceNamespace, "ns_1"), + }); + } + + var builder = Sdk.CreateTracerProviderBuilder() + .SetResourceBuilder(resourceBuilder) + .AddSource(sources[0].Name) + .AddSource(sources[1].Name); + + using var openTelemetrySdk = builder.Build(); + + var exportedItems = new List(); + var processor = new BatchActivityExportProcessor(new InMemoryExporter(exportedItems)); + const int numOfSpans = 10; + bool isEven; + for (var i = 0; i < numOfSpans; i++) + { + isEven = i % 2 == 0; + var source = sources[i % 2]; + var activityKind = isEven ? ActivityKind.Client : ActivityKind.Server; + var activityTags = isEven ? evenTags : oddTags; + + using Activity activity = source.StartActivity($"span-{i}", activityKind, parentContext: default, activityTags); + processor.OnEnd(activity); + } + + processor.Shutdown(); + + var batch = new Batch(exportedItems.ToArray(), exportedItems.Count); + RunTest(batch); + + void RunTest(Batch batch) + { + var request = new OtlpCollector.ExportTraceServiceRequest(); + + request.AddBatch(resourceBuilder.Build().ToOtlpResource(), batch); + + // Act + var result = exportClient.SendExportRequest(request); + + // Assert + Assert.True(result); + Assert.NotNull(httpRequest); + Assert.Equal(HttpMethod.Post, httpRequest.Method); + Assert.Equal("http://localhost:4317/", httpRequest.RequestUri.AbsoluteUri); + Assert.Equal(2, httpRequest.Headers.Count()); + Assert.Contains(httpRequest.Headers, h => h.Key == header1.Name && h.Value.First() == header1.Value); + Assert.Contains(httpRequest.Headers, h => h.Key == header2.Name && h.Value.First() == header2.Value); + + Assert.NotNull(httpRequest.Content); + Assert.IsType(httpRequest.Content); + Assert.Contains(httpRequest.Content.Headers, h => h.Key == "Content-Type" && h.Value.First() == OtlpHttpTraceExportClient.MediaContentType); + + var exportTraceRequest = OtlpCollector.ExportTraceServiceRequest.Parser.ParseFrom(httpRequestContent); + Assert.NotNull(exportTraceRequest); + Assert.Single(exportTraceRequest.ResourceSpans); + + var resourceSpan = exportTraceRequest.ResourceSpans.First(); + if (includeServiceNameInResource) + { + Assert.Contains(resourceSpan.Resource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceName && kvp.Value.StringValue == "service_name"); + Assert.Contains(resourceSpan.Resource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceNamespace && kvp.Value.StringValue == "ns_1"); + } + else + { + Assert.Contains(resourceSpan.Resource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceName && kvp.Value.ToString().Contains("unknown_service:")); + } + } + } + } +} diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTests.cs index 2fa973653dd..12403c22d0f 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/IntegrationTests.cs @@ -24,23 +24,29 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests { public class IntegrationTests { - private const string CollectorEndpointEnvVarName = "OTEL_EXPORTER_OTLP_ENDPOINT"; - private static readonly string CollectorEndpoint = SkipUnlessEnvVarFoundFactAttribute.GetEnvironmentVariable(CollectorEndpointEnvVarName); + private const string CollectorHostnameEnvVarName = "OTEL_COLLECTOR_HOSTNAME"; + private static readonly string CollectorHostname = SkipUnlessEnvVarFoundTheoryAttribute.GetEnvironmentVariable(CollectorHostnameEnvVarName); + [InlineData(OtlpExportProtocol.Grpc, ":4317")] + [InlineData(OtlpExportProtocol.HttpProtobuf, ":4318/v1/traces")] [Trait("CategoryName", "CollectorIntegrationTests")] - [SkipUnlessEnvVarFoundFact(CollectorEndpointEnvVarName)] - public void ExportResultIsSuccess() + [SkipUnlessEnvVarFoundTheory(CollectorHostnameEnvVarName)] + public void ExportResultIsSuccess(OtlpExportProtocol protocol, string endpoint) { #if NETCOREAPP3_1 // Adding the OtlpExporter creates a GrpcChannel. - // This switch must be set before creating a GrpcChannel/HttpClient when calling an insecure gRPC service. + // This switch must be set before creating a GrpcChannel when calling an insecure HTTP/2 endpoint. // See: https://docs.microsoft.com/aspnet/core/grpc/troubleshoot#call-insecure-grpc-services-with-net-core-client - AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + if (protocol == OtlpExportProtocol.Grpc) + { + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + } #endif var exporterOptions = new OtlpExporterOptions { - Endpoint = new System.Uri($"http://{CollectorEndpoint}"), + Endpoint = new System.Uri($"http://{CollectorHostname}{endpoint}"), + Protocol = protocol, }; var otlpExporter = new OtlpTraceExporter(exporterOptions); @@ -56,11 +62,38 @@ public void ExportResultIsSuccess() using var tracerProvider = builder.Build(); var source = new ActivitySource(activitySourceName); - var activity = source.StartActivity("Test Activity"); + var activity = source.StartActivity($"{protocol} Test Activity"); activity?.Stop(); Assert.Single(delegatingExporter.ExportResults); Assert.Equal(ExportResult.Success, delegatingExporter.ExportResults[0]); } + + [Trait("CategoryName", "CollectorIntegrationTests")] + [SkipUnlessEnvVarFoundFact(CollectorHostnameEnvVarName)] + public void ConstructingGrpcExporterFailsWhenHttp2UnencryptedSupportIsDisabledForNetcoreapp31() + { + // Adding the OtlpExporter creates a GrpcChannel. + // This switch must be set before creating a GrpcChannel/HttpClient when calling an insecure gRPC service. + // We want to fail fast so we are disabling it + // See: https://docs.microsoft.com/aspnet/core/grpc/troubleshoot#call-insecure-grpc-services-with-net-core-client + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", false); + + var exporterOptions = new OtlpExporterOptions + { + Endpoint = new Uri($"http://{CollectorHostname}:4317"), + }; + + var exception = Record.Exception(() => new OtlpTraceExporter(exporterOptions)); + + if (Environment.Version.Major == 3) + { + Assert.NotNull(exception); + } + else + { + Assert.Null(exception); + } + } } } 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 76516e01b7a..1bce078c2cb 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests.csproj +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1;net5.0 + netcoreapp3.1;net5.0;net6.0 $(TargetFrameworks);net461 $(TARGET_FRAMEWORK) false @@ -15,19 +15,24 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + - + diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsExtensionsTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsExtensionsTests.cs new file mode 100644 index 00000000000..970f96daaea --- /dev/null +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsExtensionsTests.cs @@ -0,0 +1,232 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; +using Xunit; +using Xunit.Sdk; + +namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests +{ + public class OtlpExporterOptionsExtensionsTests : Http2UnencryptedSupportTests + { + [Theory] + [InlineData("key=value", new string[] { "key" }, new string[] { "value" })] + [InlineData("key1=value1,key2=value2", new string[] { "key1", "key2" }, new string[] { "value1", "value2" })] + [InlineData("key1 = value1, key2=value2 ", new string[] { "key1", "key2" }, new string[] { "value1", "value2" })] + [InlineData("key==value", new string[] { "key" }, new string[] { "=value" })] + [InlineData("access-token=abc=/123,timeout=1234", new string[] { "access-token", "timeout" }, new string[] { "abc=/123", "1234" })] + [InlineData("key1=value1;key2=value2", new string[] { "key1" }, new string[] { "value1;key2=value2" })] // semicolon is not treated as a delimeter (https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#specifying-headers-via-environment-variables) + public void GetMetadataFromHeadersWorksCorrectFormat(string headers, string[] keys, string[] values) + { + var options = new OtlpExporterOptions(); + options.Headers = headers; + var metadata = options.GetMetadataFromHeaders(); + + Assert.Equal(keys.Length, metadata.Count); + + for (int i = 0; i < keys.Length; i++) + { + Assert.Contains(metadata, entry => entry.Key == keys[i] && entry.Value == values[i]); + } + } + + [Theory] + [InlineData("headers")] + [InlineData("key,value")] + public void GetMetadataFromHeadersThrowsExceptionOnInvalidFormat(string headers) + { + try + { + var options = new OtlpExporterOptions(); + options.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"); + } + + [Theory] + [InlineData("")] + [InlineData(null)] + public void GetHeaders_NoOptionHeaders_ReturnsEmptyHeadres(string optionHeaders) + { + var options = new OtlpExporterOptions + { + Headers = optionHeaders, + }; + + var headers = options.GetHeaders>((d, k, v) => d.Add(k, v)); + + Assert.Empty(headers); + } + + [Theory] + [InlineData(OtlpExportProtocol.Grpc, typeof(OtlpGrpcTraceExportClient))] + [InlineData(OtlpExportProtocol.HttpProtobuf, typeof(OtlpHttpTraceExportClient))] + public void GetTraceExportClient_SupportedProtocol_ReturnsCorrectExportClient(OtlpExportProtocol protocol, Type expectedExportClientType) + { + if (protocol == OtlpExportProtocol.Grpc && Environment.Version.Major == 3) + { + // Adding the OtlpExporter creates a GrpcChannel. + // This switch must be set before creating a GrpcChannel when calling an insecure HTTP/2 endpoint. + // See: https://docs.microsoft.com/aspnet/core/grpc/troubleshoot#call-insecure-grpc-services-with-net-core-client + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + } + + var options = new OtlpExporterOptions + { + Protocol = protocol, + }; + + var exportClient = options.GetTraceExportClient(); + + Assert.Equal(expectedExportClientType, exportClient.GetType()); + } + + [Fact] + public void GetTraceExportClient_GetClientForGrpcWithoutUnencryptedFlag_ThrowsException() + { + // Adding the OtlpExporter creates a GrpcChannel. + // This switch must be set before creating a GrpcChannel when calling an insecure HTTP/2 endpoint. + // See: https://docs.microsoft.com/aspnet/core/grpc/troubleshoot#call-insecure-grpc-services-with-net-core-client + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", false); + + var options = new OtlpExporterOptions + { + Protocol = OtlpExportProtocol.Grpc, + }; + + var exception = Record.Exception(() => options.GetTraceExportClient()); + + if (Environment.Version.Major == 3) + { + Assert.NotNull(exception); + Assert.IsType(exception); + } + else + { + Assert.Null(exception); + } + } + + [Fact] + public void GetTraceExportClient_UnsupportedProtocol_Throws() + { + var options = new OtlpExporterOptions + { + Protocol = (OtlpExportProtocol)123, + }; + + Assert.Throws(() => options.GetTraceExportClient()); + } + + [Theory] + [InlineData("grpc", OtlpExportProtocol.Grpc)] + [InlineData("http/protobuf", OtlpExportProtocol.HttpProtobuf)] + [InlineData("unsupported", null)] + public void ToOtlpExportProtocol_Protocol_MapsToCorrectValue(string protocol, OtlpExportProtocol? expectedExportProtocol) + { + var exportProtocol = protocol.ToOtlpExportProtocol(); + + Assert.Equal(expectedExportProtocol, exportProtocol); + } + + [Theory] + [InlineData("http://test:8888", "http://test:8888/v1/traces")] + [InlineData("http://test:8888/", "http://test:8888/v1/traces")] + [InlineData("http://test:8888/v1/traces", "http://test:8888/v1/traces")] + [InlineData("http://test:8888/v1/traces/", "http://test:8888/v1/traces/")] + public void AppendPathIfNotPresent_TracesPath_AppendsCorrectly(string inputUri, string expectedUri) + { + var uri = new Uri(inputUri, UriKind.Absolute); + + var resultUri = uri.AppendPathIfNotPresent(OtlpExporterOptions.TracesExportPath); + + Assert.Equal(expectedUri, resultUri.AbsoluteUri); + } + + [Fact] + public void AppendExportPath_EndpointNotSet_EnvironmentVariableNotDefined_NotAppended() + { + ClearEndpointEnvVar(); + + var options = new OtlpExporterOptions { Protocol = OtlpExportProtocol.HttpProtobuf }; + + options.AppendExportPath(options.Endpoint, "test/path"); + + Assert.Equal("http://localhost:4317/", options.Endpoint.AbsoluteUri); + } + + [Fact] + public void AppendExportPath_EndpointNotSet_EnvironmentVariableDefined_Appended() + { + Environment.SetEnvironmentVariable(OtlpExporterOptions.EndpointEnvVarName, "http://test:8888"); + + var options = new OtlpExporterOptions { Protocol = OtlpExportProtocol.HttpProtobuf }; + + options.AppendExportPath(options.Endpoint, "test/path"); + + Assert.Equal("http://test:8888/test/path", options.Endpoint.AbsoluteUri); + + ClearEndpointEnvVar(); + } + + [Fact] + public void AppendExportPath_EndpointSetEqualToEnvironmentVariable_EnvironmentVariableDefined_NotAppended() + { + Environment.SetEnvironmentVariable(OtlpExporterOptions.EndpointEnvVarName, "http://test:8888"); + + var options = new OtlpExporterOptions { Protocol = OtlpExportProtocol.HttpProtobuf }; + var originalEndpoint = options.Endpoint; + options.Endpoint = new Uri("http://test:8888"); + + options.AppendExportPath(originalEndpoint, "test/path"); + + Assert.Equal("http://test:8888/", options.Endpoint.AbsoluteUri); + + ClearEndpointEnvVar(); + } + + [Theory] + [InlineData("http://localhost:4317/")] + [InlineData("http://test:8888/")] + public void AppendExportPath_EndpointSet_EnvironmentVariableNotDefined_NotAppended(string endpoint) + { + ClearEndpointEnvVar(); + + var options = new OtlpExporterOptions { Protocol = OtlpExportProtocol.HttpProtobuf }; + var originalEndpoint = options.Endpoint; + options.Endpoint = new Uri(endpoint); + + options.AppendExportPath(originalEndpoint, "test/path"); + + Assert.Equal(endpoint, options.Endpoint.AbsoluteUri); + } + + private static void ClearEndpointEnvVar() + { + Environment.SetEnvironmentVariable(OtlpExporterOptions.EndpointEnvVarName, null); + } + } +} diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsGrpcExtensionsTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsGrpcExtensionsTests.cs deleted file mode 100644 index c75384cb0aa..00000000000 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsGrpcExtensionsTests.cs +++ /dev/null @@ -1,67 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -using System; -using Xunit; -using Xunit.Sdk; - -namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests -{ - public class OtlpExporterOptionsGrpcExtensionsTests - { - [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 delimeter (https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#specifying-headers-via-environment-variables) - public void GetMetadataFromHeadersWorksCorrectFormat(string headers, string[] keys, string[] values) - { - var options = new OtlpExporterOptions(); - options.Headers = headers; - var metadata = options.GetMetadataFromHeaders(); - - Assert.Equal(keys.Length, metadata.Count); - - for (int i = 0; i < keys.Length; i++) - { - Assert.Contains(metadata, entry => entry.Key == keys[i] && entry.Value == values[i]); - } - } - - [Theory] - [InlineData("headers")] - [InlineData("key,value")] - public void GetMetadataFromHeadersThrowsExceptionOnOnvalidFormat(string headers) - { - try - { - var options = new OtlpExporterOptions(); - options.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"); - } - } -} diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs index d7a698a0d6c..2b7a612fa06 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpExporterOptionsTests.cs @@ -39,6 +39,7 @@ public void OtlpExporterOptions_Defaults() Assert.Equal(new Uri("http://localhost:4317"), options.Endpoint); Assert.Null(options.Headers); Assert.Equal(10000, options.TimeoutMilliseconds); + Assert.Equal(OtlpExportProtocol.Grpc, options.Protocol); } [Fact] @@ -47,12 +48,14 @@ public void OtlpExporterOptions_EnvironmentVariableOverride() Environment.SetEnvironmentVariable(OtlpExporterOptions.EndpointEnvVarName, "http://test:8888"); Environment.SetEnvironmentVariable(OtlpExporterOptions.HeadersEnvVarName, "A=2,B=3"); Environment.SetEnvironmentVariable(OtlpExporterOptions.TimeoutEnvVarName, "2000"); + Environment.SetEnvironmentVariable(OtlpExporterOptions.ProtocolEnvVarName, "http/protobuf"); var options = new OtlpExporterOptions(); Assert.Equal(new Uri("http://test:8888"), options.Endpoint); Assert.Equal("A=2,B=3", options.Headers); Assert.Equal(2000, options.TimeoutMilliseconds); + Assert.Equal(OtlpExportProtocol.HttpProtobuf, options.Protocol); } [Fact] @@ -60,9 +63,7 @@ public void OtlpExporterOptions_InvalidEndpointVariableOverride() { Environment.SetEnvironmentVariable(OtlpExporterOptions.EndpointEnvVarName, "invalid"); - var options = new OtlpExporterOptions(); - - Assert.Equal(new Uri("http://localhost:4317"), options.Endpoint); // use default + Assert.Throws(() => new OtlpExporterOptions()); } [Fact] @@ -70,9 +71,15 @@ public void OtlpExporterOptions_InvalidTimeoutVariableOverride() { Environment.SetEnvironmentVariable(OtlpExporterOptions.TimeoutEnvVarName, "invalid"); - var options = new OtlpExporterOptions(); + Assert.Throws(() => new OtlpExporterOptions()); + } + + [Fact] + public void OtlpExporterOptions_InvalidProtocolVariableOverride() + { + Environment.SetEnvironmentVariable(OtlpExporterOptions.ProtocolEnvVarName, "invalid"); - Assert.Equal(10000, options.TimeoutMilliseconds); // use default + Assert.Throws(() => new OtlpExporterOptions()); } [Fact] @@ -81,17 +88,20 @@ public void OtlpExporterOptions_SetterOverridesEnvironmentVariable() Environment.SetEnvironmentVariable(OtlpExporterOptions.EndpointEnvVarName, "http://test:8888"); Environment.SetEnvironmentVariable(OtlpExporterOptions.HeadersEnvVarName, "A=2,B=3"); Environment.SetEnvironmentVariable(OtlpExporterOptions.TimeoutEnvVarName, "2000"); + Environment.SetEnvironmentVariable(OtlpExporterOptions.ProtocolEnvVarName, "grpc"); var options = new OtlpExporterOptions { Endpoint = new Uri("http://localhost:200"), Headers = "C=3", TimeoutMilliseconds = 40000, + Protocol = OtlpExportProtocol.HttpProtobuf, }; Assert.Equal(new Uri("http://localhost:200"), options.Endpoint); Assert.Equal("C=3", options.Headers); Assert.Equal(40000, options.TimeoutMilliseconds); + Assert.Equal(OtlpExportProtocol.HttpProtobuf, options.Protocol); } [Fact] @@ -100,6 +110,7 @@ public void OtlpExporterOptions_EnvironmentVariableNames() Assert.Equal("OTEL_EXPORTER_OTLP_ENDPOINT", OtlpExporterOptions.EndpointEnvVarName); Assert.Equal("OTEL_EXPORTER_OTLP_HEADERS", OtlpExporterOptions.HeadersEnvVarName); Assert.Equal("OTEL_EXPORTER_OTLP_TIMEOUT", OtlpExporterOptions.TimeoutEnvVarName); + Assert.Equal("OTEL_EXPORTER_OTLP_PROTOCOL", OtlpExporterOptions.ProtocolEnvVarName); } private static void ClearEnvVars() @@ -107,6 +118,7 @@ private static void ClearEnvVars() Environment.SetEnvironmentVariable(OtlpExporterOptions.EndpointEnvVarName, null); Environment.SetEnvironmentVariable(OtlpExporterOptions.HeadersEnvVarName, null); Environment.SetEnvironmentVariable(OtlpExporterOptions.TimeoutEnvVarName, null); + Environment.SetEnvironmentVariable(OtlpExporterOptions.ProtocolEnvVarName, null); } } } diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMetricsExporterTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMetricsExporterTests.cs index 1d6c286a993..75b170bf501 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMetricsExporterTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpMetricsExporterTests.cs @@ -18,14 +18,13 @@ using System.Collections.Generic; using System.Diagnostics.Metrics; using System.Linq; -using System.Threading; +using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; using OpenTelemetry.Metrics; using OpenTelemetry.Resources; using OpenTelemetry.Tests; using OpenTelemetry.Trace; using Xunit; -using GrpcCore = Grpc.Core; using OtlpCollector = Opentelemetry.Proto.Collector.Metrics.V1; using OtlpMetrics = Opentelemetry.Proto.Metrics.V1; @@ -33,15 +32,76 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests { public class OtlpMetricsExporterTests { + [Fact] + public void UserHttpFactoryCalled() + { + OtlpExporterOptions options = new OtlpExporterOptions(); + + var defaultFactory = options.HttpClientFactory; + + int invocations = 0; + options.Protocol = OtlpExportProtocol.HttpProtobuf; + options.HttpClientFactory = () => + { + invocations++; + return defaultFactory(); + }; + + using (var exporter = new OtlpMetricExporter(options)) + { + Assert.Equal(1, invocations); + } + + using (var provider = Sdk.CreateMeterProviderBuilder() + .AddOtlpExporter(o => + { + o.Protocol = OtlpExportProtocol.HttpProtobuf; + o.HttpClientFactory = options.HttpClientFactory; + }) + .Build()) + { + Assert.Equal(2, invocations); + } + + options.HttpClientFactory = null; + Assert.Throws(() => + { + using var exporter = new OtlpMetricExporter(options); + }); + + options.HttpClientFactory = () => null; + Assert.Throws(() => + { + using var exporter = new OtlpMetricExporter(options); + }); + } + + [Fact] + public void ServiceProviderHttpClientFactoryInvoked() + { + IServiceCollection services = new ServiceCollection(); + + services.AddHttpClient(); + + int invocations = 0; + + services.AddHttpClient("OtlpMetricExporter", configureClient: (client) => invocations++); + + services.AddOpenTelemetryMetrics(builder => builder.AddOtlpExporter( + o => o.Protocol = OtlpExportProtocol.HttpProtobuf)); + + using var serviceProvider = services.BuildServiceProvider(); + + var meterProvider = serviceProvider.GetRequiredService(); + + Assert.Equal(1, invocations); + } + [Theory] [InlineData(true)] [InlineData(false)] public void ToOtlpResourceMetricsTest(bool includeServiceNameInResource) { - using var exporter = new OtlpMetricsExporter( - new OtlpExporterOptions(), - new NoopMetricsServiceClient()); - var resourceBuilder = ResourceBuilder.CreateEmpty(); if (includeServiceNameInResource) { @@ -53,96 +113,332 @@ public void ToOtlpResourceMetricsTest(bool includeServiceNameInResource) }); } - var tags = new KeyValuePair[] + var metrics = new List(); + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{includeServiceNameInResource}", "0.0.1"); + using var provider = Sdk.CreateMeterProviderBuilder() + .SetResourceBuilder(resourceBuilder) + .AddMeter(meter.Name) + .AddInMemoryExporter(metrics) + .Build(); + + var counter = meter.CreateCounter("counter"); + counter.Add(100); + + provider.ForceFlush(); + + var batch = new Batch(metrics.ToArray(), metrics.Count); + + var request = new OtlpCollector.ExportMetricsServiceRequest(); + request.AddMetrics(resourceBuilder.Build().ToOtlpResource(), batch); + + Assert.Single(request.ResourceMetrics); + var resourceMetric = request.ResourceMetrics.First(); + var oltpResource = resourceMetric.Resource; + + if (includeServiceNameInResource) { - new KeyValuePair("key1", "value1"), - new KeyValuePair("key2", "value2"), - }; + Assert.Contains(oltpResource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceName && kvp.Value.StringValue == "service-name"); + Assert.Contains(oltpResource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceNamespace && kvp.Value.StringValue == "ns1"); + } + else + { + Assert.Contains(oltpResource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceName && kvp.Value.ToString().Contains("unknown_service:")); + } + + Assert.Single(resourceMetric.InstrumentationLibraryMetrics); + var instrumentationLibraryMetrics = resourceMetric.InstrumentationLibraryMetrics.First(); + Assert.Equal(string.Empty, instrumentationLibraryMetrics.SchemaUrl); + Assert.Equal(meter.Name, instrumentationLibraryMetrics.InstrumentationLibrary.Name); + Assert.Equal("0.0.1", instrumentationLibraryMetrics.InstrumentationLibrary.Version); + } - var processor = new PullMetricProcessor(new TestExporter(RunTest), true); + [Theory] + [InlineData("test_gauge", null, null, 123, null)] + [InlineData("test_gauge", null, null, null, 123.45)] + [InlineData("test_gauge", "description", "unit", 123, null)] + public void TestGaugeToOtlpMetric(string name, string description, string unit, long? longValue, double? doubleValue) + { + var metrics = new List(); + using var meter = new Meter(Utils.GetCurrentMethodName()); using var provider = Sdk.CreateMeterProviderBuilder() - .SetResourceBuilder(resourceBuilder) - .AddSource("TestMeter") - .AddMetricProcessor(processor) + .AddMeter(meter.Name) + .AddInMemoryExporter(metrics) .Build(); - exporter.ParentProvider = provider; + if (longValue.HasValue) + { + meter.CreateObservableGauge(name, () => longValue.Value, unit, description); + } + else + { + meter.CreateObservableGauge(name, () => doubleValue.Value, unit, description); + } + + provider.ForceFlush(); - using var meter = new Meter("TestMeter", "0.0.1"); + var batch = new Batch(metrics.ToArray(), metrics.Count); - var counter = meter.CreateCounter("counter"); + var request = new OtlpCollector.ExportMetricsServiceRequest(); + request.AddMetrics(ResourceBuilder.CreateEmpty().Build().ToOtlpResource(), batch); + + var resourceMetric = request.ResourceMetrics.Single(); + var instrumentationLibraryMetrics = resourceMetric.InstrumentationLibraryMetrics.Single(); + var actual = instrumentationLibraryMetrics.Metrics.Single(); - counter.Add(100, tags); + Assert.Equal(name, actual.Name); + Assert.Equal(description ?? string.Empty, actual.Description); + Assert.Equal(unit ?? string.Empty, actual.Unit); - var testCompleted = false; + Assert.Equal(OtlpMetrics.Metric.DataOneofCase.Gauge, actual.DataCase); - // Invokes the TestExporter which will invoke RunTest - processor.PullRequest(); + Assert.NotNull(actual.Gauge); + Assert.Null(actual.Sum); + Assert.Null(actual.Histogram); + Assert.Null(actual.ExponentialHistogram); + Assert.Null(actual.Summary); - Assert.True(testCompleted); + Assert.Single(actual.Gauge.DataPoints); + var dataPoint = actual.Gauge.DataPoints.First(); + Assert.True(dataPoint.StartTimeUnixNano > 0); + Assert.True(dataPoint.TimeUnixNano > 0); - void RunTest(Batch metricItem) + if (longValue.HasValue) { - var request = new OtlpCollector.ExportMetricsServiceRequest(); - request.AddBatch(exporter.ProcessResource, metricItem); + Assert.Equal(OtlpMetrics.NumberDataPoint.ValueOneofCase.AsInt, dataPoint.ValueCase); + Assert.Equal(longValue, dataPoint.AsInt); + } + else + { + Assert.Equal(OtlpMetrics.NumberDataPoint.ValueOneofCase.AsDouble, dataPoint.ValueCase); + Assert.Equal(doubleValue, dataPoint.AsDouble); + } - Assert.Single(request.ResourceMetrics); - var resourceMetric = request.ResourceMetrics.First(); - var oltpResource = resourceMetric.Resource; + Assert.Empty(dataPoint.Attributes); - if (includeServiceNameInResource) - { - Assert.Contains(oltpResource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceName && kvp.Value.StringValue == "service-name"); - Assert.Contains(oltpResource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceNamespace && kvp.Value.StringValue == "ns1"); - } - else - { - Assert.Contains(oltpResource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceName && kvp.Value.ToString().Contains("unknown_service:")); - } + Assert.Empty(dataPoint.Exemplars); + +#pragma warning disable CS0612 // Type or member is obsolete + Assert.Null(actual.IntGauge); + Assert.Null(actual.IntSum); + Assert.Null(actual.IntHistogram); + Assert.Empty(dataPoint.Labels); +#pragma warning restore CS0612 // Type or member is obsolete + } + + [Theory] + [InlineData("test_counter", null, null, 123, null, AggregationTemporality.Cumulative, true)] + [InlineData("test_counter", null, null, null, 123.45, AggregationTemporality.Cumulative, true)] + [InlineData("test_counter", null, null, 123, null, AggregationTemporality.Delta, true)] + [InlineData("test_counter", "description", "unit", 123, null, AggregationTemporality.Cumulative, true)] + [InlineData("test_counter", null, null, 123, null, AggregationTemporality.Delta, true, "key1", "value1", "key2", 123)] + public void TestCounterToOltpMetric(string name, string description, string unit, long? longValue, double? doubleValue, AggregationTemporality aggregationTemporality, bool isMonotonic, params object[] keysValues) + { + var metrics = new List(); + + using var meter = new Meter(Utils.GetCurrentMethodName()); + using var provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddReader( + new BaseExportingMetricReader(new InMemoryExporter(metrics)) + { + Temporality = aggregationTemporality, + }) + .Build(); + + var attributes = ToAttributes(keysValues).ToArray(); + if (longValue.HasValue) + { + var counter = meter.CreateCounter(name, unit, description); + counter.Add(longValue.Value, attributes); + } + else + { + var counter = meter.CreateCounter(name, unit, description); + counter.Add(doubleValue.Value, attributes); + } - Assert.Single(resourceMetric.InstrumentationLibraryMetrics); - var instrumentationLibraryMetrics = resourceMetric.InstrumentationLibraryMetrics.First(); - Assert.Equal(string.Empty, instrumentationLibraryMetrics.SchemaUrl); - Assert.Equal("TestMeter", instrumentationLibraryMetrics.InstrumentationLibrary.Name); - Assert.Equal("0.0.1", instrumentationLibraryMetrics.InstrumentationLibrary.Version); + provider.ForceFlush(); - Assert.Single(instrumentationLibraryMetrics.Metrics); + var batch = new Batch(metrics.ToArray(), metrics.Count); - foreach (var metric in instrumentationLibraryMetrics.Metrics) - { - Assert.Equal(string.Empty, metric.Description); - Assert.Equal(string.Empty, metric.Unit); - Assert.Equal("counter", metric.Name); + var request = new OtlpCollector.ExportMetricsServiceRequest(); + request.AddMetrics(ResourceBuilder.CreateEmpty().Build().ToOtlpResource(), batch); + + var resourceMetric = request.ResourceMetrics.Single(); + var instrumentationLibraryMetrics = resourceMetric.InstrumentationLibraryMetrics.Single(); + var actual = instrumentationLibraryMetrics.Metrics.Single(); + + Assert.Equal(name, actual.Name); + Assert.Equal(description ?? string.Empty, actual.Description); + Assert.Equal(unit ?? string.Empty, actual.Unit); - Assert.Equal(OtlpMetrics.Metric.DataOneofCase.Sum, metric.DataCase); - Assert.True(metric.Sum.IsMonotonic); - Assert.Equal(OtlpMetrics.AggregationTemporality.Delta, metric.Sum.AggregationTemporality); + Assert.Equal(OtlpMetrics.Metric.DataOneofCase.Sum, actual.DataCase); - Assert.Single(metric.Sum.DataPoints); - var dataPoint = metric.Sum.DataPoints.First(); - Assert.True(dataPoint.StartTimeUnixNano > 0); - Assert.True(dataPoint.TimeUnixNano > 0); - Assert.Equal(OtlpMetrics.NumberDataPoint.ValueOneofCase.AsInt, dataPoint.ValueCase); - Assert.Equal(100, dataPoint.AsInt); + Assert.Null(actual.Gauge); + Assert.NotNull(actual.Sum); + Assert.Null(actual.Histogram); + Assert.Null(actual.ExponentialHistogram); + Assert.Null(actual.Summary); + + Assert.Equal(isMonotonic, actual.Sum.IsMonotonic); + + var otlpAggregationTemporality = aggregationTemporality == AggregationTemporality.Cumulative + ? OtlpMetrics.AggregationTemporality.Cumulative + : OtlpMetrics.AggregationTemporality.Delta; + Assert.Equal(otlpAggregationTemporality, actual.Sum.AggregationTemporality); + + Assert.Single(actual.Sum.DataPoints); + var dataPoint = actual.Sum.DataPoints.First(); + Assert.True(dataPoint.StartTimeUnixNano > 0); + Assert.True(dataPoint.TimeUnixNano > 0); + + if (longValue.HasValue) + { + Assert.Equal(OtlpMetrics.NumberDataPoint.ValueOneofCase.AsInt, dataPoint.ValueCase); + Assert.Equal(longValue, dataPoint.AsInt); + } + else + { + Assert.Equal(OtlpMetrics.NumberDataPoint.ValueOneofCase.AsDouble, dataPoint.ValueCase); + Assert.Equal(doubleValue, dataPoint.AsDouble); + } + + if (attributes.Length > 0) + { + OtlpTestHelpers.AssertOtlpAttributes(attributes, dataPoint.Attributes); + } + else + { + Assert.Empty(dataPoint.Attributes); + } + + Assert.Empty(dataPoint.Exemplars); #pragma warning disable CS0612 // Type or member is obsolete - Assert.Empty(dataPoint.Labels); + Assert.Null(actual.IntGauge); + Assert.Null(actual.IntSum); + Assert.Null(actual.IntHistogram); + Assert.Empty(dataPoint.Labels); #pragma warning restore CS0612 // Type or member is obsolete - OtlpTestHelpers.AssertOtlpAttributes(tags.ToList(), dataPoint.Attributes); + } + + [Theory] + [InlineData("test_histogram", null, null, 123, null, AggregationTemporality.Cumulative)] + [InlineData("test_histogram", null, null, null, 123.45, AggregationTemporality.Cumulative)] + [InlineData("test_histogram", null, null, 123, null, AggregationTemporality.Delta)] + [InlineData("test_histogram", "description", "unit", 123, null, AggregationTemporality.Cumulative)] + [InlineData("test_histogram", null, null, 123, null, AggregationTemporality.Delta, "key1", "value1", "key2", 123)] + public void TestHistogramToOltpMetric(string name, string description, string unit, long? longValue, double? doubleValue, AggregationTemporality aggregationTemporality, params object[] keysValues) + { + var metrics = new List(); + + var metricReader = new BaseExportingMetricReader(new InMemoryExporter(metrics)); + metricReader.Temporality = aggregationTemporality; - Assert.Empty(dataPoint.Exemplars); + using var meter = new Meter(Utils.GetCurrentMethodName()); + using var provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddReader(metricReader) + .Build(); + + var attributes = ToAttributes(keysValues).ToArray(); + if (longValue.HasValue) + { + var histogram = meter.CreateHistogram(name, unit, description); + histogram.Record(longValue.Value, attributes); + } + else + { + var histogram = meter.CreateHistogram(name, unit, description); + histogram.Record(doubleValue.Value, attributes); + } + + provider.ForceFlush(); + + var batch = new Batch(metrics.ToArray(), metrics.Count); + + var request = new OtlpCollector.ExportMetricsServiceRequest(); + request.AddMetrics(ResourceBuilder.CreateEmpty().Build().ToOtlpResource(), batch); + + var resourceMetric = request.ResourceMetrics.Single(); + var instrumentationLibraryMetrics = resourceMetric.InstrumentationLibraryMetrics.Single(); + var actual = instrumentationLibraryMetrics.Metrics.Single(); + + Assert.Equal(name, actual.Name); + Assert.Equal(description ?? string.Empty, actual.Description); + Assert.Equal(unit ?? string.Empty, actual.Unit); + + Assert.Equal(OtlpMetrics.Metric.DataOneofCase.Histogram, actual.DataCase); + + Assert.Null(actual.Gauge); + Assert.Null(actual.Sum); + Assert.NotNull(actual.Histogram); + Assert.Null(actual.ExponentialHistogram); + Assert.Null(actual.Summary); + + var otlpAggregationTemporality = aggregationTemporality == AggregationTemporality.Cumulative + ? OtlpMetrics.AggregationTemporality.Cumulative + : OtlpMetrics.AggregationTemporality.Delta; + Assert.Equal(otlpAggregationTemporality, actual.Histogram.AggregationTemporality); + + Assert.Single(actual.Histogram.DataPoints); + var dataPoint = actual.Histogram.DataPoints.First(); + Assert.True(dataPoint.StartTimeUnixNano > 0); + Assert.True(dataPoint.TimeUnixNano > 0); + + Assert.Equal(1UL, dataPoint.Count); + + if (longValue.HasValue) + { + Assert.Equal((double)longValue, dataPoint.Sum); + } + else + { + Assert.Equal(doubleValue, dataPoint.Sum); + } + + int bucketIndex; + for (bucketIndex = 0; bucketIndex < dataPoint.ExplicitBounds.Count; ++bucketIndex) + { + if (dataPoint.Sum <= dataPoint.ExplicitBounds[bucketIndex]) + { + break; } - testCompleted = true; + Assert.Equal(0UL, dataPoint.BucketCounts[bucketIndex]); } + + Assert.Equal(1UL, dataPoint.BucketCounts[bucketIndex]); + + if (attributes.Length > 0) + { + OtlpTestHelpers.AssertOtlpAttributes(attributes, dataPoint.Attributes); + } + else + { + Assert.Empty(dataPoint.Attributes); + } + + Assert.Empty(dataPoint.Exemplars); + +#pragma warning disable CS0612 // Type or member is obsolete + Assert.Null(actual.IntGauge); + Assert.Null(actual.IntSum); + Assert.Null(actual.IntHistogram); + Assert.Empty(dataPoint.Labels); +#pragma warning restore CS0612 // Type or member is obsolete } - private class NoopMetricsServiceClient : OtlpCollector.MetricsService.IMetricsServiceClient + private static IEnumerable> ToAttributes(object[] keysValues) { - public OtlpCollector.ExportMetricsServiceResponse Export(OtlpCollector.ExportMetricsServiceRequest request, GrpcCore.Metadata headers = null, DateTime? deadline = null, CancellationToken cancellationToken = default) + var keys = keysValues?.Where((_, index) => index % 2 == 0).ToArray(); + var values = keysValues?.Where((_, index) => index % 2 != 0).ToArray(); + + for (var i = 0; keys != null && i < keys.Length; ++i) { - return null; + yield return new KeyValuePair(keys[i].ToString(), values[i]); } } } diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTraceExporterTests.cs b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTraceExporterTests.cs index 62e0d50f978..64070b84772 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTraceExporterTests.cs +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpTraceExporterTests.cs @@ -18,15 +18,15 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using System.Reflection; using System.Threading; -using Grpc.Core; +using Microsoft.Extensions.DependencyInjection; +using Moq; using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation; +using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.ExportClient; using OpenTelemetry.Resources; using OpenTelemetry.Tests; using OpenTelemetry.Trace; using Xunit; -using Xunit.Sdk; using GrpcCore = Grpc.Core; using OtlpCollector = Opentelemetry.Proto.Collector.Trace.V1; using OtlpCommon = Opentelemetry.Proto.Common.V1; @@ -35,7 +35,7 @@ namespace OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests { - public class OtlpTraceExporterTests + public class OtlpTraceExporterTests : Http2UnencryptedSupportTests { static OtlpTraceExporterTests() { @@ -58,6 +58,71 @@ public void OtlpExporter_BadArgs() Assert.Throws(() => builder.AddOtlpExporter()); } + [Fact] + public void UserHttpFactoryCalled() + { + OtlpExporterOptions options = new OtlpExporterOptions(); + + var defaultFactory = options.HttpClientFactory; + + int invocations = 0; + options.Protocol = OtlpExportProtocol.HttpProtobuf; + options.HttpClientFactory = () => + { + invocations++; + return defaultFactory(); + }; + + using (var exporter = new OtlpTraceExporter(options)) + { + Assert.Equal(1, invocations); + } + + using (var provider = Sdk.CreateTracerProviderBuilder() + .AddOtlpExporter(o => + { + o.Protocol = OtlpExportProtocol.HttpProtobuf; + o.HttpClientFactory = options.HttpClientFactory; + }) + .Build()) + { + Assert.Equal(2, invocations); + } + + options.HttpClientFactory = null; + Assert.Throws(() => + { + using var exporter = new OtlpTraceExporter(options); + }); + + options.HttpClientFactory = () => null; + Assert.Throws(() => + { + using var exporter = new OtlpTraceExporter(options); + }); + } + + [Fact] + public void ServiceProviderHttpClientFactoryInvoked() + { + IServiceCollection services = new ServiceCollection(); + + services.AddHttpClient(); + + int invocations = 0; + + services.AddHttpClient("OtlpTraceExporter", configureClient: (client) => invocations++); + + services.AddOpenTelemetryTracing(builder => builder.AddOtlpExporter( + o => o.Protocol = OtlpExportProtocol.HttpProtobuf)); + + using var serviceProvider = services.BuildServiceProvider(); + + var tracerProvider = serviceProvider.GetRequiredService(); + + Assert.Equal(1, invocations); + } + [Theory] [InlineData(true)] [InlineData(false)] @@ -71,10 +136,6 @@ public void ToOtlpResourceSpansTest(bool includeServiceNameInResource) new ActivitySource("odd", "1.3.5"), }; - using var exporter = new OtlpTraceExporter( - new OtlpExporterOptions(), - new NoopTraceServiceClient()); - var resourceBuilder = ResourceBuilder.CreateEmpty(); if (includeServiceNameInResource) { @@ -93,9 +154,8 @@ public void ToOtlpResourceSpansTest(bool includeServiceNameInResource) using var openTelemetrySdk = builder.Build(); - exporter.ParentProvider = openTelemetrySdk; - - var processor = new BatchActivityExportProcessor(new TestExporter(RunTest)); + var exportedItems = new List(); + var processor = new BatchActivityExportProcessor(new InMemoryExporter(exportedItems)); const int numOfSpans = 10; bool isEven; for (var i = 0; i < numOfSpans; i++) @@ -111,22 +171,25 @@ public void ToOtlpResourceSpansTest(bool includeServiceNameInResource) processor.Shutdown(); + var batch = new Batch(exportedItems.ToArray(), exportedItems.Count); + RunTest(batch); + void RunTest(Batch batch) { var request = new OtlpCollector.ExportTraceServiceRequest(); - request.AddBatch(exporter.ProcessResource, batch); + request.AddBatch(resourceBuilder.Build().ToOtlpResource(), batch); Assert.Single(request.ResourceSpans); var oltpResource = request.ResourceSpans.First().Resource; if (includeServiceNameInResource) { - Assert.Contains(oltpResource.Attributes, (kvp) => kvp.Key == Resources.ResourceSemanticConventions.AttributeServiceName && kvp.Value.StringValue == "service-name"); - Assert.Contains(oltpResource.Attributes, (kvp) => kvp.Key == Resources.ResourceSemanticConventions.AttributeServiceNamespace && kvp.Value.StringValue == "ns1"); + Assert.Contains(oltpResource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceName && kvp.Value.StringValue == "service-name"); + Assert.Contains(oltpResource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceNamespace && kvp.Value.StringValue == "ns1"); } else { - Assert.Contains(oltpResource.Attributes, (kvp) => kvp.Key == Resources.ResourceSemanticConventions.AttributeServiceName && kvp.Value.ToString().Contains("unknown_service:")); + Assert.Contains(oltpResource.Attributes, (kvp) => kvp.Key == ResourceSemanticConventions.AttributeServiceName && kvp.Value.ToString().Contains("unknown_service:")); } foreach (var instrumentationLibrarySpans in request.ResourceSpans.First().InstrumentationLibrarySpans) @@ -309,6 +372,14 @@ public void ToOtlpSpanPeerServiceTest() [Fact] public void UseOpenTelemetryProtocolActivityExporterWithCustomActivityProcessor() { + if (Environment.Version.Major == 3) + { + // Adding the OtlpExporter creates a GrpcChannel. + // This switch must be set before creating a GrpcChannel when calling an insecure HTTP/2 endpoint. + // See: https://docs.microsoft.com/aspnet/core/grpc/troubleshoot#call-insecure-grpc-services-with-net-core-client + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + } + const string ActivitySourceName = "otlp.test"; TestActivityProcessor testActivityProcessor = new TestActivityProcessor(); @@ -327,7 +398,7 @@ public void UseOpenTelemetryProtocolActivityExporterWithCustomActivityProcessor( endCalled = true; }; - var openTelemetrySdk = Sdk.CreateTracerProviderBuilder() + var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource(ActivitySourceName) .AddProcessor(testActivityProcessor) .AddOtlpExporter() @@ -341,6 +412,18 @@ public void UseOpenTelemetryProtocolActivityExporterWithCustomActivityProcessor( Assert.True(endCalled); } + [Fact] + public void Shutdown_ClientShutdownIsCalled() + { + var exportClientMock = new Mock>(); + + var exporter = new OtlpTraceExporter(new OtlpExporterOptions(), exportClientMock.Object); + + var result = exporter.Shutdown(); + + exportClientMock.Verify(m => m.Shutdown(It.IsAny()), Times.Once()); + } + private class NoopTraceServiceClient : OtlpCollector.TraceService.ITraceServiceClient { public OtlpCollector.ExportTraceServiceResponse Export(OtlpCollector.ExportTraceServiceRequest request, GrpcCore.Metadata headers = null, DateTime? deadline = null, CancellationToken cancellationToken = default) diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/config.yaml b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/config.yaml index 544292318ff..000ccbdf4c7 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/config.yaml +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/config.yaml @@ -8,6 +8,9 @@ receivers: otlp: protocols: grpc: + endpoint: 0.0.0.0:4317 + http: + endpoint: 0.0.0.0:4318 exporters: logging: diff --git a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/docker-compose.yml b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/docker-compose.yml index 094695190cb..b5830f3b25a 100644 --- a/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/docker-compose.yml +++ b/test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/docker-compose.yml @@ -12,6 +12,7 @@ services: command: --config=/cfg/config.yaml ports: - "4317:4317" + - "4318:4318" tests: build: @@ -19,6 +20,6 @@ services: dockerfile: ./test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/Dockerfile command: --TestCaseFilter:CategoryName=CollectorIntegrationTests environment: - - OTEL_EXPORTER_OTLP_ENDPOINT=otel-collector:4317 + - OTEL_COLLECTOR_HOSTNAME=otel-collector depends_on: - otel-collector diff --git a/test/OpenTelemetry.Exporter.Prometheus.Tests/OpenTelemetry.Exporter.Prometheus.Tests.csproj b/test/OpenTelemetry.Exporter.Prometheus.Tests/OpenTelemetry.Exporter.Prometheus.Tests.csproj new file mode 100644 index 00000000000..8a62f0d0e3b --- /dev/null +++ b/test/OpenTelemetry.Exporter.Prometheus.Tests/OpenTelemetry.Exporter.Prometheus.Tests.csproj @@ -0,0 +1,44 @@ + + + Unit test project for Prometheus Exporter for OpenTelemetry + netcoreapp3.1;net5.0 + $(TargetFrameworks);net461 + + false + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/OpenTelemetry.Exporter.Prometheus.Tests/PrometheusCollectionManagerTests.cs b/test/OpenTelemetry.Exporter.Prometheus.Tests/PrometheusCollectionManagerTests.cs new file mode 100644 index 00000000000..9c650db27cb --- /dev/null +++ b/test/OpenTelemetry.Exporter.Prometheus.Tests/PrometheusCollectionManagerTests.cs @@ -0,0 +1,161 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Diagnostics.Metrics; +#if NET461 +using System.Linq; +#endif +using System.Threading; +using System.Threading.Tasks; +using OpenTelemetry.Metrics; +using OpenTelemetry.Tests; +using Xunit; + +namespace OpenTelemetry.Exporter.Prometheus.Tests +{ + public sealed class PrometheusCollectionManagerTests + { + [Fact] + public async Task EnterExitCollectTest() + { + using var meter = new Meter(Utils.GetCurrentMethodName()); + + using (var provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddPrometheusExporter() + .Build()) + { + if (!provider.TryFindExporter(out PrometheusExporter exporter)) + { + throw new InvalidOperationException("PrometheusExporter could not be found on MeterProvider."); + } + + int runningCollectCount = 0; + var collectFunc = exporter.Collect; + exporter.Collect = (timeout) => + { + bool result = collectFunc(timeout); + runningCollectCount++; + Thread.Sleep(5000); + return result; + }; + + var counter = meter.CreateCounter("counter_int", description: "Prometheus help text goes here \n escaping."); + counter.Add(100); + + Task[] collectTasks = new Task[10]; + for (int i = 0; i < collectTasks.Length; i++) + { + collectTasks[i] = Task.Run(async () => + { + var response = await exporter.CollectionManager.EnterCollect().ConfigureAwait(false); + try + { + return new Response + { + CollectionResponse = response, + ViewPayload = response.View.ToArray(), + }; + } + finally + { + exporter.CollectionManager.ExitCollect(); + } + }); + } + + await Task.WhenAll(collectTasks).ConfigureAwait(false); + + Assert.Equal(1, runningCollectCount); + + var firstResponse = collectTasks[0].Result; + + Assert.False(firstResponse.CollectionResponse.FromCache); + + for (int i = 1; i < collectTasks.Length; i++) + { + Assert.Equal(firstResponse.ViewPayload, collectTasks[i].Result.ViewPayload); + Assert.Equal(firstResponse.CollectionResponse.GeneratedAtUtc, collectTasks[i].Result.CollectionResponse.GeneratedAtUtc); + } + + counter.Add(100); + + // This should use the cache and ignore the second counter update. + var task = exporter.CollectionManager.EnterCollect(); + Assert.True(task.IsCompleted); + var response = await task.ConfigureAwait(false); + try + { + Assert.Equal(1, runningCollectCount); + Assert.True(response.FromCache); + Assert.Equal(firstResponse.CollectionResponse.GeneratedAtUtc, response.GeneratedAtUtc); + } + finally + { + exporter.CollectionManager.ExitCollect(); + } + + Thread.Sleep(exporter.Options.ScrapeResponseCacheDurationMilliseconds); + + counter.Add(100); + + for (int i = 0; i < collectTasks.Length; i++) + { + collectTasks[i] = Task.Run(async () => + { + var response = await exporter.CollectionManager.EnterCollect().ConfigureAwait(false); + try + { + return new Response + { + CollectionResponse = response, + ViewPayload = response.View.ToArray(), + }; + } + finally + { + exporter.CollectionManager.ExitCollect(); + } + }); + } + + await Task.WhenAll(collectTasks).ConfigureAwait(false); + + Assert.Equal(2, runningCollectCount); + Assert.NotEqual(firstResponse.ViewPayload, collectTasks[0].Result.ViewPayload); + Assert.NotEqual(firstResponse.CollectionResponse.GeneratedAtUtc, collectTasks[0].Result.CollectionResponse.GeneratedAtUtc); + + firstResponse = collectTasks[0].Result; + + Assert.False(firstResponse.CollectionResponse.FromCache); + + for (int i = 1; i < collectTasks.Length; i++) + { + Assert.Equal(firstResponse.ViewPayload, collectTasks[i].Result.ViewPayload); + Assert.Equal(firstResponse.CollectionResponse.GeneratedAtUtc, collectTasks[i].Result.CollectionResponse.GeneratedAtUtc); + } + } + } + + private class Response + { + public PrometheusCollectionManager.CollectionResponse CollectionResponse; + + public byte[] ViewPayload; + } + } +} diff --git a/test/OpenTelemetry.Exporter.Prometheus.Tests/PrometheusExporterHttpServerTests.cs b/test/OpenTelemetry.Exporter.Prometheus.Tests/PrometheusExporterHttpServerTests.cs new file mode 100644 index 00000000000..aec281387c3 --- /dev/null +++ b/test/OpenTelemetry.Exporter.Prometheus.Tests/PrometheusExporterHttpServerTests.cs @@ -0,0 +1,154 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using System.Diagnostics.Metrics; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using OpenTelemetry.Metrics; +using OpenTelemetry.Tests; +using Xunit; + +namespace OpenTelemetry.Exporter.Prometheus.Tests +{ + public class PrometheusExporterHttpServerTests + { + [Fact] + public async Task PrometheusExporterHttpServerIntegration() + { + Random random = new Random(); + int port = 0; + int retryCount = 5; + MeterProvider provider; + string address = null; + + using var meter = new Meter(Utils.GetCurrentMethodName()); + + while (true) + { + try + { + port = random.Next(2000, 5000); + provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddPrometheusExporter(o => + { +#if NET461 + bool expectedDefaultState = true; +#else + bool expectedDefaultState = false; +#endif + if (o.StartHttpListener != expectedDefaultState) + { + throw new InvalidOperationException("StartHttpListener value is unexpected."); + } + + if (!o.StartHttpListener) + { + o.StartHttpListener = true; + } + + address = $"http://localhost:{port}/"; + o.HttpListenerPrefixes = new string[] { address }; + }) + .Build(); + break; + } + catch (Exception ex) + { + if (ex.Message != PrometheusExporter.HttpListenerStartFailureExceptionMessage) + { + throw; + } + + if (retryCount-- <= 0) + { + throw new InvalidOperationException("HttpListener could not be started."); + } + } + } + + var tags = new KeyValuePair[] + { + new KeyValuePair("key1", "value1"), + new KeyValuePair("key2", "value2"), + }; + + var counter = meter.CreateCounter("counter_double"); + counter.Add(100.18D, tags); + counter.Add(0.99D, tags); + + using HttpClient client = new HttpClient(); + + using var response = await client.GetAsync($"{address}metrics").ConfigureAwait(false); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.True(response.Content.Headers.Contains("Last-Modified")); + Assert.Equal("text/plain; charset=utf-8; version=0.0.4", response.Content.Headers.ContentType.ToString()); + + Assert.Matches( + "^# TYPE counter_double counter\ncounter_double{key1='value1',key2='value2'} 101.17 \\d+\n$".Replace('\'', '"'), + await response.Content.ReadAsStringAsync().ConfigureAwait(false)); + } + + [Theory] + [InlineData("http://example.com")] + [InlineData("https://example.com")] + [InlineData("http://127.0.0.1")] + [InlineData("http://example.com", "https://example.com", "http://127.0.0.1")] + public void ServerEndpointSanityCheckPositiveTest(params string[] uris) + { + using MeterProvider meterProvider = Sdk.CreateMeterProviderBuilder() + .AddPrometheusExporter(opt => + { + opt.HttpListenerPrefixes = uris; + }) + .Build(); + } + + [Theory] + [InlineData("")] + [InlineData(null)] + [InlineData("ftp://example.com")] + [InlineData("http://example.com", "https://example.com", "ftp://example.com")] + public void ServerEndpointSanityCheckNegativeTest(params string[] uris) + { + try + { + using MeterProvider meterProvider = Sdk.CreateMeterProviderBuilder() + .AddPrometheusExporter(opt => + { + opt.HttpListenerPrefixes = uris; + }) + .Build(); + } + catch (Exception ex) + { + if (ex is not ArgumentNullException) + { + Assert.Equal("System.ArgumentException", ex.GetType().ToString()); +#if NET461 + Assert.Equal("Prometheus server path should be a valid URI with http/https scheme.\r\nParameter name: httpListenerPrefixes", ex.Message); +#else + Assert.Equal("Prometheus server path should be a valid URI with http/https scheme. (Parameter 'httpListenerPrefixes')", ex.Message); +#endif + } + } + } + } +} diff --git a/test/OpenTelemetry.Exporter.Prometheus.Tests/PrometheusExporterMiddlewareTests.cs b/test/OpenTelemetry.Exporter.Prometheus.Tests/PrometheusExporterMiddlewareTests.cs new file mode 100644 index 00000000000..b4fdc9b0edf --- /dev/null +++ b/test/OpenTelemetry.Exporter.Prometheus.Tests/PrometheusExporterMiddlewareTests.cs @@ -0,0 +1,114 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +#if !NET461 +using System; +using System.Collections.Generic; +using System.Diagnostics.Metrics; +using System.Net; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using OpenTelemetry.Metrics; +using OpenTelemetry.Tests; +using Xunit; + +namespace OpenTelemetry.Exporter.Prometheus.Tests +{ + public sealed class PrometheusExporterMiddlewareTests + { + private static readonly string MeterName = Utils.GetCurrentMethodName(); + + [Fact] + public async Task PrometheusExporterMiddlewareIntegration() + { + var host = await new HostBuilder() + .ConfigureWebHost(webBuilder => webBuilder + .UseTestServer() + .UseStartup()) + .StartAsync(); + + var tags = new KeyValuePair[] + { + new KeyValuePair("key1", "value1"), + new KeyValuePair("key2", "value2"), + }; + + using var meter = new Meter(MeterName); + + var beginTimestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + + var counter = meter.CreateCounter("counter_double"); + counter.Add(100.18D, tags); + counter.Add(0.99D, tags); + + using var response = await host.GetTestClient().GetAsync("/metrics").ConfigureAwait(false); + + var endTimestamp = DateTimeOffset.Now.ToUnixTimeMilliseconds(); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.True(response.Content.Headers.Contains("Last-Modified")); + Assert.Equal("text/plain; charset=utf-8; version=0.0.4", response.Content.Headers.ContentType.ToString()); + + string content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + + string[] lines = content.Split('\n'); + + Assert.Equal( + $"# TYPE counter_double counter", + lines[0]); + + Assert.Contains( + $"counter_double{{key1=\"value1\",key2=\"value2\"}} 101.17", + lines[1]); + + var index = content.LastIndexOf(' '); + + Assert.Equal('\n', content[content.Length - 1]); + + var timestamp = long.Parse(content.Substring(index, content.Length - index - 1)); + + Assert.True(beginTimestamp <= timestamp && timestamp <= endTimestamp); + + await host.StopAsync().ConfigureAwait(false); + } + + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + services.AddOpenTelemetryMetrics(builder => builder + .AddMeter(MeterName) + .AddPrometheusExporter(o => + { + if (o.StartHttpListener) + { + throw new InvalidOperationException("StartHttpListener should be false on .NET Core 3.1+."); + } + })); + } + + public void Configure(IApplicationBuilder app) + { + app.UseOpenTelemetryPrometheusScrapingEndpoint(); + } + } + } +} +#endif diff --git a/test/OpenTelemetry.Exporter.Prometheus.Tests/PrometheusSerializerTests.cs b/test/OpenTelemetry.Exporter.Prometheus.Tests/PrometheusSerializerTests.cs new file mode 100644 index 00000000000..9ce27c9d63b --- /dev/null +++ b/test/OpenTelemetry.Exporter.Prometheus.Tests/PrometheusSerializerTests.cs @@ -0,0 +1,386 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Collections.Generic; +using System.Diagnostics.Metrics; +using System.Text; +using OpenTelemetry.Metrics; +using OpenTelemetry.Tests; +using Xunit; + +namespace OpenTelemetry.Exporter.Prometheus.Tests +{ + public sealed class PrometheusSerializerTests + { + [Fact] + public void GaugeZeroDimension() + { + var buffer = new byte[85000]; + var metrics = new List(); + + using var meter = new Meter(Utils.GetCurrentMethodName()); + using var provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(metrics) + .Build(); + + meter.CreateObservableGauge("test_gauge", () => 123); + + provider.ForceFlush(); + + var cursor = PrometheusSerializer.WriteMetric(buffer, 0, metrics[0]); + Assert.Matches( + ("^" + + "# TYPE test_gauge gauge\n" + + "test_gauge 123 \\d+\n" + + "$").Replace('\'', '"'), + Encoding.UTF8.GetString(buffer, 0, cursor)); + } + + [Fact] + public void GaugeZeroDimensionWithDescription() + { + var buffer = new byte[85000]; + var metrics = new List(); + + using var meter = new Meter(Utils.GetCurrentMethodName()); + using var provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(metrics) + .Build(); + + meter.CreateObservableGauge("test_gauge", () => 123, description: "Hello, world!"); + + provider.ForceFlush(); + + var cursor = PrometheusSerializer.WriteMetric(buffer, 0, metrics[0]); + Assert.Matches( + ("^" + + "# HELP test_gauge Hello, world!\n" + + "# TYPE test_gauge gauge\n" + + "test_gauge 123 \\d+\n" + + "$").Replace('\'', '"'), + Encoding.UTF8.GetString(buffer, 0, cursor)); + } + + [Fact] + public void GaugeZeroDimensionWithUnit() + { + var buffer = new byte[85000]; + var metrics = new List(); + + using var meter = new Meter(Utils.GetCurrentMethodName()); + using var provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(metrics) + .Build(); + + meter.CreateObservableGauge("test_gauge", () => 123, unit: "seconds"); + + provider.ForceFlush(); + + var cursor = PrometheusSerializer.WriteMetric(buffer, 0, metrics[0]); + Assert.Matches( + ("^" + + "# TYPE test_gauge_seconds gauge\n" + + "test_gauge_seconds 123 \\d+\n" + + "$").Replace('\'', '"'), + Encoding.UTF8.GetString(buffer, 0, cursor)); + } + + [Fact] + public void GaugeOneDimension() + { + var buffer = new byte[85000]; + var metrics = new List(); + + using var meter = new Meter(Utils.GetCurrentMethodName()); + using var provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(metrics) + .Build(); + + var counter = meter.CreateCounter("test_counter"); + counter.Add(123, new KeyValuePair("tagKey", "tagValue")); + + provider.ForceFlush(); + + var cursor = PrometheusSerializer.WriteMetric(buffer, 0, metrics[0]); + Assert.Matches( + ("^" + + "# TYPE test_counter counter\n" + + "test_counter{tagKey='tagValue'} 123 \\d+\n" + + "$").Replace('\'', '"'), + Encoding.UTF8.GetString(buffer, 0, cursor)); + } + + [Fact] + public void GaugeDoubleSubnormal() + { + var buffer = new byte[85000]; + var metrics = new List(); + + using var meter = new Meter(Utils.GetCurrentMethodName()); + using var provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(metrics) + .Build(); + + meter.CreateObservableGauge("test_gauge", () => new List> + { + new(double.NegativeInfinity, new("x", "1"), new("y", "2")), + new(double.PositiveInfinity, new("x", "3"), new("y", "4")), + new(double.NaN, new("x", "5"), new("y", "6")), + }); + + provider.ForceFlush(); + + var cursor = PrometheusSerializer.WriteMetric(buffer, 0, metrics[0]); + Assert.Matches( + ("^" + + "# TYPE test_gauge gauge\n" + + "test_gauge{x='1',y='2'} -Inf \\d+\n" + + "test_gauge{x='3',y='4'} \\+Inf \\d+\n" + + "test_gauge{x='5',y='6'} Nan \\d+\n" + + "$").Replace('\'', '"'), + Encoding.UTF8.GetString(buffer, 0, cursor)); + } + + [Fact] + public void SumDoubleInfinites() + { + var buffer = new byte[85000]; + var metrics = new List(); + + using var meter = new Meter(Utils.GetCurrentMethodName()); + using var provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(metrics) + .Build(); + + var counter = meter.CreateCounter("test_counter"); + counter.Add(1.0E308); + counter.Add(1.0E308); + + provider.ForceFlush(); + + var cursor = PrometheusSerializer.WriteMetric(buffer, 0, metrics[0]); + Assert.Matches( + ("^" + + "# TYPE test_counter counter\n" + + "test_counter \\+Inf \\d+\n" + + "$").Replace('\'', '"'), + Encoding.UTF8.GetString(buffer, 0, cursor)); + } + + [Fact] + public void HistogramZeroDimension() + { + var buffer = new byte[85000]; + var metrics = new List(); + + using var meter = new Meter(Utils.GetCurrentMethodName()); + using var provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(metrics) + .Build(); + + var histogram = meter.CreateHistogram("test_histogram"); + histogram.Record(18); + histogram.Record(100); + + provider.ForceFlush(); + + var cursor = PrometheusSerializer.WriteMetric(buffer, 0, metrics[0]); + Assert.Matches( + ("^" + + "# TYPE test_histogram histogram\n" + + "test_histogram_bucket{le='0'} 0 \\d+\n" + + "test_histogram_bucket{le='5'} 0 \\d+\n" + + "test_histogram_bucket{le='10'} 0 \\d+\n" + + "test_histogram_bucket{le='25'} 1 \\d+\n" + + "test_histogram_bucket{le='50'} 1 \\d+\n" + + "test_histogram_bucket{le='75'} 1 \\d+\n" + + "test_histogram_bucket{le='100'} 2 \\d+\n" + + "test_histogram_bucket{le='250'} 2 \\d+\n" + + "test_histogram_bucket{le='500'} 2 \\d+\n" + + "test_histogram_bucket{le='1000'} 2 \\d+\n" + + "test_histogram_bucket{le='\\+Inf'} 2 \\d+\n" + + "test_histogram_sum 118 \\d+\n" + + "test_histogram_count 2 \\d+\n" + + "$").Replace('\'', '"'), + Encoding.UTF8.GetString(buffer, 0, cursor)); + } + + [Fact] + public void HistogramOneDimension() + { + var buffer = new byte[85000]; + var metrics = new List(); + + using var meter = new Meter(Utils.GetCurrentMethodName()); + using var provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(metrics) + .Build(); + + var histogram = meter.CreateHistogram("test_histogram"); + histogram.Record(18, new KeyValuePair("x", "1")); + histogram.Record(100, new KeyValuePair("x", "1")); + + provider.ForceFlush(); + + var cursor = PrometheusSerializer.WriteMetric(buffer, 0, metrics[0]); + Assert.Matches( + ("^" + + "# TYPE test_histogram histogram\n" + + "test_histogram_bucket{x='1',le='0'} 0 \\d+\n" + + "test_histogram_bucket{x='1',le='5'} 0 \\d+\n" + + "test_histogram_bucket{x='1',le='10'} 0 \\d+\n" + + "test_histogram_bucket{x='1',le='25'} 1 \\d+\n" + + "test_histogram_bucket{x='1',le='50'} 1 \\d+\n" + + "test_histogram_bucket{x='1',le='75'} 1 \\d+\n" + + "test_histogram_bucket{x='1',le='100'} 2 \\d+\n" + + "test_histogram_bucket{x='1',le='250'} 2 \\d+\n" + + "test_histogram_bucket{x='1',le='500'} 2 \\d+\n" + + "test_histogram_bucket{x='1',le='1000'} 2 \\d+\n" + + "test_histogram_bucket{x='1',le='\\+Inf'} 2 \\d+\n" + + "test_histogram_sum{x='1'} 118 \\d+\n" + + "test_histogram_count{x='1'} 2 \\d+\n" + + "$").Replace('\'', '"'), + Encoding.UTF8.GetString(buffer, 0, cursor)); + } + + [Fact] + public void HistogramTwoDimensions() + { + var buffer = new byte[85000]; + var metrics = new List(); + + using var meter = new Meter(Utils.GetCurrentMethodName()); + using var provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(metrics) + .Build(); + + var histogram = meter.CreateHistogram("test_histogram"); + histogram.Record(18, new("x", "1"), new("y", "2")); + histogram.Record(100, new("x", "1"), new("y", "2")); + + provider.ForceFlush(); + + var cursor = PrometheusSerializer.WriteMetric(buffer, 0, metrics[0]); + Assert.Matches( + ("^" + + "# TYPE test_histogram histogram\n" + + "test_histogram_bucket{x='1',y='2',le='0'} 0 \\d+\n" + + "test_histogram_bucket{x='1',y='2',le='5'} 0 \\d+\n" + + "test_histogram_bucket{x='1',y='2',le='10'} 0 \\d+\n" + + "test_histogram_bucket{x='1',y='2',le='25'} 1 \\d+\n" + + "test_histogram_bucket{x='1',y='2',le='50'} 1 \\d+\n" + + "test_histogram_bucket{x='1',y='2',le='75'} 1 \\d+\n" + + "test_histogram_bucket{x='1',y='2',le='100'} 2 \\d+\n" + + "test_histogram_bucket{x='1',y='2',le='250'} 2 \\d+\n" + + "test_histogram_bucket{x='1',y='2',le='500'} 2 \\d+\n" + + "test_histogram_bucket{x='1',y='2',le='1000'} 2 \\d+\n" + + "test_histogram_bucket{x='1',y='2',le='\\+Inf'} 2 \\d+\n" + + "test_histogram_sum{x='1',y='2'} 118 \\d+\n" + + "test_histogram_count{x='1',y='2'} 2 \\d+\n" + + "$").Replace('\'', '"'), + Encoding.UTF8.GetString(buffer, 0, cursor)); + } + + [Fact] + public void HistogramInfinites() + { + var buffer = new byte[85000]; + var metrics = new List(); + + using var meter = new Meter(Utils.GetCurrentMethodName()); + using var provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(metrics) + .Build(); + + var histogram = meter.CreateHistogram("test_histogram"); + histogram.Record(18); + histogram.Record(double.PositiveInfinity); + histogram.Record(double.PositiveInfinity); + + provider.ForceFlush(); + + var cursor = PrometheusSerializer.WriteMetric(buffer, 0, metrics[0]); + Assert.Matches( + ("^" + + "# TYPE test_histogram histogram\n" + + "test_histogram_bucket{le='0'} 0 \\d+\n" + + "test_histogram_bucket{le='5'} 0 \\d+\n" + + "test_histogram_bucket{le='10'} 0 \\d+\n" + + "test_histogram_bucket{le='25'} 1 \\d+\n" + + "test_histogram_bucket{le='50'} 1 \\d+\n" + + "test_histogram_bucket{le='75'} 1 \\d+\n" + + "test_histogram_bucket{le='100'} 1 \\d+\n" + + "test_histogram_bucket{le='250'} 1 \\d+\n" + + "test_histogram_bucket{le='500'} 1 \\d+\n" + + "test_histogram_bucket{le='1000'} 1 \\d+\n" + + "test_histogram_bucket{le='\\+Inf'} 3 \\d+\n" + + "test_histogram_sum \\+Inf \\d+\n" + + "test_histogram_count 3 \\d+\n" + + "$").Replace('\'', '"'), + Encoding.UTF8.GetString(buffer, 0, cursor)); + } + + [Fact] + public void HistogramNaN() + { + var buffer = new byte[85000]; + var metrics = new List(); + + using var meter = new Meter(Utils.GetCurrentMethodName()); + using var provider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(metrics) + .Build(); + + var histogram = meter.CreateHistogram("test_histogram"); + histogram.Record(18); + histogram.Record(double.PositiveInfinity); + histogram.Record(double.NaN); + + provider.ForceFlush(); + + var cursor = PrometheusSerializer.WriteMetric(buffer, 0, metrics[0]); + Assert.Matches( + ("^" + + "# TYPE test_histogram histogram\n" + + "test_histogram_bucket{le='0'} 0 \\d+\n" + + "test_histogram_bucket{le='5'} 0 \\d+\n" + + "test_histogram_bucket{le='10'} 0 \\d+\n" + + "test_histogram_bucket{le='25'} 1 \\d+\n" + + "test_histogram_bucket{le='50'} 1 \\d+\n" + + "test_histogram_bucket{le='75'} 1 \\d+\n" + + "test_histogram_bucket{le='100'} 1 \\d+\n" + + "test_histogram_bucket{le='250'} 1 \\d+\n" + + "test_histogram_bucket{le='500'} 1 \\d+\n" + + "test_histogram_bucket{le='1000'} 1 \\d+\n" + + "test_histogram_bucket{le='\\+Inf'} 3 \\d+\n" + + "test_histogram_sum Nan \\d+\n" + + "test_histogram_count 3 \\d+\n" + + "$").Replace('\'', '"'), + Encoding.UTF8.GetString(buffer, 0, cursor)); + } + } +} diff --git a/test/OpenTelemetry.Exporter.ZPages.Tests/OpenTelemetry.Exporter.ZPages.Tests.csproj b/test/OpenTelemetry.Exporter.ZPages.Tests/OpenTelemetry.Exporter.ZPages.Tests.csproj index 222c00b976e..5fb58e19000 100644 --- a/test/OpenTelemetry.Exporter.ZPages.Tests/OpenTelemetry.Exporter.ZPages.Tests.csproj +++ b/test/OpenTelemetry.Exporter.ZPages.Tests/OpenTelemetry.Exporter.ZPages.Tests.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1;net5.0 + netcoreapp3.1;net5.0;net6.0 $(TargetFrameworks);net461 false diff --git a/test/OpenTelemetry.Exporter.ZPages.Tests/ZPagesExporterTests.cs b/test/OpenTelemetry.Exporter.ZPages.Tests/ZPagesExporterTests.cs index ac413ca0c6c..5713f3f9ef3 100644 --- a/test/OpenTelemetry.Exporter.ZPages.Tests/ZPagesExporterTests.cs +++ b/test/OpenTelemetry.Exporter.ZPages.Tests/ZPagesExporterTests.cs @@ -75,7 +75,7 @@ public void CheckingCustomActivityProcessor() endCalled = true; }; - using var openTelemetrySdk = Sdk.CreateTracerProviderBuilder() + using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource(ActivitySourceName) .AddProcessor(testActivityProcessor) .AddZPagesExporter() 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 b589ee66be0..368a21a52af 100644 --- a/test/OpenTelemetry.Exporter.Zipkin.Tests/OpenTelemetry.Exporter.Zipkin.Tests.csproj +++ b/test/OpenTelemetry.Exporter.Zipkin.Tests/OpenTelemetry.Exporter.Zipkin.Tests.csproj @@ -1,7 +1,7 @@  Unit test project for Zipkin Exporter for OpenTelemetry - netcoreapp3.1;net5.0 + netcoreapp3.1;net5.0;net6.0 $(TargetFrameworks);net461 false @@ -22,10 +22,13 @@ runtime; build; native; contentfiles; analyzers + + + diff --git a/test/OpenTelemetry.Exporter.Zipkin.Tests/ZipkinExporterTests.cs b/test/OpenTelemetry.Exporter.Zipkin.Tests/ZipkinExporterTests.cs index 8b4a6482b65..a547b09ba8a 100644 --- a/test/OpenTelemetry.Exporter.Zipkin.Tests/ZipkinExporterTests.cs +++ b/test/OpenTelemetry.Exporter.Zipkin.Tests/ZipkinExporterTests.cs @@ -21,7 +21,9 @@ using System.IO; using System.Linq; using System.Net; +using System.Net.Http; using System.Text; +using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Exporter.Zipkin.Implementation; using OpenTelemetry.Resources; using OpenTelemetry.Tests; @@ -110,7 +112,7 @@ public void SuppresssesInstrumentation() var zipkinExporter = new ZipkinExporter(exporterOptions); var exportActivityProcessor = new BatchActivityExportProcessor(zipkinExporter); - var openTelemetrySdk = Sdk.CreateTracerProviderBuilder() + var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource(ActivitySourceName) .AddProcessor(testActivityProcessor) .AddProcessor(exportActivityProcessor) @@ -175,9 +177,7 @@ public void ErrorGettingUriFromEnvVarSetsDefaultEndpointValue() { Environment.SetEnvironmentVariable(ZipkinExporterOptions.ZipkinEndpointEnvVar, "InvalidUri"); - var exporterOptions = new ZipkinExporterOptions(); - - Assert.Equal(new Uri(ZipkinExporterOptions.DefaultZipkinEndpoint).AbsoluteUri, exporterOptions.Endpoint.AbsoluteUri); + Assert.Throws(() => new ZipkinExporterOptions()); } finally { @@ -185,6 +185,73 @@ public void ErrorGettingUriFromEnvVarSetsDefaultEndpointValue() } } + [Fact] + public void UserHttpFactoryCalled() + { + ZipkinExporterOptions options = new ZipkinExporterOptions(); + + var defaultFactory = options.HttpClientFactory; + + int invocations = 0; + options.HttpClientFactory = () => + { + invocations++; + return defaultFactory(); + }; + + using (var exporter = new ZipkinExporter(options)) + { + Assert.Equal(1, invocations); + } + + using (var provider = Sdk.CreateTracerProviderBuilder() + .AddZipkinExporter(o => o.HttpClientFactory = options.HttpClientFactory) + .Build()) + { + Assert.Equal(2, invocations); + } + + using var client = new HttpClient(); + + using (var exporter = new ZipkinExporter(options, client)) + { + // Factory not called when client is passed as a param. + Assert.Equal(2, invocations); + } + + options.HttpClientFactory = null; + Assert.Throws(() => + { + using var exporter = new ZipkinExporter(options); + }); + + options.HttpClientFactory = () => null; + Assert.Throws(() => + { + using var exporter = new ZipkinExporter(options); + }); + } + + [Fact] + public void ServiceProviderHttpClientFactoryInvoked() + { + IServiceCollection services = new ServiceCollection(); + + services.AddHttpClient(); + + int invocations = 0; + + services.AddHttpClient("ZipkinExporter", configureClient: (client) => invocations++); + + services.AddOpenTelemetryTracing(builder => builder.AddZipkinExporter()); + + using var serviceProvider = services.BuildServiceProvider(); + + var tracerProvider = serviceProvider.GetRequiredService(); + + Assert.Equal(1, invocations); + } + [Theory] [InlineData(true, false, false)] [InlineData(false, false, false)] diff --git a/test/OpenTelemetry.Extensions.Hosting.Tests/HostingMeterExtensionTests.cs b/test/OpenTelemetry.Extensions.Hosting.Tests/HostingMeterExtensionTests.cs new file mode 100644 index 00000000000..9ed9e3a1f75 --- /dev/null +++ b/test/OpenTelemetry.Extensions.Hosting.Tests/HostingMeterExtensionTests.cs @@ -0,0 +1,233 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using OpenTelemetry.Metrics; +using Xunit; + +namespace OpenTelemetry.Extensions.Hosting.Tests +{ + public class HostingMeterExtensionTests + { + [Fact] + public async Task AddOpenTelemetryMeterProviderInstrumentationCreationAndDisposal() + { + var testInstrumentation = new TestInstrumentation(); + var callbackRun = false; + + var builder = new HostBuilder().ConfigureServices(services => + { + services.AddOpenTelemetryMetrics(builder => + { + builder.AddInstrumentation(() => + { + callbackRun = true; + return testInstrumentation; + }); + }); + }); + + var host = builder.Build(); + + Assert.False(callbackRun); + Assert.False(testInstrumentation.Disposed); + + await host.StartAsync(); + + Assert.True(callbackRun); + Assert.False(testInstrumentation.Disposed); + + await host.StopAsync(); + + Assert.True(callbackRun); + Assert.False(testInstrumentation.Disposed); + + host.Dispose(); + + Assert.True(callbackRun); + Assert.True(testInstrumentation.Disposed); + } + + [Fact] + public void AddOpenTelemetryMeterProvider_HostBuilt_OpenTelemetrySdk_RegisteredAsSingleton() + { + var builder = new HostBuilder().ConfigureServices(services => + { + services.AddOpenTelemetryMetrics(); + }); + + var host = builder.Build(); + + var meterProvider1 = host.Services.GetRequiredService(); + var meterProvider2 = host.Services.GetRequiredService(); + + Assert.Same(meterProvider1, meterProvider2); + } + + [Fact] + public void AddOpenTelemetryMeterProvider_ServiceProviderArgument_ServicesRegistered() + { + var testInstrumentation = new TestInstrumentation(); + + var services = new ServiceCollection(); + services.AddSingleton(testInstrumentation); + services.AddOpenTelemetryMetrics(builder => + { + builder.Configure( + (sp, b) => b.AddInstrumentation(() => sp.GetRequiredService())); + }); + + var serviceProvider = services.BuildServiceProvider(); + + var meterFactory = serviceProvider.GetRequiredService(); + Assert.NotNull(meterFactory); + + Assert.False(testInstrumentation.Disposed); + + serviceProvider.Dispose(); + + Assert.True(testInstrumentation.Disposed); + } + + [Fact] + public void AddOpenTelemetryMeterProvider_BadArgs_NullServiceCollection() + { + ServiceCollection services = null; + Assert.Throws(() => services.AddOpenTelemetryMetrics(null)); + Assert.Throws(() => + services.AddOpenTelemetryMetrics(builder => + { + builder.Configure( + (sp, b) => b.AddInstrumentation(() => sp.GetRequiredService())); + })); + } + + [Fact] + public void AddOpenTelemetryMeterProvider_GetServicesExtension() + { + var services = new ServiceCollection(); + services.AddOpenTelemetryMetrics(builder => AddMyFeature(builder)); + + using var serviceProvider = services.BuildServiceProvider(); + + var meterProvider = (MeterProviderSdk)serviceProvider.GetRequiredService(); + + Assert.True(meterProvider.Reader is TestReader); + } + + [Fact] + public void AddOpenTelemetryMeterProvider_NestedConfigureCallbacks() + { + int configureCalls = 0; + var services = new ServiceCollection(); + services.AddOpenTelemetryMetrics(builder => builder + .Configure((sp1, builder1) => + { + configureCalls++; + builder1.Configure((sp2, builder2) => + { + configureCalls++; + }); + })); + + using var serviceProvider = services.BuildServiceProvider(); + + var meterFactory = serviceProvider.GetRequiredService(); + + Assert.Equal(2, configureCalls); + } + + [Fact] + public void AddOpenTelemetryMeterProvider_ConfigureCallbacksUsingExtensions() + { + var services = new ServiceCollection(); + + services.AddSingleton(); + services.AddSingleton(); + + services.AddOpenTelemetryMetrics(builder => builder + .Configure((sp1, builder1) => + { + builder1 + .AddInstrumentation() + .AddReader(); + })); + + using var serviceProvider = services.BuildServiceProvider(); + + var meterProvider = (MeterProviderSdk)serviceProvider.GetRequiredService(); + + Assert.True(meterProvider.Instrumentations.FirstOrDefault() is TestInstrumentation); + Assert.True(meterProvider.Reader is TestReader); + } + + [Fact(Skip = "Known limitation. See issue 1215.")] + public void AddOpenTelemetryMeterProvider_Idempotent() + { + var testInstrumentation1 = new TestInstrumentation(); + var testInstrumentation2 = new TestInstrumentation(); + + var services = new ServiceCollection(); + services.AddSingleton(testInstrumentation1); + services.AddOpenTelemetryMetrics(builder => + { + builder.AddInstrumentation(() => testInstrumentation1); + }); + + services.AddOpenTelemetryMetrics(builder => + { + builder.AddInstrumentation(() => testInstrumentation2); + }); + + var serviceProvider = services.BuildServiceProvider(); + + var meterFactory = serviceProvider.GetRequiredService(); + Assert.NotNull(meterFactory); + + Assert.False(testInstrumentation1.Disposed); + Assert.False(testInstrumentation2.Disposed); + serviceProvider.Dispose(); + Assert.True(testInstrumentation1.Disposed); + Assert.True(testInstrumentation2.Disposed); + } + + private static MeterProviderBuilder AddMyFeature(MeterProviderBuilder meterProviderBuilder) + { + (meterProviderBuilder.GetServices() ?? throw new NotSupportedException("MyFeature requires a hosting MeterProviderBuilder instance.")) + .AddSingleton(); + + return meterProviderBuilder.AddReader(); + } + + internal class TestInstrumentation : IDisposable + { + public bool Disposed { get; private set; } + + public void Dispose() + { + this.Disposed = true; + } + } + + internal class TestReader : MetricReader + { + } + } +} diff --git a/test/OpenTelemetry.Extensions.Hosting.Tests/HostingExtensionsTests.cs b/test/OpenTelemetry.Extensions.Hosting.Tests/HostingTracerExtensionTests.cs similarity index 98% rename from test/OpenTelemetry.Extensions.Hosting.Tests/HostingExtensionsTests.cs rename to test/OpenTelemetry.Extensions.Hosting.Tests/HostingTracerExtensionTests.cs index 0a56f542e42..b897dd42098 100644 --- a/test/OpenTelemetry.Extensions.Hosting.Tests/HostingExtensionsTests.cs +++ b/test/OpenTelemetry.Extensions.Hosting.Tests/HostingTracerExtensionTests.cs @@ -1,4 +1,4 @@ -// +// // Copyright The OpenTelemetry Authors // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,7 +25,7 @@ namespace OpenTelemetry.Extensions.Hosting.Tests { - public class HostingExtensionsTests + public class HostingTracerExtensionTests { [Fact] public async Task AddOpenTelemetryTracerProviderInstrumentationCreationAndDisposal() 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 8212752c894..d7916fd5a2c 100644 --- a/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetry.Extensions.Hosting.Tests.csproj +++ b/test/OpenTelemetry.Extensions.Hosting.Tests/OpenTelemetry.Extensions.Hosting.Tests.csproj @@ -1,7 +1,7 @@  Unit test project for OpenTelemetry .NET Core hosting library - netcoreapp3.1;net5.0 + netcoreapp3.1;net5.0;net6.0 $(TargetFrameworks);net461 diff --git a/test/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.Tests/ActivityExtensionsTest.cs b/test/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.Tests/ActivityExtensionsTest.cs deleted file mode 100644 index 8bb6505ca48..00000000000 --- a/test/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.Tests/ActivityExtensionsTest.cs +++ /dev/null @@ -1,322 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -namespace OpenTelemetry.Instrumentation.AspNet.Tests -{ - using System.Collections.Generic; - using System.Collections.Specialized; - using System.Diagnostics; - using System.Linq; - using Xunit; - - public class ActivityExtensionsTest - { - private const string TestActivityName = "Activity.Test"; - - [Fact] - public void Restore_Nothing_If_Header_Does_Not_Contain_RequestId() - { - var activity = new Activity(TestActivityName); - var requestHeaders = new NameValueCollection(); - - Assert.False(activity.Extract(requestHeaders)); - - Assert.True(string.IsNullOrEmpty(activity.ParentId)); - Assert.Null(activity.TraceStateString); - Assert.Empty(activity.Baggage); - } - - [Fact] - public void Can_Restore_First_RequestId_When_Multiple_RequestId_In_Headers() - { - var activity = new Activity(TestActivityName); - var requestHeaders = new NameValueCollection - { - { ActivityExtensions.RequestIdHeaderName, "|aba2f1e978b11111.1" }, - { ActivityExtensions.RequestIdHeaderName, "|aba2f1e978b22222.1" }, - }; - Assert.True(activity.Extract(requestHeaders)); - - Assert.Equal("|aba2f1e978b11111.1", activity.ParentId); - Assert.Empty(activity.Baggage); - } - - [Fact] - public void Extract_RequestId_Is_Ignored_When_Traceparent_Is_Present() - { - var activity = new Activity(TestActivityName); - var requestHeaders = new NameValueCollection - { - { ActivityExtensions.RequestIdHeaderName, "|aba2f1e978b11111.1" }, - { ActivityExtensions.TraceparentHeaderName, "00-0123456789abcdef0123456789abcdef-0123456789abcdef-00" }, - }; - Assert.True(activity.Extract(requestHeaders)); - - activity.Start(); - Assert.Equal(ActivityIdFormat.W3C, activity.IdFormat); - Assert.Equal("00-0123456789abcdef0123456789abcdef-0123456789abcdef-00", activity.ParentId); - Assert.Equal("0123456789abcdef0123456789abcdef", activity.TraceId.ToHexString()); - Assert.Equal("0123456789abcdef", activity.ParentSpanId.ToHexString()); - Assert.False(activity.Recorded); - - Assert.Null(activity.TraceStateString); - Assert.Empty(activity.Baggage); - } - - [Fact] - public void Can_Extract_First_Traceparent_When_Multiple_Traceparents_In_Headers() - { - var activity = new Activity(TestActivityName); - var requestHeaders = new NameValueCollection - { - { ActivityExtensions.TraceparentHeaderName, "00-0123456789abcdef0123456789abcdef-0123456789abcdef-00" }, - { ActivityExtensions.TraceparentHeaderName, "00-fedcba09876543210fedcba09876543210-fedcba09876543210-01" }, - }; - Assert.True(activity.Extract(requestHeaders)); - - activity.Start(); - Assert.Equal(ActivityIdFormat.W3C, activity.IdFormat); - Assert.Equal("00-0123456789abcdef0123456789abcdef-0123456789abcdef-00", activity.ParentId); - Assert.Equal("0123456789abcdef0123456789abcdef", activity.TraceId.ToHexString()); - Assert.Equal("0123456789abcdef", activity.ParentSpanId.ToHexString()); - Assert.False(activity.Recorded); - - Assert.Null(activity.TraceStateString); - Assert.Empty(activity.Baggage); - } - - [Fact] - public void Can_Extract_RootActivity_From_W3C_Headers_And_CC() - { - var activity = new Activity(TestActivityName); - var requestHeaders = new NameValueCollection - { - { ActivityExtensions.TraceparentHeaderName, "00-0123456789abcdef0123456789abcdef-0123456789abcdef-01" }, - { ActivityExtensions.TracestateHeaderName, "ts1=v1,ts2=v2" }, - { ActivityExtensions.CorrelationContextHeaderName, "key1=123,key2=456,key3=789" }, - }; - - Assert.True(activity.Extract(requestHeaders)); - activity.Start(); - Assert.Equal(ActivityIdFormat.W3C, activity.IdFormat); - Assert.Equal("0123456789abcdef0123456789abcdef", activity.TraceId.ToHexString()); - Assert.Equal("0123456789abcdef", activity.ParentSpanId.ToHexString()); - Assert.True(activity.Recorded); - - Assert.Equal("ts1=v1,ts2=v2", activity.TraceStateString); - var baggageItems = new List> - { - new KeyValuePair("key1", "123"), - new KeyValuePair("key2", "456"), - new KeyValuePair("key3", "789"), - }; - var expectedBaggage = baggageItems.OrderBy(kvp => kvp.Key); - var actualBaggage = activity.Baggage.OrderBy(kvp => kvp.Key); - Assert.Equal(expectedBaggage, actualBaggage); - } - - [Fact] - public void Can_Extract_Empty_Traceparent() - { - var activity = new Activity(TestActivityName); - var requestHeaders = new NameValueCollection - { - { ActivityExtensions.TraceparentHeaderName, string.Empty }, - }; - - Assert.False(activity.Extract(requestHeaders)); - - Assert.Equal(default, activity.ParentSpanId); - Assert.Null(activity.ParentId); - } - - [Fact] - public void Can_Extract_Multi_Line_Tracestate() - { - var activity = new Activity(TestActivityName); - var requestHeaders = new NameValueCollection - { - { ActivityExtensions.TraceparentHeaderName, "00-0123456789abcdef0123456789abcdef-0123456789abcdef-01" }, - { ActivityExtensions.TracestateHeaderName, "ts1=v1" }, - { ActivityExtensions.TracestateHeaderName, "ts2=v2" }, - }; - - Assert.True(activity.Extract(requestHeaders)); - activity.Start(); - Assert.Equal(ActivityIdFormat.W3C, activity.IdFormat); - Assert.Equal("0123456789abcdef0123456789abcdef", activity.TraceId.ToHexString()); - Assert.Equal("0123456789abcdef", activity.ParentSpanId.ToHexString()); - Assert.True(activity.Recorded); - - Assert.Equal("ts1=v1,ts2=v2", activity.TraceStateString); - } - - [Fact] - public void Restore_Empty_RequestId_Should_Not_Throw_Exception() - { - var activity = new Activity(TestActivityName); - var requestHeaders = new NameValueCollection - { - { ActivityExtensions.RequestIdHeaderName, string.Empty }, - }; - Assert.False(activity.Extract(requestHeaders)); - - Assert.Null(activity.ParentId); - Assert.Empty(activity.Baggage); - } - - [Fact] - public void Restore_Empty_Traceparent_Should_Not_Throw_Exception() - { - var activity = new Activity(TestActivityName); - var requestHeaders = new NameValueCollection - { - { ActivityExtensions.TraceparentHeaderName, string.Empty }, - }; - Assert.False(activity.Extract(requestHeaders)); - - Assert.Null(activity.ParentId); - Assert.Null(activity.TraceStateString); - Assert.Empty(activity.Baggage); - } - - [Fact] - public void Can_Restore_Baggages_When_CorrelationContext_In_Headers() - { - var activity = new Activity(TestActivityName); - var requestHeaders = new NameValueCollection - { - { ActivityExtensions.RequestIdHeaderName, "|aba2f1e978b11111.1" }, - { ActivityExtensions.CorrelationContextHeaderName, "key1=123,key2=456,key3=789" }, - }; - Assert.True(activity.Extract(requestHeaders)); - - Assert.Equal("|aba2f1e978b11111.1", activity.ParentId); - var baggageItems = new List> - { - new KeyValuePair("key1", "123"), - new KeyValuePair("key2", "456"), - new KeyValuePair("key3", "789"), - }; - var expectedBaggage = baggageItems.OrderBy(kvp => kvp.Key); - var actualBaggage = activity.Baggage.OrderBy(kvp => kvp.Key); - Assert.Equal(expectedBaggage, actualBaggage); - } - - [Fact] - public void Can_Restore_Baggages_When_Multiple_CorrelationContext_In_Headers() - { - var activity = new Activity(TestActivityName); - var requestHeaders = new NameValueCollection - { - { ActivityExtensions.RequestIdHeaderName, "|aba2f1e978b11111.1" }, - { ActivityExtensions.CorrelationContextHeaderName, "key1=123,key2=456,key3=789" }, - { ActivityExtensions.CorrelationContextHeaderName, "key4=abc,key5=def" }, - { ActivityExtensions.CorrelationContextHeaderName, "key6=xyz" }, - }; - Assert.True(activity.Extract(requestHeaders)); - - Assert.Equal("|aba2f1e978b11111.1", activity.ParentId); - var baggageItems = new List> - { - new KeyValuePair("key1", "123"), - new KeyValuePair("key2", "456"), - new KeyValuePair("key3", "789"), - new KeyValuePair("key4", "abc"), - new KeyValuePair("key5", "def"), - new KeyValuePair("key6", "xyz"), - }; - var expectedBaggage = baggageItems.OrderBy(kvp => kvp.Key); - var actualBaggage = activity.Baggage.OrderBy(kvp => kvp.Key); - Assert.Equal(expectedBaggage, actualBaggage); - } - - [Fact] - public void Can_Restore_Baggages_When_Some_MalFormat_CorrelationContext_In_Headers() - { - var activity = new Activity(TestActivityName); - var requestHeaders = new NameValueCollection - { - { ActivityExtensions.RequestIdHeaderName, "|aba2f1e978b11111.1" }, - { ActivityExtensions.CorrelationContextHeaderName, "key1=123,key2=456,key3=789" }, - { ActivityExtensions.CorrelationContextHeaderName, "key4=abc;key5=def" }, - { ActivityExtensions.CorrelationContextHeaderName, "key6????xyz" }, - { ActivityExtensions.CorrelationContextHeaderName, "key7=123=456" }, - }; - Assert.True(activity.Extract(requestHeaders)); - - Assert.Equal("|aba2f1e978b11111.1", activity.ParentId); - var baggageItems = new List> - { - new KeyValuePair("key1", "123"), - new KeyValuePair("key2", "456"), - new KeyValuePair("key3", "789"), - }; - var expectedBaggage = baggageItems.OrderBy(kvp => kvp.Key); - var actualBaggage = activity.Baggage.OrderBy(kvp => kvp.Key); - Assert.Equal(expectedBaggage, actualBaggage); - } - - [Theory] - [InlineData( - "key0=value0,key1=value1,key2=value2,key3=value3,key4=value4,key5=value5,key6=value6,key7=value7,key8=value8,key9=value9," + - "key10=value10,key11=value11,key12=value12,key13=value13,key14=value14,key15=value15,key16=value16,key17=value17,key18=value18,key19=value19," + - "key20=value20,key21=value21,key22=value22,key23=value23,key24=value24,key25=value25,key26=value26,key27=value27,key28=value28,key29=value29," + - "key30=value30,key31=value31,key32=value32,key33=value33,key34=value34,key35=value35,key36=value36,key37=value37,key38=value38,key39=value39," + - "key40=value40,key41=value41,key42=value42,key43=value43,key44=value44,key45=value45,key46=value46,key47=value47,key48=value48,key49=value49," + - "key50=value50,key51=value51,key52=value52,key53=value53,key54=value54,key55=value55,key56=value56,key57=value57,key58=value58,key59=value59," + - "key60=value60,key61=value61,key62=value62,key63=value63,key64=value64,key65=value65,key66=value66,key67=value67,key68=value68,key69=value69," + - "key70=value70,key71=value71,key72=value72,key73=value73,k100=vx", 1023)] // 1023 chars - [InlineData( - "key0=value0,key1=value1,key2=value2,key3=value3,key4=value4,key5=value5,key6=value6,key7=value7,key8=value8,key9=value9," + - "key10=value10,key11=value11,key12=value12,key13=value13,key14=value14,key15=value15,key16=value16,key17=value17,key18=value18,key19=value19," + - "key20=value20,key21=value21,key22=value22,key23=value23,key24=value24,key25=value25,key26=value26,key27=value27,key28=value28,key29=value29," + - "key30=value30,key31=value31,key32=value32,key33=value33,key34=value34,key35=value35,key36=value36,key37=value37,key38=value38,key39=value39," + - "key40=value40,key41=value41,key42=value42,key43=value43,key44=value44,key45=value45,key46=value46,key47=value47,key48=value48,key49=value49," + - "key50=value50,key51=value51,key52=value52,key53=value53,key54=value54,key55=value55,key56=value56,key57=value57,key58=value58,key59=value59," + - "key60=value60,key61=value61,key62=value62,key63=value63,key64=value64,key65=value65,key66=value66,key67=value67,key68=value68,key69=value69," + - "key70=value70,key71=value71,key72=value72,key73=value73,k100=vx1", 1024)] // 1024 chars - [InlineData( - "key0=value0,key1=value1,key2=value2,key3=value3,key4=value4,key5=value5,key6=value6,key7=value7,key8=value8,key9=value9," + - "key10=value10,key11=value11,key12=value12,key13=value13,key14=value14,key15=value15,key16=value16,key17=value17,key18=value18,key19=value19," + - "key20=value20,key21=value21,key22=value22,key23=value23,key24=value24,key25=value25,key26=value26,key27=value27,key28=value28,key29=value29," + - "key30=value30,key31=value31,key32=value32,key33=value33,key34=value34,key35=value35,key36=value36,key37=value37,key38=value38,key39=value39," + - "key40=value40,key41=value41,key42=value42,key43=value43,key44=value44,key45=value45,key46=value46,key47=value47,key48=value48,key49=value49," + - "key50=value50,key51=value51,key52=value52,key53=value53,key54=value54,key55=value55,key56=value56,key57=value57,key58=value58,key59=value59," + - "key60=value60,key61=value61,key62=value62,key63=value63,key64=value64,key65=value65,key66=value66,key67=value67,key68=value68,key69=value69," + - "key70=value70,key71=value71,key72=value72,key73=value73,key74=value74", 1029)] // more than 1024 chars - public void Validates_Correlation_Context_Length(string correlationContext, int expectedLength) - { - var activity = new Activity(TestActivityName); - var requestHeaders = new NameValueCollection - { - { ActivityExtensions.RequestIdHeaderName, "|abc.1" }, - { ActivityExtensions.CorrelationContextHeaderName, correlationContext }, - }; - Assert.True(activity.Extract(requestHeaders)); - - var baggageItems = Enumerable.Range(0, 74).Select(i => new KeyValuePair("key" + i, "value" + i)).ToList(); - if (expectedLength < 1024) - { - baggageItems.Add(new KeyValuePair("k100", "vx")); - } - - var expectedBaggage = baggageItems.OrderBy(kvp => kvp.Key); - var actualBaggage = activity.Baggage.OrderBy(kvp => kvp.Key); - Assert.Equal(expectedBaggage, actualBaggage); - } - } -} diff --git a/test/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.Tests/ActivityHelperTest.cs b/test/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.Tests/ActivityHelperTest.cs index 8cfc2b76919..5252a04dea6 100644 --- a/test/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.Tests/ActivityHelperTest.cs +++ b/test/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.Tests/ActivityHelperTest.cs @@ -21,111 +21,148 @@ namespace OpenTelemetry.Instrumentation.AspNet.Tests using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; - using System.Linq; - using System.Reflection; using System.Threading; using System.Threading.Tasks; using System.Web; + using OpenTelemetry.Context.Propagation; using Xunit; public class ActivityHelperTest : IDisposable { + private const string TraceParentHeaderName = "traceparent"; + private const string TraceStateHeaderName = "tracestate"; + private const string BaggageHeaderName = "baggage"; + private const string BaggageInHeader = "TestKey1=123,TestKey2=456,TestKey1=789"; private const string TestActivityName = "Activity.Test"; - private readonly List> baggageItems; - private readonly string baggageInHeader; - private IDisposable subscriptionAllListeners; - private IDisposable subscriptionAspNetListener; + private readonly TextMapPropagator noopTextMapPropagator = new NoopTextMapPropagator(); + private ActivityListener activitySourceListener; - public ActivityHelperTest() + public void Dispose() { - this.baggageItems = new List> - { - new KeyValuePair("TestKey1", "123"), - new KeyValuePair("TestKey2", "456"), - new KeyValuePair("TestKey1", "789"), - }; - - this.baggageInHeader = "TestKey1=123,TestKey2=456,TestKey1=789"; - - // reset static fields - var allListenerField = typeof(DiagnosticListener). - GetField("s_allListenerObservable", BindingFlags.Static | BindingFlags.NonPublic); - allListenerField.SetValue(null, null); - var aspnetListenerField = typeof(ActivityHelper). - GetField("AspNetListener", BindingFlags.Static | BindingFlags.NonPublic); - aspnetListenerField.SetValue(null, new DiagnosticListener(ActivityHelper.AspNetListenerName)); + this.activitySourceListener?.Dispose(); } - public void Dispose() + [Fact] + public void Has_Started_Returns_Correctly() { - this.subscriptionAspNetListener?.Dispose(); - this.subscriptionAllListeners?.Dispose(); + var context = HttpContextHelper.GetFakeHttpContext(); + + bool result = ActivityHelper.HasStarted(context, out Activity aspNetActivity); + + Assert.False(result); + Assert.Null(aspNetActivity); + + context.Items[ActivityHelper.ContextKey] = ActivityHelper.StartedButNotSampledObj; + + result = ActivityHelper.HasStarted(context, out aspNetActivity); + + Assert.True(result); + Assert.Null(aspNetActivity); + + Activity activity = new Activity(TestActivityName); + context.Items[ActivityHelper.ContextKey] = new ActivityHelper.ContextHolder { Activity = activity }; + + result = ActivityHelper.HasStarted(context, out aspNetActivity); + + Assert.True(result); + Assert.NotNull(aspNetActivity); + Assert.Equal(activity, aspNetActivity); } [Fact] - public void Can_Restore_Activity() + public async Task Can_Restore_Activity() { - this.EnableAll(); + this.EnableListener(); var context = HttpContextHelper.GetFakeHttpContext(); - var rootActivity = ActivityHelper.CreateRootActivity(context, false); + using var rootActivity = ActivityHelper.StartAspNetActivity(this.noopTextMapPropagator, context, null); rootActivity.AddTag("k1", "v1"); rootActivity.AddTag("k2", "v2"); - Activity.Current = null; + Task testTask; + using (ExecutionContext.SuppressFlow()) + { + testTask = Task.Run(() => + { + Task.Yield(); + + Assert.Null(Activity.Current); + + ActivityHelper.RestoreContextIfNeeded(context); - ActivityHelper.RestoreActivityIfNeeded(context.Items); + Assert.Same(Activity.Current, rootActivity); + }); + } - Assert.Same(Activity.Current, rootActivity); + await testTask.ConfigureAwait(false); } - [Fact] - public void Can_Stop_Lost_Activity() + [Fact(Skip = "Temporarily disable until stable.")] + public async Task Can_Restore_Baggage() { - this.EnableAll(pair => + this.EnableListener(); + + var requestHeaders = new Dictionary { - Assert.NotNull(Activity.Current); - Assert.Equal(ActivityHelper.AspNetActivityName, Activity.Current.OperationName); - }); - var context = HttpContextHelper.GetFakeHttpContext(); - var rootActivity = ActivityHelper.CreateRootActivity(context, false); + { BaggageHeaderName, BaggageInHeader }, + }; + + var context = HttpContextHelper.GetFakeHttpContext(headers: requestHeaders); + using var rootActivity = ActivityHelper.StartAspNetActivity(new CompositeTextMapPropagator(new TextMapPropagator[] { new TraceContextPropagator(), new BaggagePropagator() }), context, null); + rootActivity.AddTag("k1", "v1"); rootActivity.AddTag("k2", "v2"); - Activity.Current = null; + Task testTask; + using (ExecutionContext.SuppressFlow()) + { + testTask = Task.Run(() => + { + Task.Yield(); - ActivityHelper.StopAspNetActivity(context.Items); - Assert.True(rootActivity.Duration != TimeSpan.Zero); - Assert.Null(Activity.Current); - Assert.Null(context.Items[ActivityHelper.ActivityKey]); + Assert.Null(Activity.Current); + Assert.Equal(0, Baggage.Current.Count); + + ActivityHelper.RestoreContextIfNeeded(context); + + Assert.Same(Activity.Current, rootActivity); + Assert.Empty(rootActivity.Baggage); + + Assert.Equal(2, Baggage.Current.Count); + Assert.Equal("789", Baggage.Current.GetBaggage("TestKey1")); + Assert.Equal("456", Baggage.Current.GetBaggage("TestKey2")); + }); + } + + await testTask.ConfigureAwait(false); } [Fact] - public void Can_Not_Stop_Lost_Activity_If_Not_In_Context() + public void Can_Stop_Lost_Activity() { - this.EnableAll(pair => + this.EnableListener(a => { Assert.NotNull(Activity.Current); - Assert.Equal(ActivityHelper.AspNetActivityName, Activity.Current.OperationName); + Assert.Equal(Activity.Current, a); + Assert.Equal(TelemetryHttpModule.AspNetActivityName, Activity.Current.OperationName); }); var context = HttpContextHelper.GetFakeHttpContext(); - var rootActivity = ActivityHelper.CreateRootActivity(context, false); - context.Items.Remove(ActivityHelper.ActivityKey); + using var rootActivity = ActivityHelper.StartAspNetActivity(this.noopTextMapPropagator, context, null); rootActivity.AddTag("k1", "v1"); rootActivity.AddTag("k2", "v2"); Activity.Current = null; - ActivityHelper.StopAspNetActivity(context.Items); - Assert.True(rootActivity.Duration == TimeSpan.Zero); + ActivityHelper.StopAspNetActivity(this.noopTextMapPropagator, rootActivity, context, null); + Assert.True(rootActivity.Duration != TimeSpan.Zero); Assert.Null(Activity.Current); - Assert.Null(context.Items[ActivityHelper.ActivityKey]); + Assert.Null(context.Items[ActivityHelper.ContextKey]); } [Fact] public void Do_Not_Restore_Activity_When_There_Is_No_Activity_In_Context() { - this.EnableAll(); - ActivityHelper.RestoreActivityIfNeeded(HttpContextHelper.GetFakeHttpContext().Items); + this.EnableListener(); + ActivityHelper.RestoreContextIfNeeded(HttpContextHelper.GetFakeHttpContext()); Assert.Null(Activity.Current); } @@ -133,15 +170,13 @@ public void Do_Not_Restore_Activity_When_There_Is_No_Activity_In_Context() [Fact] public void Do_Not_Restore_Activity_When_It_Is_Not_Lost() { - this.EnableAll(); + this.EnableListener(); var root = new Activity("root").Start(); var context = HttpContextHelper.GetFakeHttpContext(); - context.Items[ActivityHelper.ActivityKey] = root; + context.Items[ActivityHelper.ContextKey] = new ActivityHelper.ContextHolder { Activity = root }; - var module = new TelemetryHttpModule(); - - ActivityHelper.RestoreActivityIfNeeded(context.Items); + ActivityHelper.RestoreContextIfNeeded(context); Assert.Equal(root, Activity.Current); } @@ -150,107 +185,73 @@ public void Do_Not_Restore_Activity_When_It_Is_Not_Lost() public void Can_Stop_Activity_Without_AspNetListener_Enabled() { var context = HttpContextHelper.GetFakeHttpContext(); - var rootActivity = this.CreateActivity(); + var rootActivity = new Activity(TestActivityName); rootActivity.Start(); - context.Items[ActivityHelper.ActivityKey] = rootActivity; + context.Items[ActivityHelper.ContextKey] = new ActivityHelper.ContextHolder { Activity = rootActivity }; Thread.Sleep(100); - ActivityHelper.StopAspNetActivity(context.Items); + ActivityHelper.StopAspNetActivity(this.noopTextMapPropagator, rootActivity, context, null); Assert.True(rootActivity.Duration != TimeSpan.Zero); Assert.Null(rootActivity.Parent); - Assert.Null(context.Items[ActivityHelper.ActivityKey]); + Assert.Null(context.Items[ActivityHelper.ContextKey]); } [Fact] public void Can_Stop_Activity_With_AspNetListener_Enabled() { var context = HttpContextHelper.GetFakeHttpContext(); - var rootActivity = this.CreateActivity(); + var rootActivity = new Activity(TestActivityName); rootActivity.Start(); - context.Items[ActivityHelper.ActivityKey] = rootActivity; + context.Items[ActivityHelper.ContextKey] = new ActivityHelper.ContextHolder { Activity = rootActivity }; Thread.Sleep(100); - this.EnableAspNetListenerOnly(); - ActivityHelper.StopAspNetActivity(context.Items); + this.EnableListener(); + ActivityHelper.StopAspNetActivity(this.noopTextMapPropagator, rootActivity, context, null); Assert.True(rootActivity.Duration != TimeSpan.Zero); Assert.Null(rootActivity.Parent); - Assert.Null(context.Items[ActivityHelper.ActivityKey]); + Assert.Null(context.Items[ActivityHelper.ContextKey]); } [Fact] public void Can_Stop_Root_Activity_With_All_Children() { - this.EnableAll(); + this.EnableListener(); var context = HttpContextHelper.GetFakeHttpContext(); - var rootActivity = ActivityHelper.CreateRootActivity(context, false); + using var rootActivity = ActivityHelper.StartAspNetActivity(this.noopTextMapPropagator, context, null); var child = new Activity("child").Start(); new Activity("grandchild").Start(); - ActivityHelper.StopAspNetActivity(context.Items); + ActivityHelper.StopAspNetActivity(this.noopTextMapPropagator, rootActivity, context, null); Assert.True(rootActivity.Duration != TimeSpan.Zero); Assert.True(child.Duration == TimeSpan.Zero); Assert.Null(rootActivity.Parent); - Assert.Null(context.Items[ActivityHelper.ActivityKey]); + Assert.Null(context.Items[ActivityHelper.ContextKey]); } [Fact] public void Can_Stop_Root_While_Child_Is_Current() { - this.EnableAll(); + this.EnableListener(); var context = HttpContextHelper.GetFakeHttpContext(); - var rootActivity = ActivityHelper.CreateRootActivity(context, false); + using var rootActivity = ActivityHelper.StartAspNetActivity(this.noopTextMapPropagator, context, null); var child = new Activity("child").Start(); - ActivityHelper.StopAspNetActivity(context.Items); + ActivityHelper.StopAspNetActivity(this.noopTextMapPropagator, rootActivity, context, null); Assert.True(child.Duration == TimeSpan.Zero); - Assert.Null(Activity.Current); - Assert.Null(context.Items[ActivityHelper.ActivityKey]); - } - - [Fact] - public void OnImportActivity_Is_Called() - { - bool onImportIsCalled = false; - Activity importedActivity = null; - this.EnableAll(onImport: (activity, _) => - { - onImportIsCalled = true; - importedActivity = activity; - Assert.Null(Activity.Current); - }); - - var context = HttpContextHelper.GetFakeHttpContext(); - var rootActivity = ActivityHelper.CreateRootActivity(context, false); - Assert.True(onImportIsCalled); - Assert.NotNull(importedActivity); - Assert.Equal(importedActivity, Activity.Current); - Assert.Equal(importedActivity, rootActivity); - } - - [Fact] - public void OnImportActivity_Can_Set_Parent() - { - this.EnableAll(onImport: (activity, _) => - { - Assert.Null(activity.ParentId); - activity.SetParentId("|guid.123."); - }); - - var context = HttpContextHelper.GetFakeHttpContext(); - var rootActivity = ActivityHelper.CreateRootActivity(context, false); - - Assert.Equal("|guid.123.", Activity.Current.ParentId); + Assert.NotNull(Activity.Current); + Assert.Equal(Activity.Current, child); + Assert.Null(context.Items[ActivityHelper.ContextKey]); } [Fact] public async Task Can_Stop_Root_Activity_If_It_Is_Broken() { - this.EnableAll(); + this.EnableListener(); var context = HttpContextHelper.GetFakeHttpContext(); - var root = ActivityHelper.CreateRootActivity(context, false); + using var root = ActivityHelper.StartAspNetActivity(this.noopTextMapPropagator, context, null); new Activity("child").Start(); for (int i = 0; i < 2; i++) @@ -269,18 +270,18 @@ await Task.Run(() => // do not affect 'parent' context in which Task.Run is called. // But 'child' Activity is stopped, thus consequent calls to Stop will // not update Current - ActivityHelper.StopAspNetActivity(context.Items); + ActivityHelper.StopAspNetActivity(this.noopTextMapPropagator, root, context, null); Assert.True(root.Duration != TimeSpan.Zero); - Assert.Null(context.Items[ActivityHelper.ActivityKey]); + Assert.Null(context.Items[ActivityHelper.ContextKey]); Assert.Null(Activity.Current); } [Fact] public void Stop_Root_Activity_With_129_Nesting_Depth() { - this.EnableAll(); + this.EnableListener(); var context = HttpContextHelper.GetFakeHttpContext(); - var root = ActivityHelper.CreateRootActivity(context, false); + using var root = ActivityHelper.StartAspNetActivity(this.noopTextMapPropagator, context, null); for (int i = 0; i < 129; i++) { @@ -288,100 +289,77 @@ public void Stop_Root_Activity_With_129_Nesting_Depth() } // can stop any activity regardless of the stack depth - ActivityHelper.StopAspNetActivity(context.Items); + ActivityHelper.StopAspNetActivity(this.noopTextMapPropagator, root, context, null); Assert.True(root.Duration != TimeSpan.Zero); - Assert.Null(context.Items[ActivityHelper.ActivityKey]); - Assert.Null(Activity.Current); + Assert.Null(context.Items[ActivityHelper.ContextKey]); + Assert.NotNull(Activity.Current); } [Fact] public void Should_Not_Create_RootActivity_If_AspNetListener_Not_Enabled() { var context = HttpContextHelper.GetFakeHttpContext(); - var rootActivity = ActivityHelper.CreateRootActivity(context, true); + using var rootActivity = ActivityHelper.StartAspNetActivity(this.noopTextMapPropagator, context, null); Assert.Null(rootActivity); - } - - [Fact] - public void Should_Not_Create_RootActivity_If_AspNetActivity_Not_Enabled() - { - var context = HttpContextHelper.GetFakeHttpContext(); - this.EnableAspNetListenerOnly(); - var rootActivity = ActivityHelper.CreateRootActivity(context, true); + Assert.Equal(ActivityHelper.StartedButNotSampledObj, context.Items[ActivityHelper.ContextKey]); - Assert.Null(rootActivity); + ActivityHelper.StopAspNetActivity(this.noopTextMapPropagator, rootActivity, context, null); + Assert.Null(context.Items[ActivityHelper.ContextKey]); } [Fact] - public void Should_Not_Create_RootActivity_If_AspNetActivity_Not_Enabled_With_Arguments() + public void Should_Not_Create_RootActivity_If_AspNetActivity_Not_Enabled() { var context = HttpContextHelper.GetFakeHttpContext(); - this.EnableAspNetListenerAndDisableActivity(); - var rootActivity = ActivityHelper.CreateRootActivity(context, true); + this.EnableListener(onSample: (context) => ActivitySamplingResult.None); + using var rootActivity = ActivityHelper.StartAspNetActivity(this.noopTextMapPropagator, context, null); Assert.Null(rootActivity); - } - - [Fact] - public void Can_Create_RootActivity_And_Restore_Info_From_Request_Header() - { - this.EnableAll(); - var requestHeaders = new Dictionary - { - { ActivityExtensions.RequestIdHeaderName, "|aba2f1e978b2cab6.1." }, - { ActivityExtensions.CorrelationContextHeaderName, this.baggageInHeader }, - }; - - var context = HttpContextHelper.GetFakeHttpContext(headers: requestHeaders); - this.EnableAspNetListenerAndActivity(); - var rootActivity = ActivityHelper.CreateRootActivity(context, true); + Assert.Equal(ActivityHelper.StartedButNotSampledObj, context.Items[ActivityHelper.ContextKey]); - Assert.NotNull(rootActivity); - Assert.True(rootActivity.ParentId == "|aba2f1e978b2cab6.1."); - var expectedBaggage = this.baggageItems.OrderBy(item => item.Value); - var actualBaggage = rootActivity.Baggage.OrderBy(item => item.Value); - Assert.Equal(expectedBaggage, actualBaggage); + ActivityHelper.StopAspNetActivity(this.noopTextMapPropagator, rootActivity, context, null); + Assert.Null(context.Items[ActivityHelper.ContextKey]); } [Fact] public void Can_Create_RootActivity_From_W3C_Traceparent() { - this.EnableAll(); + this.EnableListener(); var requestHeaders = new Dictionary { - { ActivityExtensions.TraceparentHeaderName, "00-0123456789abcdef0123456789abcdef-0123456789abcdef-00" }, + { TraceParentHeaderName, "00-0123456789abcdef0123456789abcdef-0123456789abcdef-00" }, }; var context = HttpContextHelper.GetFakeHttpContext(headers: requestHeaders); - this.EnableAspNetListenerAndActivity(); - var rootActivity = ActivityHelper.CreateRootActivity(context, true); + using var rootActivity = ActivityHelper.StartAspNetActivity(new TraceContextPropagator(), context, null); Assert.NotNull(rootActivity); Assert.Equal(ActivityIdFormat.W3C, rootActivity.IdFormat); Assert.Equal("00-0123456789abcdef0123456789abcdef-0123456789abcdef-00", rootActivity.ParentId); Assert.Equal("0123456789abcdef0123456789abcdef", rootActivity.TraceId.ToHexString()); Assert.Equal("0123456789abcdef", rootActivity.ParentSpanId.ToHexString()); - Assert.False(rootActivity.Recorded); + Assert.True(rootActivity.Recorded); // note: We're not using a parent-based sampler in this test so the recorded flag of traceparent is ignored. Assert.Null(rootActivity.TraceStateString); Assert.Empty(rootActivity.Baggage); + + Assert.Equal(0, Baggage.Current.Count); } [Fact] public void Can_Create_RootActivityWithTraceState_From_W3C_TraceContext() { - this.EnableAll(); + this.EnableListener(); var requestHeaders = new Dictionary { - { ActivityExtensions.TraceparentHeaderName, "00-0123456789abcdef0123456789abcdef-0123456789abcdef-01" }, - { ActivityExtensions.TracestateHeaderName, "ts1=v1,ts2=v2" }, + { TraceParentHeaderName, "00-0123456789abcdef0123456789abcdef-0123456789abcdef-01" }, + { TraceStateHeaderName, "ts1=v1,ts2=v2" }, }; var context = HttpContextHelper.GetFakeHttpContext(headers: requestHeaders); - this.EnableAspNetListenerAndActivity(); - var rootActivity = ActivityHelper.CreateRootActivity(context, true); + using var rootActivity = ActivityHelper.StartAspNetActivity(new TraceContextPropagator(), context, null); Assert.NotNull(rootActivity); Assert.Equal(ActivityIdFormat.W3C, rootActivity.IdFormat); @@ -392,34 +370,48 @@ public void Can_Create_RootActivityWithTraceState_From_W3C_TraceContext() Assert.Equal("ts1=v1,ts2=v2", rootActivity.TraceStateString); Assert.Empty(rootActivity.Baggage); + + Assert.Equal(0, Baggage.Current.Count); } [Fact] - public void Can_Create_RootActivity_And_Ignore_Info_From_Request_Header_If_ParseHeaders_Is_False() + public void Can_Create_RootActivity_From_W3C_Traceparent_With_Baggage() { - this.EnableAll(); + this.EnableListener(); var requestHeaders = new Dictionary { - { ActivityExtensions.RequestIdHeaderName, "|aba2f1e978b2cab6.1." }, - { ActivityExtensions.CorrelationContextHeaderName, this.baggageInHeader }, + { TraceParentHeaderName, "00-0123456789abcdef0123456789abcdef-0123456789abcdef-00" }, + { BaggageHeaderName, BaggageInHeader }, }; var context = HttpContextHelper.GetFakeHttpContext(headers: requestHeaders); - this.EnableAspNetListenerAndActivity(); - var rootActivity = ActivityHelper.CreateRootActivity(context, parseHeaders: false); + using var rootActivity = ActivityHelper.StartAspNetActivity(new CompositeTextMapPropagator(new TextMapPropagator[] { new TraceContextPropagator(), new BaggagePropagator() }), context, null); Assert.NotNull(rootActivity); - Assert.Null(rootActivity.ParentId); + Assert.Equal(ActivityIdFormat.W3C, rootActivity.IdFormat); + Assert.Equal("00-0123456789abcdef0123456789abcdef-0123456789abcdef-00", rootActivity.ParentId); + Assert.Equal("0123456789abcdef0123456789abcdef", rootActivity.TraceId.ToHexString()); + Assert.Equal("0123456789abcdef", rootActivity.ParentSpanId.ToHexString()); + Assert.True(rootActivity.Recorded); // note: We're not using a parent-based sampler in this test so the recorded flag of traceparent is ignored. + + Assert.Null(rootActivity.TraceStateString); Assert.Empty(rootActivity.Baggage); + + Assert.Equal(2, Baggage.Current.Count); + Assert.Equal("789", Baggage.Current.GetBaggage("TestKey1")); + Assert.Equal("456", Baggage.Current.GetBaggage("TestKey2")); + + ActivityHelper.StopAspNetActivity(this.noopTextMapPropagator, rootActivity, context, null); + + Assert.Equal(0, Baggage.Current.Count); } [Fact] public void Can_Create_RootActivity_And_Start_Activity() { - this.EnableAll(); + this.EnableListener(); var context = HttpContextHelper.GetFakeHttpContext(); - this.EnableAspNetListenerAndActivity(); - var rootActivity = ActivityHelper.CreateRootActivity(context, true); + using var rootActivity = ActivityHelper.StartAspNetActivity(this.noopTextMapPropagator, context, null); Assert.NotNull(rootActivity); Assert.True(!string.IsNullOrEmpty(rootActivity.Id)); @@ -428,83 +420,52 @@ public void Can_Create_RootActivity_And_Start_Activity() [Fact] public void Can_Create_RootActivity_And_Saved_In_HttContext() { - this.EnableAll(); + this.EnableListener(); var context = HttpContextHelper.GetFakeHttpContext(); - this.EnableAspNetListenerAndActivity(); - var rootActivity = ActivityHelper.CreateRootActivity(context, true); + using var rootActivity = ActivityHelper.StartAspNetActivity(this.noopTextMapPropagator, context, null); Assert.NotNull(rootActivity); - Assert.Same(rootActivity, context.Items[ActivityHelper.ActivityKey]); + Assert.Same(rootActivity, ((ActivityHelper.ContextHolder)context.Items[ActivityHelper.ContextKey])?.Activity); } - private Activity CreateActivity() + [Fact] + public void Fire_Exception_Events() { - var activity = new Activity(TestActivityName); - this.baggageItems.ForEach(kv => activity.AddBaggage(kv.Key, kv.Value)); + int callbacksFired = 0; - return activity; - } + var context = HttpContextHelper.GetFakeHttpContext(); - private void EnableAll(Action> onNext = null, Action onImport = null) - { - this.subscriptionAllListeners = DiagnosticListener.AllListeners.Subscribe(listener => - { - // if AspNetListener has subscription, then it is enabled - if (listener.Name == ActivityHelper.AspNetListenerName) - { - this.subscriptionAspNetListener = listener.Subscribe( - new TestDiagnosticListener(onNext), - (name, a1, a2) => true, - (a, o) => onImport?.Invoke(a, o), - (a, o) => { }); - } - }); - } + Activity activity = new Activity(TestActivityName); - private void EnableAspNetListenerAndDisableActivity( - Action> onNext = null, - string activityName = ActivityHelper.AspNetActivityName) - { - this.subscriptionAllListeners = DiagnosticListener.AllListeners.Subscribe(listener => - { - // if AspNetListener has subscription, then it is enabled - if (listener.Name == ActivityHelper.AspNetListenerName) - { - this.subscriptionAspNetListener = listener.Subscribe( - new TestDiagnosticListener(onNext), - (name, arg1, arg2) => name == activityName && arg1 == null); - } - }); - } + ActivityHelper.WriteActivityException(activity, context, new InvalidOperationException(), (a, c, e) => { callbacksFired++; }); - private void EnableAspNetListenerAndActivity( - Action> onNext = null, - string activityName = ActivityHelper.AspNetActivityName) - { - this.subscriptionAllListeners = DiagnosticListener.AllListeners.Subscribe(listener => - { - // if AspNetListener has subscription, then it is enabled - if (listener.Name == ActivityHelper.AspNetListenerName) - { - this.subscriptionAspNetListener = listener.Subscribe( - new TestDiagnosticListener(onNext), - (name, arg1, arg2) => name == activityName); - } - }); + ActivityHelper.WriteActivityException(null, context, new InvalidOperationException(), (a, c, e) => { callbacksFired++; }); + + // Callback should fire only for non-null activity + Assert.Equal(1, callbacksFired); } - private void EnableAspNetListenerOnly(Action> onNext = null) + private void EnableListener(Action onStarted = null, Action onStopped = null, Func onSample = null) { - this.subscriptionAllListeners = DiagnosticListener.AllListeners.Subscribe(listener => + Debug.Assert(this.activitySourceListener == null, "Cannot attach multiple listeners in tests."); + + this.activitySourceListener = new ActivityListener { - // if AspNetListener has subscription, then it is enabled - if (listener.Name == ActivityHelper.AspNetListenerName) + ShouldListenTo = (activitySource) => activitySource.Name == TelemetryHttpModule.AspNetSourceName, + ActivityStarted = (a) => onStarted?.Invoke(a), + ActivityStopped = (a) => onStopped?.Invoke(a), + Sample = (ref ActivityCreationOptions options) => { - this.subscriptionAspNetListener = listener.Subscribe( - new TestDiagnosticListener(onNext), - activityName => false); - } - }); + if (onSample != null) + { + return onSample(options.Parent); + } + + return ActivitySamplingResult.AllDataAndRecorded; + }, + }; + + ActivitySource.AddActivityListener(this.activitySourceListener); } private class TestHttpRequest : HttpRequestBase @@ -565,5 +526,21 @@ public TestHttpContext(Exception error = null) public override HttpServerUtilityBase Server { get; } } + + private class NoopTextMapPropagator : TextMapPropagator + { + private static readonly PropagationContext DefaultPropagationContext = default; + + public override ISet Fields => null; + + public override PropagationContext Extract(PropagationContext context, T carrier, Func> getter) + { + return DefaultPropagationContext; + } + + public override void Inject(PropagationContext context, T carrier, Action setter) + { + } + } } } diff --git a/test/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.Tests/HttpContextHelper.cs b/test/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.Tests/HttpContextHelper.cs index 0c0e2827803..ebb5ede4f05 100644 --- a/test/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.Tests/HttpContextHelper.cs +++ b/test/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.Tests/HttpContextHelper.cs @@ -89,7 +89,7 @@ public override string GetUnknownRequestHeader(string name) public override string GetKnownRequestHeader(int index) { - var name = HttpWorkerRequest.GetKnownRequestHeaderName(index); + var name = GetKnownRequestHeaderName(index); if (this.headers.ContainsKey(name)) { diff --git a/test/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.Tests/TestDiagnosticListener.cs b/test/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.Tests/TestDiagnosticListener.cs deleted file mode 100644 index 4b29e2ab9f2..00000000000 --- a/test/OpenTelemetry.Instrumentation.AspNet.TelemetryHttpModule.Tests/TestDiagnosticListener.cs +++ /dev/null @@ -1,44 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -namespace OpenTelemetry.Instrumentation.AspNet.Tests -{ - using System; - using System.Collections.Generic; - - internal class TestDiagnosticListener : IObserver> - { - private readonly Action> onNextCallBack; - - public TestDiagnosticListener(Action> onNext) - { - this.onNextCallBack = onNext; - } - - public void OnCompleted() - { - } - - public void OnError(Exception error) - { - } - - public void OnNext(KeyValuePair value) - { - this.onNextCallBack?.Invoke(value); - } - } -} diff --git a/test/OpenTelemetry.Instrumentation.AspNet.Tests/HttpInListenerTests.cs b/test/OpenTelemetry.Instrumentation.AspNet.Tests/HttpInListenerTests.cs index 543d6b44f8b..465c3192173 100644 --- a/test/OpenTelemetry.Instrumentation.AspNet.Tests/HttpInListenerTests.cs +++ b/test/OpenTelemetry.Instrumentation.AspNet.Tests/HttpInListenerTests.cs @@ -31,49 +31,34 @@ namespace OpenTelemetry.Instrumentation.AspNet.Tests { - public class HttpInListenerTests : IDisposable + public class HttpInListenerTests { - private readonly FakeAspNetDiagnosticSource fakeAspNetDiagnosticSource; - - public HttpInListenerTests() - { - this.fakeAspNetDiagnosticSource = new FakeAspNetDiagnosticSource(); - } - - public void Dispose() - { - this.fakeAspNetDiagnosticSource.Dispose(); - } - [Theory] - [InlineData("http://localhost/", "http://localhost/", 0, null, "TraceContext")] - [InlineData("http://localhost/", "http://localhost/", 0, null, "TraceContext", true)] - [InlineData("https://localhost/", "https://localhost/", 0, null, "TraceContext")] - [InlineData("https://localhost/", "https://user:pass@localhost/", 0, null, "TraceContext")] // Test URL sanitization - [InlineData("http://localhost:443/", "http://localhost:443/", 0, null, "TraceContext")] // Test http over 443 - [InlineData("https://localhost:80/", "https://localhost:80/", 0, null, "TraceContext")] // Test https over 80 - [InlineData("https://localhost:80/Home/Index.htm?q1=v1&q2=v2#FragmentName", "https://localhost:80/Home/Index.htm?q1=v1&q2=v2#FragmentName", 0, null, "TraceContext")] // Test complex URL - [InlineData("https://localhost:80/Home/Index.htm?q1=v1&q2=v2#FragmentName", "https://user:password@localhost:80/Home/Index.htm?q1=v1&q2=v2#FragmentName", 0, null, "TraceContext")] // Test complex URL sanitization - [InlineData("http://localhost:80/Index", "http://localhost:80/Index", 1, "{controller}/{action}/{id}", "TraceContext")] - [InlineData("https://localhost:443/about_attr_route/10", "https://localhost:443/about_attr_route/10", 2, "about_attr_route/{customerId}", "TraceContext")] - [InlineData("http://localhost:1880/api/weatherforecast", "http://localhost:1880/api/weatherforecast", 3, "api/{controller}/{id}", "TraceContext")] - [InlineData("https://localhost:1843/subroute/10", "https://localhost:1843/subroute/10", 4, "subroute/{customerId}", "TraceContext")] - [InlineData("http://localhost/api/value", "http://localhost/api/value", 0, null, "TraceContext", false, "/api/value")] // Request will be filtered - [InlineData("http://localhost/api/value", "http://localhost/api/value", 0, null, "TraceContext", false, "{ThrowException}")] // Filter user code will throw an exception - [InlineData("http://localhost/api/value/2", "http://localhost/api/value/2", 0, null, "CustomContextMatchParent")] - [InlineData("http://localhost/api/value/2", "http://localhost/api/value/2", 0, null, "CustomContextNonmatchParent")] - [InlineData("http://localhost/api/value/2", "http://localhost/api/value/2", 0, null, "CustomContextNonmatchParent", false, null, true)] + [InlineData("http://localhost/", "http://localhost/", 0, null)] + [InlineData("http://localhost/", "http://localhost/", 0, null, true)] + [InlineData("https://localhost/", "https://localhost/", 0, null)] + [InlineData("https://localhost/", "https://user:pass@localhost/", 0, null)] // Test URL sanitization + [InlineData("http://localhost:443/", "http://localhost:443/", 0, null)] // Test http over 443 + [InlineData("https://localhost:80/", "https://localhost:80/", 0, null)] // Test https over 80 + [InlineData("https://localhost:80/Home/Index.htm?q1=v1&q2=v2#FragmentName", "https://localhost:80/Home/Index.htm?q1=v1&q2=v2#FragmentName", 0, null)] // Test complex URL + [InlineData("https://localhost:80/Home/Index.htm?q1=v1&q2=v2#FragmentName", "https://user:password@localhost:80/Home/Index.htm?q1=v1&q2=v2#FragmentName", 0, null)] // Test complex URL sanitization + [InlineData("http://localhost:80/Index", "http://localhost:80/Index", 1, "{controller}/{action}/{id}")] + [InlineData("https://localhost:443/about_attr_route/10", "https://localhost:443/about_attr_route/10", 2, "about_attr_route/{customerId}")] + [InlineData("http://localhost:1880/api/weatherforecast", "http://localhost:1880/api/weatherforecast", 3, "api/{controller}/{id}")] + [InlineData("https://localhost:1843/subroute/10", "https://localhost:1843/subroute/10", 4, "subroute/{customerId}")] + [InlineData("http://localhost/api/value", "http://localhost/api/value", 0, null, false, "/api/value")] // Request will be filtered + [InlineData("http://localhost/api/value", "http://localhost/api/value", 0, null, false, "{ThrowException}")] // Filter user code will throw an exception + [InlineData("http://localhost/", "http://localhost/", 0, null, false, null, true)] // Test RecordException option public void AspNetRequestsAreCollectedSuccessfully( string expectedUrl, string url, int routeType, string routeTemplate, - string carrierFormat, bool setStatusToErrorInEnrich = false, string filter = null, - bool restoreCurrentActivity = false) + bool recordException = false) { - IDisposable openTelemetry = null; + IDisposable tracerProvider = null; RouteData routeData; switch (routeType) { @@ -130,28 +115,11 @@ public void AspNetRequestsAreCollectedSuccessfully( typeof(HttpRequest).GetField("_wr", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(HttpContext.Current.Request, workerRequest.Object); - var expectedTraceId = ActivityTraceId.CreateRandom(); - var expectedSpanId = ActivitySpanId.CreateRandom(); - var propagator = new Mock(); - propagator.Setup(m => m.Extract(It.IsAny(), It.IsAny(), It.IsAny>>())).Returns(new PropagationContext( - new ActivityContext( - expectedTraceId, - expectedSpanId, - ActivityTraceFlags.Recorded, - isRemote: true), - default)); - - var activity = new Activity(HttpInListener.ActivityOperationName); - if (carrierFormat == "TraceContext" || carrierFormat == "CustomContextMatchParent") - { - activity.SetParentId(expectedTraceId, expectedSpanId, ActivityTraceFlags.Recorded); - } + List exportedItems = new List(16); - var activityProcessor = new Mock>(); - Sdk.SetDefaultTextMapPropagator(propagator.Object); - using (openTelemetry = Sdk.CreateTracerProviderBuilder() - .AddAspNetInstrumentation( - (options) => + Sdk.SetDefaultTextMapPropagator(new TraceContextPropagator()); + using (tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetInstrumentation((options) => { options.Filter = httpContext => { @@ -177,109 +145,43 @@ public void AspNetRequestsAreCollectedSuccessfully( { options.Enrich = GetEnrichmentAction(default); } + + options.RecordException = recordException; }) - .AddProcessor(activityProcessor.Object).Build()) + .AddInMemoryExporter(exportedItems) + .Build()) { - activity.Start(); + using var inMemoryEventListener = new InMemoryEventListener(AspNetInstrumentationEventSource.Log); - using (var inMemoryEventListener = new InMemoryEventListener(AspNetInstrumentationEventSource.Log)) - { - this.fakeAspNetDiagnosticSource.Write("Start", null); + var activity = ActivityHelper.StartAspNetActivity(Propagators.DefaultTextMapPropagator, HttpContext.Current, TelemetryHttpModule.Options.OnRequestStartedCallback); - if (filter == "{ThrowException}") - { - Assert.Single(inMemoryEventListener.Events.Where((e) => e.EventId == 3)); - } + if (filter == "{ThrowException}") + { + Assert.Single(inMemoryEventListener.Events.Where((e) => e.EventId == 2)); } - if (restoreCurrentActivity) + Assert.Equal(TelemetryHttpModule.AspNetActivityName, Activity.Current.OperationName); + + if (recordException) { - Activity.Current = activity; + ActivityHelper.WriteActivityException(activity, HttpContext.Current, new InvalidOperationException(), TelemetryHttpModule.Options.OnExceptionCallback); } - this.fakeAspNetDiagnosticSource.Write("Stop", null); - - // The above line fires DS event which is listened by Instrumentation. - // Validate that Current activity is still the one created by Asp.Net - Assert.Equal(HttpInListener.ActivityOperationName, Activity.Current.OperationName); - activity.Stop(); + ActivityHelper.StopAspNetActivity(Propagators.DefaultTextMapPropagator, activity, HttpContext.Current, TelemetryHttpModule.Options.OnRequestStoppedCallback); } if (HttpContext.Current.Request.Path == filter || filter == "{ThrowException}") { - // only SetParentProvider/Shutdown/Dispose/OnStart are called because request was filtered. - Assert.Equal(4, activityProcessor.Invocations.Count); + Assert.Empty(exportedItems); return; } - // Validate that Activity.Current is always the one created by Asp.Net - var currentActivity = Activity.Current; - - Activity span; - if (carrierFormat == "CustomContextNonmatchParent") - { - Assert.Equal(6, activityProcessor.Invocations.Count); // SetParentProvider/OnStart(framework activity)/OnStart(sibling activity)/OnEnd(sibling activity)/OnShutdown/Dispose called. - - var startedActivities = activityProcessor.Invocations.Where(invo => invo.Method.Name == "OnStart"); - var stoppedActivities = activityProcessor.Invocations.Where(invo => invo.Method.Name == "OnEnd"); - Assert.Equal(2, startedActivities.Count()); - Assert.Single(stoppedActivities); - - // The activity created by the framework and the sibling activity are both sent to Processor.OnStart - Assert.Contains(startedActivities, item => - { - var startedActivity = item.Arguments[0] as Activity; - return startedActivity.OperationName == HttpInListener.ActivityOperationName; - }); - - Assert.Contains(startedActivities, item => - { - var startedActivity = item.Arguments[0] as Activity; - return startedActivity.OperationName == HttpInListener.ActivityNameByHttpInListener; - }); - - // Only the sibling activity is sent to Processor.OnEnd - Assert.Contains(stoppedActivities, item => - { - var stoppedActivity = item.Arguments[0] as Activity; - return stoppedActivity.OperationName == HttpInListener.ActivityNameByHttpInListener; - }); - } - else - { - Assert.Equal(5, activityProcessor.Invocations.Count); // SetParentProvider/OnStart/OnEnd/OnShutdown/Dispose called. - - var startedActivities = activityProcessor.Invocations.Where(invo => invo.Method.Name == "OnStart"); - var stoppedActivities = activityProcessor.Invocations.Where(invo => invo.Method.Name == "OnEnd"); - - // There is no sibling activity created - Assert.Single(startedActivities); - Assert.Single(stoppedActivities); - - Assert.Contains(startedActivities, item => - { - var startedActivity = item.Arguments[0] as Activity; - return startedActivity.OperationName == HttpInListener.ActivityOperationName; - }); - - // Only the sibling activity is sent to Processor.OnEnd - Assert.Contains(stoppedActivities, item => - { - var stoppedActivity = item.Arguments[0] as Activity; - return stoppedActivity.OperationName == HttpInListener.ActivityOperationName; - }); - } + Assert.Single(exportedItems); - span = (Activity)activityProcessor.Invocations[2].Arguments[0]; + Activity span = exportedItems[0]; - Assert.Equal( - carrierFormat == "TraceContext" || carrierFormat == "CustomContextMatchParent" - ? HttpInListener.ActivityOperationName - : HttpInListener.ActivityNameByHttpInListener, - span.OperationName); + Assert.Equal(TelemetryHttpModule.AspNetActivityName, span.OperationName); Assert.NotEqual(TimeSpan.Zero, span.Duration); - Assert.Equal(expectedTraceId, span.TraceId); - Assert.Equal(expectedSpanId, span.ParentSpanId); Assert.Equal(routeTemplate ?? HttpContext.Current.Request.Path, span.DisplayName); Assert.Equal(ActivityKind.Server, span.Kind); @@ -287,25 +189,6 @@ public void AspNetRequestsAreCollectedSuccessfully( Assert.Equal(200, span.GetTagValue(SemanticConventions.AttributeHttpStatusCode)); - if (setStatusToErrorInEnrich) - { - // This validates that users can override the - // status in Enrich. - Assert.Equal(Status.Error, span.GetStatus()); - - // Instrumentation is not expected to set status description - // as the reason can be inferred from SemanticConventions.AttributeHttpStatusCode - Assert.True(string.IsNullOrEmpty(span.GetStatus().Description)); - } - else - { - Assert.Equal(Status.Unset, span.GetStatus()); - - // Instrumentation is not expected to set status description - // as the reason can be inferred from SemanticConventions.AttributeHttpStatusCode - Assert.True(string.IsNullOrEmpty(span.GetStatus().Description)); - } - var expectedUri = new Uri(expectedUrl); var actualUrl = span.GetTagValue(SemanticConventions.AttributeHttpUrl); @@ -336,15 +219,39 @@ public void AspNetRequestsAreCollectedSuccessfully( } Assert.Equal(HttpContext.Current.Request.HttpMethod, span.GetTagValue(SemanticConventions.AttributeHttpMethod) as string); - Assert.Equal(HttpContext.Current.Request.Path, span.GetTagValue(SpanAttributeConstants.HttpPathKey) as string); + Assert.Equal(HttpContext.Current.Request.Path, span.GetTagValue(SemanticConventions.AttributeHttpTarget) as string); Assert.Equal(HttpContext.Current.Request.UserAgent, span.GetTagValue(SemanticConventions.AttributeHttpUserAgent) as string); + + if (recordException) + { + var status = span.GetStatus(); + Assert.Equal(Status.Error.StatusCode, status.StatusCode); + Assert.Equal("Operation is not valid due to the current state of the object.", status.Description); + } + else if (setStatusToErrorInEnrich) + { + // This validates that users can override the + // status in Enrich. + Assert.Equal(Status.Error, span.GetStatus()); + + // Instrumentation is not expected to set status description + // as the reason can be inferred from SemanticConventions.AttributeHttpStatusCode + Assert.True(string.IsNullOrEmpty(span.GetStatus().Description)); + } + else + { + Assert.Equal(Status.Unset, span.GetStatus()); + + // Instrumentation is not expected to set status description + // as the reason can be inferred from SemanticConventions.AttributeHttpStatusCode + Assert.True(string.IsNullOrEmpty(span.GetStatus().Description)); + } } [Theory] [InlineData(SamplingDecision.Drop)] [InlineData(SamplingDecision.RecordOnly)] [InlineData(SamplingDecision.RecordAndSample)] - public void ExtractContextIrrespectiveOfSamplingDecision(SamplingDecision samplingDecision) { HttpContext.Current = new HttpContext( @@ -363,27 +270,18 @@ public void ExtractContextIrrespectiveOfSamplingDecision(SamplingDecision sampli .Returns(() => { isPropagatorCalled = true; - return default(PropagationContext); + return default; }); - var activity = new Activity(HttpInListener.ActivityOperationName); - var activityProcessor = new Mock>(); Sdk.SetDefaultTextMapPropagator(propagator.Object); - using (var openTelemetry = Sdk.CreateTracerProviderBuilder() + using (var tracerProvider = Sdk.CreateTracerProviderBuilder() .SetSampler(new TestSampler(samplingDecision)) .AddAspNetInstrumentation() .AddProcessor(activityProcessor.Object).Build()) { - activity.Start(); - - using (var inMemoryEventListener = new InMemoryEventListener(AspNetInstrumentationEventSource.Log)) - { - this.fakeAspNetDiagnosticSource.Write("Start", null); - } - - this.fakeAspNetDiagnosticSource.Write("Stop", null); - activity.Stop(); + var activity = ActivityHelper.StartAspNetActivity(Propagators.DefaultTextMapPropagator, HttpContext.Current, TelemetryHttpModule.Options.OnRequestStartedCallback); + ActivityHelper.StopAspNetActivity(Propagators.DefaultTextMapPropagator, activity, HttpContext.Current, TelemetryHttpModule.Options.OnRequestStoppedCallback); } Assert.True(isPropagatorCalled); @@ -408,15 +306,13 @@ public void ExtractContextIrrespectiveOfTheFilterApplied() .Returns(() => { isPropagatorCalled = true; - return default(PropagationContext); + return default; }); - var activity = new Activity(HttpInListener.ActivityOperationName); - bool isFilterCalled = false; var activityProcessor = new Mock>(); Sdk.SetDefaultTextMapPropagator(propagator.Object); - using (var openTelemetry = Sdk.CreateTracerProviderBuilder() + using (var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddAspNetInstrumentation(options => { options.Filter = context => @@ -427,15 +323,8 @@ public void ExtractContextIrrespectiveOfTheFilterApplied() }) .AddProcessor(activityProcessor.Object).Build()) { - activity.Start(); - - using (var inMemoryEventListener = new InMemoryEventListener(AspNetInstrumentationEventSource.Log)) - { - this.fakeAspNetDiagnosticSource.Write("Start", null); - } - - this.fakeAspNetDiagnosticSource.Write("Stop", null); - activity.Stop(); + var activity = ActivityHelper.StartAspNetActivity(Propagators.DefaultTextMapPropagator, HttpContext.Current, TelemetryHttpModule.Options.OnRequestStartedCallback); + ActivityHelper.StopAspNetActivity(Propagators.DefaultTextMapPropagator, activity, HttpContext.Current, TelemetryHttpModule.Options.OnRequestStoppedCallback); } Assert.True(isFilterCalled); @@ -444,9 +333,7 @@ public void ExtractContextIrrespectiveOfTheFilterApplied() private static Action GetEnrichmentAction(Status statusToBeSet) { - Action enrichAction; - - enrichAction = (activity, method, obj) => + void EnrichAction(Activity activity, string method, object obj) { Assert.True(activity.IsAllDataRequested); switch (method) @@ -467,34 +354,14 @@ private static Action GetEnrichmentAction(Status statu default: break; } - }; - - return enrichAction; - } - - private class FakeAspNetDiagnosticSource : IDisposable - { - private readonly DiagnosticListener listener; - - public FakeAspNetDiagnosticSource() - { - this.listener = new DiagnosticListener(AspNetInstrumentation.AspNetDiagnosticListenerName); - } - - public void Write(string name, object value) - { - this.listener.Write(name, value); } - public void Dispose() - { - this.listener.Dispose(); - } + return EnrichAction; } private class TestSampler : Sampler { - private SamplingDecision samplingDecision; + private readonly SamplingDecision samplingDecision; public TestSampler(SamplingDecision samplingDecision) { diff --git a/test/OpenTelemetry.Instrumentation.AspNet.Tests/OpenTelemetry.Instrumentation.AspNet.Tests.csproj b/test/OpenTelemetry.Instrumentation.AspNet.Tests/OpenTelemetry.Instrumentation.AspNet.Tests.csproj index 011b8c50816..f2b8fd8edc8 100644 --- a/test/OpenTelemetry.Instrumentation.AspNet.Tests/OpenTelemetry.Instrumentation.AspNet.Tests.csproj +++ b/test/OpenTelemetry.Instrumentation.AspNet.Tests/OpenTelemetry.Instrumentation.AspNet.Tests.csproj @@ -16,13 +16,18 @@ + - - - + + + + + + + diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs index 5fb47ca84cb..db77e75cc2c 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/BasicTests.cs @@ -31,21 +31,26 @@ using OpenTelemetry.Instrumentation.AspNetCore.Implementation; using OpenTelemetry.Tests; using OpenTelemetry.Trace; + #if NETCOREAPP3_1 using TestApp.AspNetCore._3._1; -#else +#endif +#if NET5_0 using TestApp.AspNetCore._5._0; #endif +#if NET6_0 +using TestApp.AspNetCore._6._0; +#endif using Xunit; namespace OpenTelemetry.Instrumentation.AspNetCore.Tests { // See https://github.com/aspnet/Docs/tree/master/aspnetcore/test/integration-tests/samples/2.x/IntegrationTestsSample - public class BasicTests + public sealed class BasicTests : IClassFixture>, IDisposable { private readonly WebApplicationFactory factory; - private TracerProvider openTelemetrySdk = null; + private TracerProvider tracerProvider = null; public BasicTests(WebApplicationFactory factory) { @@ -65,7 +70,7 @@ public async Task StatusIsUnsetOn200Response() var activityProcessor = new Mock>(); void ConfigureTestServices(IServiceCollection services) { - this.openTelemetrySdk = Sdk.CreateTracerProviderBuilder() + this.tracerProvider = Sdk.CreateTracerProviderBuilder() .AddAspNetCoreInstrumentation() .AddProcessor(activityProcessor.Object) .Build(); @@ -109,7 +114,7 @@ public async Task SuccessfulTemplateControllerCallGeneratesASpan(bool shouldEnri activityProcessor.Setup(x => x.OnStart(It.IsAny())).Callback(c => c.SetTag("enriched", "no")); void ConfigureTestServices(IServiceCollection services) { - this.openTelemetrySdk = Sdk.CreateTracerProviderBuilder() + this.tracerProvider = Sdk.CreateTracerProviderBuilder() .AddAspNetCoreInstrumentation(options => { if (shouldEnrich) @@ -158,7 +163,7 @@ public async Task SuccessfulTemplateControllerCallUsesParentContext() .WithWebHostBuilder(builder => builder.ConfigureTestServices(services => { - this.openTelemetrySdk = Sdk.CreateTracerProviderBuilder().AddAspNetCoreInstrumentation() + this.tracerProvider = Sdk.CreateTracerProviderBuilder().AddAspNetCoreInstrumentation() .AddProcessor(activityProcessor.Object) .Build(); }))) @@ -220,7 +225,7 @@ public async Task CustomPropagator() builder.ConfigureTestServices(services => { Sdk.SetDefaultTextMapPropagator(propagator.Object); - this.openTelemetrySdk = Sdk.CreateTracerProviderBuilder() + this.tracerProvider = Sdk.CreateTracerProviderBuilder() .AddAspNetCoreInstrumentation() .AddProcessor(activityProcessor.Object) .Build(); @@ -281,7 +286,7 @@ public async Task RequestNotCollectedWhenFilterIsApplied() void ConfigureTestServices(IServiceCollection services) { - this.openTelemetrySdk = Sdk.CreateTracerProviderBuilder() + this.tracerProvider = Sdk.CreateTracerProviderBuilder() .AddAspNetCoreInstrumentation((opt) => opt.Filter = (ctx) => ctx.Request.Path != "/api/values/2") .AddProcessor(activityProcessor.Object) .Build(); @@ -325,7 +330,7 @@ public async Task RequestNotCollectedWhenFilterThrowException() void ConfigureTestServices(IServiceCollection services) { - this.openTelemetrySdk = Sdk.CreateTracerProviderBuilder() + this.tracerProvider = Sdk.CreateTracerProviderBuilder() .AddAspNetCoreInstrumentation((opt) => opt.Filter = (ctx) => { if (ctx.Request.Path == "/api/values/2") @@ -398,7 +403,7 @@ public async Task ExtractContextIrrespectiveOfSamplingDecision(SamplingDecision .WithWebHostBuilder(builder => builder.ConfigureTestServices(services => { - this.openTelemetrySdk = Sdk.CreateTracerProviderBuilder() + this.tracerProvider = Sdk.CreateTracerProviderBuilder() .SetSampler(new TestSampler(samplingDecision)) .AddAspNetCoreInstrumentation() .Build(); @@ -457,7 +462,7 @@ public async Task ExtractContextIrrespectiveOfTheFilterApplied() .WithWebHostBuilder(builder => builder.ConfigureTestServices(services => { - this.openTelemetrySdk = Sdk.CreateTracerProviderBuilder() + this.tracerProvider = Sdk.CreateTracerProviderBuilder() .AddAspNetCoreInstrumentation(options => { options.Filter = context => @@ -508,6 +513,55 @@ public async Task ExtractContextIrrespectiveOfTheFilterApplied() } } + [Fact] + public async Task BaggageClearedWhenActivityStopped() + { + int? baggageCountAfterStart = null; + int? baggageCountAfterStop = null; + using EventWaitHandle stopSignal = new EventWaitHandle(false, EventResetMode.ManualReset); + + void ConfigureTestServices(IServiceCollection services) + { + this.tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddAspNetCoreInstrumentation(new AspNetCoreInstrumentation( + new TestHttpInListener(new AspNetCoreInstrumentationOptions()) + { + OnStartActivityCallback = (activity, payload) => + { + baggageCountAfterStart = Baggage.Current.Count; + }, + OnStopActivityCallback = (activity, payload) => + { + baggageCountAfterStop = Baggage.Current.Count; + stopSignal.Set(); + }, + })) + .Build(); + } + + // Arrange + using (var client = this.factory + .WithWebHostBuilder(builder => + builder.ConfigureTestServices(ConfigureTestServices)) + .CreateClient()) + { + using var request = new HttpRequestMessage(HttpMethod.Get, "/api/values"); + + request.Headers.TryAddWithoutValidation("baggage", "TestKey1=123,TestKey2=456"); + + // Act + using var response = await client.SendAsync(request); + } + + stopSignal.WaitOne(5000); + + // Assert + Assert.NotNull(baggageCountAfterStart); + Assert.Equal(2, baggageCountAfterStart); + Assert.NotNull(baggageCountAfterStop); + Assert.Equal(0, baggageCountAfterStop); + } + [Theory] [InlineData(SamplingDecision.Drop, false, false)] [InlineData(SamplingDecision.RecordOnly, true, true)] @@ -518,7 +572,7 @@ public async Task FilterAndEnrichAreOnlyCalledWhenSampled(SamplingDecision sampl bool enrichCalled = false; void ConfigureTestServices(IServiceCollection services) { - this.openTelemetrySdk = Sdk.CreateTracerProviderBuilder() + this.tracerProvider = Sdk.CreateTracerProviderBuilder() .SetSampler(new TestSampler(samplingDecision)) .AddAspNetCoreInstrumentation(options => { @@ -551,7 +605,7 @@ void ConfigureTestServices(IServiceCollection services) public void Dispose() { - this.openTelemetrySdk?.Dispose(); + this.tracerProvider?.Dispose(); } private static void WaitForProcessorInvocations(Mock> activityProcessor, int invocationCount) @@ -573,7 +627,7 @@ private static void ValidateAspNetCoreActivity(Activity activityToValidate, stri Assert.Equal(ActivityKind.Server, activityToValidate.Kind); Assert.Equal(HttpInListener.ActivitySourceName, activityToValidate.Source.Name); Assert.Equal(HttpInListener.Version.ToString(), activityToValidate.Source.Version); - Assert.Equal(expectedHttpPath, activityToValidate.GetTagValue(SpanAttributeConstants.HttpPathKey) as string); + Assert.Equal(expectedHttpPath, activityToValidate.GetTagValue(SemanticConventions.AttributeHttpTarget) as string); } private static void ActivityEnrichment(Activity activity, string method, object obj) @@ -622,7 +676,7 @@ public override void Inject(PropagationContext context, T carrier, Action OnStartActivityCallback; + + public Action OnStopActivityCallback; + + public TestHttpInListener(AspNetCoreInstrumentationOptions options) + : base(options) + { + } + + public override void OnStartActivity(Activity activity, object payload) + { + base.OnStartActivity(activity, payload); + + this.OnStartActivityCallback?.Invoke(activity, payload); + } + + public override void OnStopActivity(Activity activity, object payload) + { + base.OnStopActivity(activity, payload); + + this.OnStopActivityCallback?.Invoke(activity, payload); + } + } } } diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/DependencyInjectionConfigTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/DependencyInjectionConfigTests.cs index cce05338e77..9441adfebbd 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/DependencyInjectionConfigTests.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/DependencyInjectionConfigTests.cs @@ -20,9 +20,13 @@ using OpenTelemetry.Trace; #if NETCOREAPP3_1 using TestApp.AspNetCore._3._1; -#else +#endif +#if NET5_0 using TestApp.AspNetCore._5._0; #endif +#if NET6_0 +using TestApp.AspNetCore._6._0; +#endif using Xunit; namespace OpenTelemetry.Instrumentation.AspNetCore.Tests diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests.cs index 8f5ba8bd95c..e13174041ff 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/IncomingRequestsCollectionsIsAccordingToTheSpecTests.cs @@ -27,9 +27,13 @@ using OpenTelemetry.Trace; #if NETCOREAPP3_1 using TestApp.AspNetCore._3._1; -#else +#endif +#if NET5_0 using TestApp.AspNetCore._5._0; #endif +#if NET6_0 +using TestApp.AspNetCore._6._0; +#endif using Xunit; namespace OpenTelemetry.Instrumentation.AspNetCore.Tests @@ -104,7 +108,7 @@ public async Task SuccessfulTemplateControllerCallGeneratesASpan( Assert.Equal(ActivityKind.Server, activity.Kind); Assert.Equal("localhost", activity.GetTagValue(SemanticConventions.AttributeHttpHost)); Assert.Equal("GET", activity.GetTagValue(SemanticConventions.AttributeHttpMethod)); - Assert.Equal(urlPath, activity.GetTagValue(SpanAttributeConstants.HttpPathKey)); + Assert.Equal(urlPath, activity.GetTagValue(SemanticConventions.AttributeHttpTarget)); Assert.Equal($"http://localhost{urlPath}", activity.GetTagValue(SemanticConventions.AttributeHttpUrl)); Assert.Equal(statusCode, activity.GetTagValue(SemanticConventions.AttributeHttpStatusCode)); diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/MetricTests.cs b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/MetricTests.cs index 8250204ef75..bfeda28b387 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/MetricTests.cs +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/MetricTests.cs @@ -17,17 +17,20 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Testing; +using OpenTelemetry.Exporter; using OpenTelemetry.Metrics; -using OpenTelemetry.Tests; using OpenTelemetry.Trace; #if NETCOREAPP3_1 using TestApp.AspNetCore._3._1; -#else +#endif +#if NET5_0 using TestApp.AspNetCore._5._0; #endif +#if NET6_0 +using TestApp.AspNetCore._6._0; +#endif using Xunit; namespace OpenTelemetry.Instrumentation.AspNetCore.Tests @@ -53,21 +56,16 @@ public void AddAspNetCoreInstrumentation_BadArgs() [Fact] public async Task RequestMetricIsCaptured() { - var metricItems = new List(); - var metricExporter = new TestExporter(ProcessExport); + var metricItems = new List(); + var metricExporter = new InMemoryExporter(metricItems); - void ProcessExport(Batch batch) + var metricReader = new BaseExportingMetricReader(metricExporter) { - foreach (var metricItem in batch) - { - metricItems.Add(metricItem); - } - } - - var processor = new PullMetricProcessor(metricExporter, true); + Temporality = AggregationTemporality.Cumulative, + }; this.meterProvider = Sdk.CreateMeterProviderBuilder() .AddAspNetCoreInstrumentation() - .AddMetricProcessor(processor) + .AddReader(metricReader) .Build(); using (var client = this.factory.CreateClient()) @@ -81,46 +79,64 @@ void ProcessExport(Batch batch) // giving some breezing room for the End callback to complete await Task.Delay(TimeSpan.FromSeconds(1)); - // Invokes the TestExporter which will invoke ProcessExport - processor.PullRequest(); - this.meterProvider.Dispose(); var requestMetrics = metricItems - .SelectMany(item => item.Metrics.Where(metric => metric.Name == "http.server.request_count")) + .Where(item => item.Name == "http.server.duration") .ToArray(); Assert.True(requestMetrics.Length == 1); - var metric = requestMetrics[0] as ISumMetricLong; + var metric = requestMetrics[0]; Assert.NotNull(metric); - Assert.Equal(1L, metric.LongSum); + Assert.True(metric.MetricType == MetricType.Histogram); + var metricPoints = new List(); + foreach (var p in metric.GetMetricPoints()) + { + metricPoints.Add(p); + } + + Assert.Single(metricPoints); + + var metricPoint = metricPoints[0]; + + var count = metricPoint.GetHistogramCount(); + var sum = metricPoint.GetHistogramSum(); + + Assert.Equal(1L, count); + Assert.True(sum > 0); + + /* + var bucket = metric.Buckets + .Where(b => + metric.PopulationSum > b.LowBoundary && + metric.PopulationSum <= b.HighBoundary) + .FirstOrDefault(); + Assert.NotEqual(default, bucket); + Assert.Equal(1, bucket.Count); + */ + + var attributes = new KeyValuePair[metricPoint.Tags.Count]; + int i = 0; + foreach (var tag in metricPoint.Tags) + { + attributes[i++] = tag; + } var method = new KeyValuePair(SemanticConventions.AttributeHttpMethod, "GET"); var scheme = new KeyValuePair(SemanticConventions.AttributeHttpScheme, "http"); var statusCode = new KeyValuePair(SemanticConventions.AttributeHttpStatusCode, 200); var flavor = new KeyValuePair(SemanticConventions.AttributeHttpFlavor, "HTTP/1.1"); - Assert.Contains(method, metric.Attributes); - Assert.Contains(scheme, metric.Attributes); - Assert.Contains(statusCode, metric.Attributes); - Assert.Contains(flavor, metric.Attributes); - Assert.Equal(4, metric.Attributes.Length); + Assert.Contains(method, attributes); + Assert.Contains(scheme, attributes); + Assert.Contains(statusCode, attributes); + Assert.Contains(flavor, attributes); + Assert.Equal(4, attributes.Length); } public void Dispose() { this.meterProvider?.Dispose(); } - - private static void WaitForMetricItems(List metricItems, int count) - { - Assert.True(SpinWait.SpinUntil( - () => - { - Thread.Sleep(10); - return metricItems.Count >= count; - }, - TimeSpan.FromSeconds(1))); - } } } diff --git a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/OpenTelemetry.Instrumentation.AspNetCore.Tests.csproj b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/OpenTelemetry.Instrumentation.AspNetCore.Tests.csproj index a035f5da834..e22014f544d 100644 --- a/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/OpenTelemetry.Instrumentation.AspNetCore.Tests.csproj +++ b/test/OpenTelemetry.Instrumentation.AspNetCore.Tests/OpenTelemetry.Instrumentation.AspNetCore.Tests.csproj @@ -1,7 +1,7 @@  Unit test project for OpenTelemetry ASP.NET Core instrumentation - netcoreapp3.1;net5.0 + netcoreapp3.1;net5.0;net6.0 @@ -16,11 +16,20 @@ + + + + + - + + + + + diff --git a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcServer.cs b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcServer.cs index 3757726a165..9e408647f5c 100644 --- a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcServer.cs +++ b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcServer.cs @@ -33,7 +33,7 @@ public class GrpcServer : IDisposable public GrpcServer() { // Allows gRPC client to call insecure gRPC services - // https://docs.microsoft.com/en-us/aspnet/core/grpc/troubleshoot?view=aspnetcore-3.1#call-insecure-grpc-services-with-net-core-client + // https://docs.microsoft.com/aspnet/core/grpc/troubleshoot?view=aspnetcore-3.1#call-insecure-grpc-services-with-net-core-client AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); this.Port = 0; diff --git a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTests.server.cs b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTests.server.cs index 08c3e0290e1..a0c23d72f57 100644 --- a/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTests.server.cs +++ b/test/OpenTelemetry.Instrumentation.Grpc.Tests/GrpcTests.server.cs @@ -107,7 +107,7 @@ public void GrpcAspNetCoreInstrumentationAddsCorrectAttributes(bool? enableGrpcA // The following are http.* attributes that are also included on the span for the gRPC invocation. Assert.Equal($"localhost:{this.server.Port}", activity.GetTagValue(SemanticConventions.AttributeHttpHost)); Assert.Equal("POST", activity.GetTagValue(SemanticConventions.AttributeHttpMethod)); - Assert.Equal("/greet.Greeter/SayHello", activity.GetTagValue(SpanAttributeConstants.HttpPathKey)); + Assert.Equal("/greet.Greeter/SayHello", activity.GetTagValue(SemanticConventions.AttributeHttpTarget)); Assert.Equal($"http://localhost:{this.server.Port}/greet.Greeter/SayHello", activity.GetTagValue(SemanticConventions.AttributeHttpUrl)); Assert.StartsWith("grpc-dotnet", activity.GetTagValue(SemanticConventions.AttributeHttpUserAgent) as string); } @@ -182,7 +182,7 @@ public void GrpcAspNetCoreInstrumentationAddsCorrectAttributesWhenItCreatesNewAc // The following are http.* attributes that are also included on the span for the gRPC invocation. Assert.Equal($"localhost:{this.server.Port}", activity.GetTagValue(SemanticConventions.AttributeHttpHost)); Assert.Equal("POST", activity.GetTagValue(SemanticConventions.AttributeHttpMethod)); - Assert.Equal("/greet.Greeter/SayHello", activity.GetTagValue(SpanAttributeConstants.HttpPathKey)); + Assert.Equal("/greet.Greeter/SayHello", activity.GetTagValue(SemanticConventions.AttributeHttpTarget)); Assert.Equal($"http://localhost:{this.server.Port}/greet.Greeter/SayHello", activity.GetTagValue(SemanticConventions.AttributeHttpUrl)); Assert.StartsWith("grpc-dotnet", activity.GetTagValue(SemanticConventions.AttributeHttpUserAgent) as string); } diff --git a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpClientTests.Basic.netcore31.cs b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpClientTests.Basic.netcore31.cs index 9083d5145d2..33c85c9277a 100644 --- a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpClientTests.Basic.netcore31.cs +++ b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpClientTests.Basic.netcore31.cs @@ -375,11 +375,11 @@ public async Task HttpClientInstrumentationContextPropagation() .Start(); parent.TraceStateString = "k1=v1,k2=v2"; parent.ActivityTraceFlags = ActivityTraceFlags.Recorded; - Baggage.Current.SetBaggage("b1", "v1"); + Baggage.SetBaggage("b1", "v1"); using (Sdk.CreateTracerProviderBuilder() - .AddHttpClientInstrumentation() - .AddProcessor(processor.Object) - .Build()) + .AddHttpClientInstrumentation() + .AddProcessor(processor.Object) + .Build()) { using var c = new HttpClient(); await c.SendAsync(request); diff --git a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpClientTests.netcore31.cs b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpClientTests.netcore31.cs index 937300c6876..a843e9fab4b 100644 --- a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpClientTests.netcore31.cs +++ b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpClientTests.netcore31.cs @@ -25,6 +25,7 @@ using System.Threading.Tasks; using Moq; using Newtonsoft.Json; +using OpenTelemetry.Exporter; using OpenTelemetry.Metrics; using OpenTelemetry.Tests; using OpenTelemetry.Trace; @@ -54,21 +55,16 @@ public async Task HttpOutCallsAreCollectedSuccessfullyAsync(HttpTestData.HttpOut var processor = new Mock>(); tc.Url = HttpTestData.NormalizeValues(tc.Url, host, port); - var metricItems = new List(); - var metricExporter = new TestExporter(ProcessExport); + var metricItems = new List(); + var metricExporter = new InMemoryExporter(metricItems); - void ProcessExport(Batch batch) + var metricReader = new BaseExportingMetricReader(metricExporter) { - foreach (var metricItem in batch) - { - metricItems.Add(metricItem); - } - } - - var metricProcessor = new PullMetricProcessor(metricExporter, true); + Temporality = AggregationTemporality.Cumulative, + }; var meterProvider = Sdk.CreateMeterProviderBuilder() .AddHttpClientInstrumentation() - .AddMetricProcessor(metricProcessor) + .AddReader(metricReader) .Build(); using (serverLifeTime) @@ -109,13 +105,10 @@ void ProcessExport(Batch batch) } } - // Invokes the TestExporter which will invoke ProcessExport - metricProcessor.PullRequest(); - meterProvider.Dispose(); var requestMetrics = metricItems - .SelectMany(item => item.Metrics.Where(metric => metric.Name == "http.client.duration")) + .Where(metric => metric.Name == "http.client.duration") .ToArray(); Assert.Equal(5, processor.Invocations.Count); // SetParentProvider/OnStart/OnEnd/OnShutdown/Dispose called. @@ -154,20 +147,41 @@ void ProcessExport(Batch batch) { Assert.Single(requestMetrics); - var metric = requestMetrics[0] as IHistogramMetric; + var metric = requestMetrics[0]; Assert.NotNull(metric); - Assert.Equal(1L, metric.PopulationCount); - Assert.Equal(activity.Duration.TotalMilliseconds, metric.PopulationSum); + Assert.True(metric.MetricType == MetricType.Histogram); + + var metricPoints = new List(); + foreach (var p in metric.GetMetricPoints()) + { + metricPoints.Add(p); + } + + Assert.Single(metricPoints); + var metricPoint = metricPoints[0]; + + var count = metricPoint.GetHistogramCount(); + var sum = metricPoint.GetHistogramSum(); + + Assert.Equal(1L, count); + Assert.Equal(activity.Duration.TotalMilliseconds, sum); + + var attributes = new KeyValuePair[metricPoint.Tags.Count]; + int i = 0; + foreach (var tag in metricPoint.Tags) + { + attributes[i++] = tag; + } var method = new KeyValuePair(SemanticConventions.AttributeHttpMethod, tc.Method); var scheme = new KeyValuePair(SemanticConventions.AttributeHttpScheme, "http"); var statusCode = new KeyValuePair(SemanticConventions.AttributeHttpStatusCode, tc.ResponseCode == 0 ? 200 : tc.ResponseCode); var flavor = new KeyValuePair(SemanticConventions.AttributeHttpFlavor, "2.0"); - Assert.Contains(method, metric.Attributes); - Assert.Contains(scheme, metric.Attributes); - Assert.Contains(statusCode, metric.Attributes); - Assert.Contains(flavor, metric.Attributes); - Assert.Equal(4, metric.Attributes.Length); + Assert.Contains(method, attributes); + Assert.Contains(scheme, attributes); + Assert.Contains(statusCode, attributes); + Assert.Contains(flavor, attributes); + Assert.Equal(4, attributes.Length); } else { diff --git a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestTests.Basic.netfx.cs b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestTests.Basic.netfx.cs index db2ec6ddd2c..620438e262e 100644 --- a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestTests.Basic.netfx.cs +++ b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestTests.Basic.netfx.cs @@ -61,7 +61,7 @@ public void Dispose() public async Task HttpWebRequestInstrumentationInjectsHeadersAsync() { var activityProcessor = new Mock>(); - using var shutdownSignal = Sdk.CreateTracerProviderBuilder() + using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddProcessor(activityProcessor.Object) .AddHttpClientInstrumentation() .Build(); @@ -108,7 +108,7 @@ public async Task HttpWebRequestInstrumentationInjectsHeadersAsyncWhenActivityIs }); Sdk.SetDefaultTextMapPropagator(propagator.Object); - using var shutdownSignal = Sdk.CreateTracerProviderBuilder() + using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddProcessor(activityProcessor.Object) .AddHttpClientInstrumentation() .Build(); @@ -155,7 +155,7 @@ public async Task HttpWebRequestInstrumentationInjectsHeadersAsync_CustomFormat( var activityProcessor = new Mock>(); Sdk.SetDefaultTextMapPropagator(propagator.Object); - using var shutdownSignal = Sdk.CreateTracerProviderBuilder() + using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddProcessor(activityProcessor.Object) .AddHttpClientInstrumentation() .Build(); @@ -199,7 +199,7 @@ public async Task HttpWebRequestInstrumentationInjectsHeadersAsync_CustomFormat( public async Task HttpWebRequestInstrumentationBacksOffIfAlreadyInstrumented() { var activityProcessor = new Mock>(); - using var shutdownSignal = Sdk.CreateTracerProviderBuilder() + using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddProcessor(activityProcessor.Object) .AddHttpClientInstrumentation() .Build(); @@ -222,7 +222,7 @@ public async Task HttpWebRequestInstrumentationBacksOffIfAlreadyInstrumented() public async Task RequestNotCollectedWhenInstrumentationFilterApplied() { var activityProcessor = new Mock>(); - using var shutdownSignal = Sdk.CreateTracerProviderBuilder() + using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddProcessor(activityProcessor.Object) .AddHttpClientInstrumentation( c => c.Filter = (req) => !req.RequestUri.OriginalString.Contains(this.url)) @@ -238,7 +238,7 @@ public async Task RequestNotCollectedWhenInstrumentationFilterApplied() public async Task RequestNotCollectedWhenInstrumentationFilterThrowsException() { var activityProcessor = new Mock>(); - using var shutdownSignal = Sdk.CreateTracerProviderBuilder() + using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddProcessor(activityProcessor.Object) .AddHttpClientInstrumentation( c => c.Filter = (req) => throw new Exception("From Instrumentation filter")) @@ -256,7 +256,7 @@ public async Task RequestNotCollectedWhenInstrumentationFilterThrowsException() public void AddHttpClientInstrumentationUsesHttpWebRequestInstrumentationOptions() { var activityProcessor = new Mock>(); - using var tracerProviderSdk = Sdk.CreateTracerProviderBuilder() + using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddProcessor(activityProcessor.Object) .AddHttpClientInstrumentation(options => { @@ -264,6 +264,24 @@ public void AddHttpClientInstrumentationUsesHttpWebRequestInstrumentationOptions }) .Build(); } + + [Fact] + public async Task HttpWebRequestInstrumentationOnExistingInstance() + { + using HttpClient client = new HttpClient(); + + await client.GetAsync(this.url).ConfigureAwait(false); + + var activityProcessor = new Mock>(); + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .AddProcessor(activityProcessor.Object) + .AddHttpClientInstrumentation() + .Build(); + + await client.GetAsync(this.url).ConfigureAwait(false); + + Assert.Equal(3, activityProcessor.Invocations.Count); // SetParentProvider/Begin/End called + } } } #endif diff --git a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestTests.netfx.cs b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestTests.netfx.cs index 16faa12974c..14c3347da41 100644 --- a/test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestTests.netfx.cs +++ b/test/OpenTelemetry.Instrumentation.Http.Tests/HttpWebRequestTests.netfx.cs @@ -46,7 +46,7 @@ public void HttpOutCallsAreCollectedSuccessfully(HttpTestData.HttpOutTestCase tc out var port); var activityProcessor = new Mock>(); - using var shutdownSignal = Sdk.CreateTracerProviderBuilder() + using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddProcessor(activityProcessor.Object) .AddHttpClientInstrumentation(options => { diff --git a/test/OpenTelemetry.Instrumentation.Http.Tests/OpenTelemetry.Instrumentation.Http.Tests.csproj b/test/OpenTelemetry.Instrumentation.Http.Tests/OpenTelemetry.Instrumentation.Http.Tests.csproj index ce177accfe2..db3e3d3ca6d 100644 --- a/test/OpenTelemetry.Instrumentation.Http.Tests/OpenTelemetry.Instrumentation.Http.Tests.csproj +++ b/test/OpenTelemetry.Instrumentation.Http.Tests/OpenTelemetry.Instrumentation.Http.Tests.csproj @@ -1,7 +1,7 @@  Unit test project for OpenTelemetry HTTP instrumentations - netcoreapp3.1;net5.0 + netcoreapp3.1;net5.0;net6.0 $(TargetFrameworks);net461 @@ -34,6 +34,7 @@ + diff --git a/test/OpenTelemetry.Instrumentation.SqlClient.Tests/dockerfile b/test/OpenTelemetry.Instrumentation.SqlClient.Tests/Dockerfile similarity index 78% rename from test/OpenTelemetry.Instrumentation.SqlClient.Tests/dockerfile rename to test/OpenTelemetry.Instrumentation.SqlClient.Tests/Dockerfile index bff388e4847..b6952d6bdbc 100644 --- a/test/OpenTelemetry.Instrumentation.SqlClient.Tests/dockerfile +++ b/test/OpenTelemetry.Instrumentation.SqlClient.Tests/Dockerfile @@ -1,11 +1,11 @@ -# Create a container for running the OpenTelemetry SQL Client integration tests. +# Create a container for running the OpenTelemetry SQL Client integration tests. # This should be run from the root of the repo: -# docker build --file test/OpenTelemetry.Instrumentation.SqlClient.Tests/dockerfile . +# docker build --file test/OpenTelemetry.Instrumentation.SqlClient.Tests/Dockerfile . -ARG SDK_VERSION=5.0 -FROM mcr.microsoft.com/dotnet/sdk:5.0-focal AS build +ARG SDK_VERSION=6.0 +FROM mcr.microsoft.com/dotnet/sdk:6.0-focal AS build ARG PUBLISH_CONFIGURATION=Release -ARG PUBLISH_FRAMEWORK=net5.0 +ARG PUBLISH_FRAMEWORK=net6.0 WORKDIR /repo COPY . ./ RUN ls -la /repo diff --git a/test/OpenTelemetry.Instrumentation.SqlClient.Tests/OpenTelemetry.Instrumentation.SqlClient.Tests.csproj b/test/OpenTelemetry.Instrumentation.SqlClient.Tests/OpenTelemetry.Instrumentation.SqlClient.Tests.csproj index dcb9b2d8c6e..16e6f3d2b88 100644 --- a/test/OpenTelemetry.Instrumentation.SqlClient.Tests/OpenTelemetry.Instrumentation.SqlClient.Tests.csproj +++ b/test/OpenTelemetry.Instrumentation.SqlClient.Tests/OpenTelemetry.Instrumentation.SqlClient.Tests.csproj @@ -1,7 +1,7 @@  Unit test project for OpenTelemetry SqlClient instrumentations - netcoreapp3.1;net5.0 + netcoreapp3.1;net5.0;net6.0 $(TargetFrameworks);net461 $(TARGET_FRAMEWORK) diff --git a/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlClientTests.cs b/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlClientTests.cs index d4ab1966c2e..aa313d0717d 100644 --- a/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlClientTests.cs +++ b/test/OpenTelemetry.Instrumentation.SqlClient.Tests/SqlClientTests.cs @@ -84,7 +84,7 @@ public void SuccessfulCommandTest( var activityProcessor = new Mock>(); activityProcessor.Setup(x => x.OnStart(It.IsAny())).Callback(c => c.SetTag("enriched", "no")); var sampler = new TestSampler(); - using var shutdownSignal = Sdk.CreateTracerProviderBuilder() + using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddProcessor(activityProcessor.Object) .SetSampler(sampler) .AddSqlClientInstrumentation(options => diff --git a/test/OpenTelemetry.Instrumentation.SqlClient.Tests/docker-compose.yml b/test/OpenTelemetry.Instrumentation.SqlClient.Tests/docker-compose.yml index 6c8a9e99802..2c4b2b2763a 100644 --- a/test/OpenTelemetry.Instrumentation.SqlClient.Tests/docker-compose.yml +++ b/test/OpenTelemetry.Instrumentation.SqlClient.Tests/docker-compose.yml @@ -16,10 +16,10 @@ services: tests: build: context: . - dockerfile: ./test/OpenTelemetry.Instrumentation.SqlClient.Tests/dockerfile + dockerfile: ./test/OpenTelemetry.Instrumentation.SqlClient.Tests/Dockerfile entrypoint: ["bash", "-c", "/wait && dotnet vstest OpenTelemetry.Instrumentation.SqlClient.Tests.dll --TestCaseFilter:CategoryName=SqlIntegrationTests"] environment: - OTEL_SQLCONNECTIONSTRING=Data Source=sql; User ID=sa; Password=Pass@word18 - WAIT_HOSTS=sql:1433 depends_on: - - sql \ No newline at end of file + - sql diff --git a/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/dockerfile b/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/Dockerfile similarity index 75% rename from test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/dockerfile rename to test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/Dockerfile index bc600583062..5f3f9f866c2 100644 --- a/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/dockerfile +++ b/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/Dockerfile @@ -1,11 +1,11 @@ -# Create a container for running the OpenTelemetry Redis integration tests. +# Create a container for running the OpenTelemetry Redis integration tests. # This should be run from the root of the repo: -# docker build --file test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/dockerfile . +# docker build --file test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/Dockerfile . -ARG SDK_VERSION=5.0 -FROM mcr.microsoft.com/dotnet/sdk:5.0-focal AS build +ARG SDK_VERSION=6.0 +FROM mcr.microsoft.com/dotnet/sdk:6.0-focal AS build ARG PUBLISH_CONFIGURATION=Release -ARG PUBLISH_FRAMEWORK=net5.0 +ARG PUBLISH_FRAMEWORK=net6.0 WORKDIR /repo COPY . ./ WORKDIR "/repo/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests" diff --git a/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/Implementation/RedisProfilerEntryToActivityConverterTests.cs b/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/Implementation/RedisProfilerEntryToActivityConverterTests.cs index a385124addb..373b8d66be0 100644 --- a/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/Implementation/RedisProfilerEntryToActivityConverterTests.cs +++ b/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/Implementation/RedisProfilerEntryToActivityConverterTests.cs @@ -30,7 +30,7 @@ namespace OpenTelemetry.Instrumentation.StackExchangeRedis.Implementation public class RedisProfilerEntryToActivityConverterTests : IDisposable { private readonly ConnectionMultiplexer connection; - private readonly IDisposable sdk; + private readonly IDisposable tracerProvider; public RedisProfilerEntryToActivityConverterTests() { @@ -42,14 +42,14 @@ public RedisProfilerEntryToActivityConverterTests() this.connection = ConnectionMultiplexer.Connect(connectionOptions); - this.sdk = Sdk.CreateTracerProviderBuilder() + this.tracerProvider = Sdk.CreateTracerProviderBuilder() .AddRedisInstrumentation(this.connection) .Build(); } public void Dispose() { - this.sdk.Dispose(); + this.tracerProvider.Dispose(); this.connection.Dispose(); } diff --git a/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests.csproj b/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests.csproj index 4fac60d2ced..9a74fca6ba4 100644 --- a/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests.csproj +++ b/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests.csproj @@ -1,7 +1,7 @@  Unit test project for OpenTelemetry StackExchangeRedis instrumentation - netcoreapp3.1;net5.0 + netcoreapp3.1;net5.0;net6.0 $(TargetFrameworks);net461 $(TARGET_FRAMEWORK) diff --git a/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/docker-compose.yml b/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/docker-compose.yml index 03bfe366cdb..7004e679c00 100644 --- a/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/docker-compose.yml +++ b/test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/docker-compose.yml @@ -12,9 +12,9 @@ services: tests: build: context: . - dockerfile: ./test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/dockerfile + dockerfile: ./test/OpenTelemetry.Instrumentation.StackExchangeRedis.Tests/Dockerfile command: --TestCaseFilter:CategoryName=RedisIntegrationTests environment: - OTEL_REDISENDPOINT=redis:6379 depends_on: - - redis \ No newline at end of file + - redis diff --git a/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/Dockerfile b/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/Dockerfile index aa0a0cf0026..cacdd48cc03 100644 --- a/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/Dockerfile +++ b/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/Dockerfile @@ -1,17 +1,17 @@ -# Create a container for running the OpenTelemetry W3C Trace Context tests https://github.com/w3c/trace-context/tree/master/test. +# Create a container for running the OpenTelemetry W3C Trace Context tests https://github.com/w3c/trace-context/tree/master/test. # This should be run from the root of the repo: # docker build --file test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/Dockerfile . -ARG SDK_VERSION=5.0 +ARG SDK_VERSION=6.0 FROM ubuntu AS w3c #Install git WORKDIR /w3c RUN apt-get update && apt-get install -y git RUN git clone https://github.com/w3c/trace-context.git -FROM mcr.microsoft.com/dotnet/sdk:5.0-focal AS build +FROM mcr.microsoft.com/dotnet/sdk:6.0-focal AS build ARG PUBLISH_CONFIGURATION=Release -ARG PUBLISH_FRAMEWORK=net5.0 +ARG PUBLISH_FRAMEWORK=net6.0 WORKDIR /repo COPY . ./ WORKDIR "/repo/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests" diff --git a/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/InProcessServer.cs b/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/InProcessServer.cs index 390ad86f563..94d46d294b3 100644 --- a/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/InProcessServer.cs +++ b/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/InProcessServer.cs @@ -20,13 +20,15 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using OpenTelemetry.Trace; -#if NETCOREAPP2_1 -using TestApp.AspNetCore._2._1; -#elif NETCOREAPP3_1 +#if NETCOREAPP3_1 using TestApp.AspNetCore._3._1; -#else +#endif +#if NET5_0 using TestApp.AspNetCore._5._0; #endif +#if NET6_0 +using TestApp.AspNetCore._6._0; +#endif using Xunit.Abstractions; namespace OpenTelemetry.Instrumentation.W3cTraceContext.Tests 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 edd45e8eb74..135e433c85f 100644 --- a/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/OpenTelemetry.Instrumentation.W3cTraceContext.Tests.csproj +++ b/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/OpenTelemetry.Instrumentation.W3cTraceContext.Tests.csproj @@ -2,7 +2,7 @@ Unit test project for OpenTelemetry ASP.NET Core instrumentation for W3C Trace Context Trace - netcoreapp3.1;net5.0 + netcoreapp3.1;net5.0;net6.0 $(TARGET_FRAMEWORK) @@ -28,6 +28,10 @@ + + + + diff --git a/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/docker-compose.yml b/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/docker-compose.yml index 05412932f2e..7c421786f5c 100644 --- a/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/docker-compose.yml +++ b/test/OpenTelemetry.Instrumentation.W3cTraceContext.Tests/docker-compose.yml @@ -1,4 +1,4 @@ -# Start a container and then run OpenTelemetry W3C Trace Context tests. +# 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' 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 ba27221aa47..880f3d7f1e7 100644 --- a/test/OpenTelemetry.Shims.OpenTracing.Tests/OpenTelemetry.Shims.OpenTracing.Tests.csproj +++ b/test/OpenTelemetry.Shims.OpenTracing.Tests/OpenTelemetry.Shims.OpenTracing.Tests.csproj @@ -1,7 +1,7 @@  Unit test project for OpenTelemetry.Shims.OpenTracing - netcoreapp3.1;net5.0 + netcoreapp3.1;net5.0;net6.0 diff --git a/test/OpenTelemetry.Shims.OpenTracing.Tests/ScopeManagerShimTests.cs b/test/OpenTelemetry.Shims.OpenTracing.Tests/ScopeManagerShimTests.cs index 1a79e2f3a1d..8ac7f2fd2ee 100644 --- a/test/OpenTelemetry.Shims.OpenTracing.Tests/ScopeManagerShimTests.cs +++ b/test/OpenTelemetry.Shims.OpenTracing.Tests/ScopeManagerShimTests.cs @@ -78,7 +78,7 @@ public void Activate_SpanMustBeShim() var tracer = TracerProvider.Default.GetTracer(TracerName); var shim = new ScopeManagerShim(tracer); - Assert.Throws(() => shim.Activate(new Mock().Object, true)); + Assert.Throws(() => shim.Activate(new Mock().Object, true)); } [Fact] diff --git a/test/OpenTelemetry.Shims.OpenTracing.Tests/SpanBuilderShimTests.cs b/test/OpenTelemetry.Shims.OpenTracing.Tests/SpanBuilderShimTests.cs index 8ee3a9d85d9..42625e14783 100644 --- a/test/OpenTelemetry.Shims.OpenTracing.Tests/SpanBuilderShimTests.cs +++ b/test/OpenTelemetry.Shims.OpenTracing.Tests/SpanBuilderShimTests.cs @@ -122,7 +122,7 @@ public void Start_ActivityOperationRootSpanChecks() { // Create an activity var activity = new Activity("foo") - .SetIdFormat(System.Diagnostics.ActivityIdFormat.W3C) + .SetIdFormat(ActivityIdFormat.W3C) .Start(); // matching root operation name diff --git a/test/OpenTelemetry.Shims.OpenTracing.Tests/SpanShimTests.cs b/test/OpenTelemetry.Shims.OpenTracing.Tests/SpanShimTests.cs index eba243588f6..ad2d68c680b 100644 --- a/test/OpenTelemetry.Shims.OpenTracing.Tests/SpanShimTests.cs +++ b/test/OpenTelemetry.Shims.OpenTracing.Tests/SpanShimTests.cs @@ -18,8 +18,8 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using global::OpenTracing.Tag; using OpenTelemetry.Trace; +using OpenTracing.Tag; using Xunit; namespace OpenTelemetry.Shims.OpenTracing.Tests @@ -102,7 +102,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 } @@ -225,7 +225,7 @@ public void SetTagBoolValue() Assert.Throws(() => shim.SetTag((string)null, true)); shim.SetTag("foo", true); - shim.SetTag(global::OpenTracing.Tag.Tags.Error.Key, 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); @@ -233,7 +233,7 @@ public void SetTagBoolValue() // A boolean tag named "error" is a special case that must be checked Assert.Equal(Status.Error, shim.Span.Activity.GetStatus()); - shim.SetTag(global::OpenTracing.Tag.Tags.Error.Key, false); + shim.SetTag(Tags.Error.Key, false); Assert.Equal(Status.Ok, shim.Span.Activity.GetStatus()); } @@ -276,7 +276,7 @@ public void SetTagBooleanTagValue() Assert.Throws(() => shim.SetTag((BooleanTag)null, true)); shim.SetTag(new BooleanTag("foo"), true); - shim.SetTag(new BooleanTag(global::OpenTracing.Tag.Tags.Error.Key), 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); @@ -284,7 +284,7 @@ public void SetTagBooleanTagValue() // A boolean tag named "error" is a special case that must be checked Assert.Equal(Status.Error, shim.Span.Activity.GetStatus()); - shim.SetTag(global::OpenTracing.Tag.Tags.Error.Key, false); + shim.SetTag(Tags.Error.Key, false); Assert.Equal(Status.Ok, shim.Span.Activity.GetStatus()); } diff --git a/test/OpenTelemetry.Shims.OpenTracing.Tests/TracerShimTests.cs b/test/OpenTelemetry.Shims.OpenTracing.Tests/TracerShimTests.cs index f24ccd922f0..f47bf6566a3 100644 --- a/test/OpenTelemetry.Shims.OpenTracing.Tests/TracerShimTests.cs +++ b/test/OpenTelemetry.Shims.OpenTracing.Tests/TracerShimTests.cs @@ -77,7 +77,7 @@ public void Inject_ArgumentValidation() var mockCarrier = new Mock(); Assert.Throws(() => shim.Inject(null, mockFormat.Object, mockCarrier.Object)); - Assert.Throws(() => shim.Inject(new Mock().Object, mockFormat.Object, mockCarrier.Object)); + Assert.Throws(() => shim.Inject(new Mock().Object, mockFormat.Object, mockCarrier.Object)); Assert.Throws(() => shim.Inject(spanContextShim, null, mockCarrier.Object)); Assert.Throws(() => shim.Inject(spanContextShim, mockFormat.Object, null)); } diff --git a/test/OpenTelemetry.Tests.Stress.Metrics/OpenTelemetry.Tests.Stress.Metrics.csproj b/test/OpenTelemetry.Tests.Stress.Metrics/OpenTelemetry.Tests.Stress.Metrics.csproj new file mode 100644 index 00000000000..76b65c6a682 --- /dev/null +++ b/test/OpenTelemetry.Tests.Stress.Metrics/OpenTelemetry.Tests.Stress.Metrics.csproj @@ -0,0 +1,19 @@ + + + + Exe + netcoreapp3.1;net5.0;net6.0;net462 + false + + + + + + + + + + + + + diff --git a/test/OpenTelemetry.Tests.Stress.Metrics/Program.cs b/test/OpenTelemetry.Tests.Stress.Metrics/Program.cs new file mode 100644 index 00000000000..869a02fca55 --- /dev/null +++ b/test/OpenTelemetry.Tests.Stress.Metrics/Program.cs @@ -0,0 +1,63 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Diagnostics.Metrics; +using System.Runtime.CompilerServices; +using System.Threading; +using OpenTelemetry.Metrics; + +namespace OpenTelemetry.Tests.Stress; + +public partial class Program +{ + private const int ArraySize = 10; + private static readonly Meter TestMeter = new Meter(Utils.GetCurrentMethodName()); + private static readonly Counter TestCounter = TestMeter.CreateCounter("TestCounter"); + private static readonly string[] DimensionValues = new string[ArraySize]; + private static readonly ThreadLocal ThreadLocalRandom = new ThreadLocal(() => new Random()); + + public static void Main() + { + for (int i = 0; i < ArraySize; i++) + { + DimensionValues[i] = $"DimValue{i}"; + } + + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(TestMeter.Name) + .AddPrometheusExporter(options => + { + options.StartHttpListener = true; + options.HttpListenerPrefixes = new string[] { $"http://localhost:9185/" }; + options.ScrapeResponseCacheDurationMilliseconds = 0; + }) + .Build(); + + Stress(prometheusPort: 9184); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected static void Run() + { + var random = ThreadLocalRandom.Value; + TestCounter.Add( + 100, + new("DimName1", DimensionValues[random.Next(0, ArraySize)]), + new("DimName2", DimensionValues[random.Next(0, ArraySize)]), + new("DimName3", DimensionValues[random.Next(0, ArraySize)])); + } +} diff --git a/test/OpenTelemetry.Tests.Stress.Metrics/README.md b/test/OpenTelemetry.Tests.Stress.Metrics/README.md new file mode 100644 index 00000000000..4652a241381 --- /dev/null +++ b/test/OpenTelemetry.Tests.Stress.Metrics/README.md @@ -0,0 +1,14 @@ +# OpenTelemetry Stress Tests for Metrics + +This is Stress Test specifically for Metrics SDK, and is +based on the [OpenTelemetry.Tests.Stress](../OpenTelemetry.Tests.Stress/README.md). + +* [Running the stress test](#running-the-stress-test) + +## Running the stress test + +Open a console, run the following command from the current folder: + +```sh +dotnet run --framework net6.0 --configuration Release +``` diff --git a/src/OpenTelemetry/Metrics/MetricAggregators/ISummaryMetric.cs b/test/OpenTelemetry.Tests.Stress/Meat.cs similarity index 64% rename from src/OpenTelemetry/Metrics/MetricAggregators/ISummaryMetric.cs rename to test/OpenTelemetry.Tests.Stress/Meat.cs index fce320de831..6959c8259f1 100644 --- a/src/OpenTelemetry/Metrics/MetricAggregators/ISummaryMetric.cs +++ b/test/OpenTelemetry.Tests.Stress/Meat.cs @@ -1,4 +1,4 @@ -// +// // Copyright The OpenTelemetry Authors // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,16 +14,19 @@ // limitations under the License. // -using System.Collections.Generic; +using System.Runtime.CompilerServices; -namespace OpenTelemetry.Metrics +namespace OpenTelemetry.Tests.Stress; + +public partial class Program { - public interface ISummaryMetric : IMetric + public static void Main() { - long PopulationCount { get; } - - double PopulationSum { get; } + Stress(concurrency: 1, prometheusPort: 9184); + } - IEnumerable Quantiles { get; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected static void Run() + { } } diff --git a/test/OpenTelemetry.Tests.Stress/OpenTelemetry.Tests.Stress.csproj b/test/OpenTelemetry.Tests.Stress/OpenTelemetry.Tests.Stress.csproj new file mode 100644 index 00000000000..a596a2997ce --- /dev/null +++ b/test/OpenTelemetry.Tests.Stress/OpenTelemetry.Tests.Stress.csproj @@ -0,0 +1,11 @@ + + + Exe + netcoreapp3.1;net5.0;net6.0;net462 + false + + + + + + diff --git a/test/OpenTelemetry.Tests.Stress/README.md b/test/OpenTelemetry.Tests.Stress/README.md new file mode 100644 index 00000000000..00d2ff247cd --- /dev/null +++ b/test/OpenTelemetry.Tests.Stress/README.md @@ -0,0 +1,118 @@ +# OpenTelemetry Stress Tests + +* [Why would you need stress test](#why-would-you-need-stress-test) +* [Running the demo](#running-the-demo) +* [Writing your own stress test](#writing-your-own-stress-test) +* [Understanding the results](#understanding-the-results) + +## Why would you need stress test + +* It helps you to understand performance. +* You can keep it running for days and nights to verify stability. +* You can use it to generate lots of load to your backend system. +* You can use it with other stress tools (e.g. a memory limiter) to verify how + your code reacts to certain resource constraints. + +## Running the demo + +Open a console, run the following command from the current folder: + +```sh +dotnet run --framework net6.0 --configuration Release +``` + +Once the application started, you will see the performance number updates from +the console window title. + +Use the `SPACE` key to toggle the console output, which is off by default. + +Use the `ENTER` key to print the latest performance statistics. + +Use the `ESC` key to exit the stress test. + + +```text +Running (concurrency = 1), press to stop... +2021-09-28T18:47:17.6807622Z Loops: 17,549,732,467, Loops/Second: 738,682,519, CPU Cycles/Loop: 3 +2021-09-28T18:47:17.8846348Z Loops: 17,699,532,304, Loops/Second: 731,866,438, CPU Cycles/Loop: 3 +2021-09-28T18:47:18.0914577Z Loops: 17,850,498,225, Loops/Second: 730,931,752, CPU Cycles/Loop: 3 +2021-09-28T18:47:18.2992864Z Loops: 18,000,133,808, Loops/Second: 724,029,883, CPU Cycles/Loop: 3 +2021-09-28T18:47:18.5052989Z Loops: 18,150,598,194, Loops/Second: 733,026,161, CPU Cycles/Loop: 3 +2021-09-28T18:47:18.7116733Z Loops: 18,299,461,007, Loops/Second: 724,950,210, CPU Cycles/Loop: 3 +``` + + +The stress test metrics are exposed via +[PrometheusExporter](../../src/OpenTelemetry.Exporter.Prometheus/README.md), +which can be accessed via +[http://localhost:9184/metrics/](http://localhost:9184/metrics/): + +```text +# TYPE Process_NonpagedSystemMemorySize64 gauge +Process_NonpagedSystemMemorySize64 31651 1637385964580 + +# TYPE Process_PagedSystemMemorySize64 gauge +Process_PagedSystemMemorySize64 238672 1637385964580 + +# TYPE Process_PagedMemorySize64 gauge +Process_PagedMemorySize64 16187392 1637385964580 + +# TYPE Process_WorkingSet64 gauge +Process_WorkingSet64 29753344 1637385964580 + +# TYPE Process_VirtualMemorySize64 gauge +Process_VirtualMemorySize64 2204045848576 1637385964580 +``` + +## Writing your own stress test + +Create a simple console application with the following code: + +```csharp +using System.Runtime.CompilerServices; + +public partial class Program +{ + public static void Main() + { + Stress(concurrency: 10, prometheusPort: 9184); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected static void Run() + { + // add your logic here + } +} +``` + +Add the [`Skeleton.cs`](./Skeleton.cs) file to your `*.csproj` file: + +```xml + + + +``` + +Now you are ready to run your own stress test. + +Some useful notes: + +* You can specify the concurrency using `Stress(concurrency: {concurrency + number})`, the default value is the number of CPU cores. Keep in mind that + concurrency level does not equal to the number of threads. +* You can specify a local PrometheusExporter listening port using + `Stress(prometheusPort: {port number})`, the default value is `0`, which will + turn off the PrometheusExporter. +* You want to put `[MethodImpl(MethodImplOptions.AggressiveInlining)]` on + `Run()`, this helps to reduce extra flushes on the CPU instruction cache. +* You might want to run the stress test under `Release` mode rather than `Debug` + mode. + +## Understanding the results + +* `Loops` represent the total number of `Run()` invocations that are completed. +* `Loops/Second` represents the rate of `Run()` invocations based on a small + sliding window of few hundreds of milliseconds. +* `CPU Cycles/Loop` represents the average CPU cycles for each `Run()` + invocation, based on a small sliding window of few hundreds of milliseconds. diff --git a/test/OpenTelemetry.Tests.Stress/Skeleton.cs b/test/OpenTelemetry.Tests.Stress/Skeleton.cs new file mode 100644 index 00000000000..7acd5b9d712 --- /dev/null +++ b/test/OpenTelemetry.Tests.Stress/Skeleton.cs @@ -0,0 +1,206 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Diagnostics; +using System.Diagnostics.Metrics; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using OpenTelemetry.Metrics; + +namespace OpenTelemetry.Tests.Stress; + +public partial class Program +{ + private static readonly Meter StressMeter = new Meter("OpenTelemetry.Tests.Stress"); + private static volatile bool bContinue = true; + private static volatile string output = "Test results not available yet."; + + static Program() + { + var process = Process.GetCurrentProcess(); + StressMeter.CreateObservableGauge("Process.NonpagedSystemMemorySize64", () => process.NonpagedSystemMemorySize64); + StressMeter.CreateObservableGauge("Process.PagedSystemMemorySize64", () => process.PagedSystemMemorySize64); + StressMeter.CreateObservableGauge("Process.PagedMemorySize64", () => process.PagedMemorySize64); + StressMeter.CreateObservableGauge("Process.WorkingSet64", () => process.WorkingSet64); + StressMeter.CreateObservableGauge("Process.VirtualMemorySize64", () => process.VirtualMemorySize64); + } + + public static void Stress(int concurrency = 0, int prometheusPort = 0) + { +#if DEBUG + Console.WriteLine("***WARNING*** The current build is DEBUG which may affect timing!"); + Console.WriteLine(); +#endif + + if (concurrency < 0) + { + throw new ArgumentOutOfRangeException(nameof(concurrency), "concurrency level should be a non-negative number."); + } + + if (concurrency == 0) + { + concurrency = Environment.ProcessorCount; + } + + using var meter = new Meter("OpenTelemetry.Tests.Stress." + Guid.NewGuid().ToString("D")); + var cntLoopsTotal = 0UL; + meter.CreateObservableCounter( + "OpenTelemetry.Tests.Stress.Loops", + () => unchecked((long)cntLoopsTotal), + description: "The total number of `Run()` invocations that are completed."); + var dLoopsPerSecond = 0D; + meter.CreateObservableGauge( + "OpenTelemetry.Tests.Stress.LoopsPerSecond", + () => dLoopsPerSecond, + description: "The rate of `Run()` invocations based on a small sliding window of few hundreds of milliseconds."); + var dCpuCyclesPerLoop = 0D; +#if NET462 + if (Environment.OSVersion.Platform == PlatformID.Win32NT) +#else + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) +#endif + { + meter.CreateObservableGauge( + "OpenTelemetry.Tests.Stress.CpuCyclesPerLoop", + () => dCpuCyclesPerLoop, + description: "The average CPU cycles for each `Run()` invocation, based on a small sliding window of few hundreds of milliseconds."); + } + + using var meterProvider = prometheusPort != 0 ? Sdk.CreateMeterProviderBuilder() + .AddMeter(StressMeter.Name) + .AddMeter(meter.Name) + .AddPrometheusExporter(options => + { + options.StartHttpListener = true; + options.HttpListenerPrefixes = new string[] { $"http://localhost:{prometheusPort}/" }; + options.ScrapeResponseCacheDurationMilliseconds = 0; + }) + .Build() : null; + + var statistics = new long[concurrency]; + var watchForTotal = Stopwatch.StartNew(); + + Parallel.Invoke( + () => + { + Console.Write($"Running (concurrency = {concurrency}"); + + if (prometheusPort != 0) + { + Console.Write($", prometheusEndpoint = http://localhost:{prometheusPort}/metrics/"); + } + + Console.WriteLine("), press to stop..."); + + var bOutput = false; + var watch = new Stopwatch(); + while (true) + { + if (Console.KeyAvailable) + { + var key = Console.ReadKey(true).Key; + + switch (key) + { + case ConsoleKey.Enter: + Console.WriteLine(string.Format("{0} {1}", DateTime.UtcNow.ToString("O"), output)); + break; + case ConsoleKey.Escape: + bContinue = false; + return; + case ConsoleKey.Spacebar: + bOutput = !bOutput; + break; + } + + continue; + } + + if (bOutput) + { + Console.WriteLine(string.Format("{0} {1}", DateTime.UtcNow.ToString("O"), output)); + } + + var cntLoopsOld = (ulong)statistics.Sum(); + var cntCpuCyclesOld = GetCpuCycles(); + + watch.Restart(); + Thread.Sleep(200); + watch.Stop(); + + cntLoopsTotal = (ulong)statistics.Sum(); + var cntCpuCyclesNew = GetCpuCycles(); + + var nLoops = cntLoopsTotal - cntLoopsOld; + var nCpuCycles = cntCpuCyclesNew - cntCpuCyclesOld; + + dLoopsPerSecond = (double)nLoops / ((double)watch.ElapsedMilliseconds / 1000.0); + dCpuCyclesPerLoop = nLoops == 0 ? 0 : nCpuCycles / nLoops; + + output = $"Loops: {cntLoopsTotal:n0}, Loops/Second: {dLoopsPerSecond:n0}, CPU Cycles/Loop: {dCpuCyclesPerLoop:n0}"; + Console.Title = output; + } + }, + () => + { + Parallel.For(0, concurrency, (i) => + { + statistics[i] = 0; + while (bContinue) + { + Run(); + statistics[i]++; + } + }); + }); + + watchForTotal.Stop(); + cntLoopsTotal = (ulong)statistics.Sum(); + var totalLoopsPerSecond = (double)cntLoopsTotal / ((double)watchForTotal.ElapsedMilliseconds / 1000.0); + var cntCpuCyclesTotal = GetCpuCycles(); + var cpuCyclesPerLoopTotal = cntLoopsTotal == 0 ? 0 : cntCpuCyclesTotal / cntLoopsTotal; + Console.WriteLine("Stopping the stress test..."); + Console.WriteLine($"* Total Loops: {cntLoopsTotal:n0}"); + Console.WriteLine($"* Average Loops/Second: {totalLoopsPerSecond:n0}"); + Console.WriteLine($"* Average CPU Cycles/Loop: {cpuCyclesPerLoopTotal:n0}"); + } + + [DllImport("kernel32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool QueryProcessCycleTime(IntPtr hProcess, out ulong cycles); + + private static ulong GetCpuCycles() + { +#if NET462 + if (Environment.OSVersion.Platform != PlatformID.Win32NT) +#else + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) +#endif + { + return 0; + } + + if (!QueryProcessCycleTime((IntPtr)(-1), out var cycles)) + { + return 0; + } + + return cycles; + } +} diff --git a/test/OpenTelemetry.Tests/BaggageTests.cs b/test/OpenTelemetry.Tests/BaggageTests.cs index eb4e62cb5b6..180039779dc 100644 --- a/test/OpenTelemetry.Tests/BaggageTests.cs +++ b/test/OpenTelemetry.Tests/BaggageTests.cs @@ -16,6 +16,7 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using Xunit; namespace OpenTelemetry.Tests @@ -47,7 +48,8 @@ public void SetAndGetTest() }; Baggage.SetBaggage(K1, V1); - Baggage.Current.SetBaggage(K2, V2); + var baggage = Baggage.Current.SetBaggage(K2, V2); + Baggage.Current = baggage; Assert.NotEmpty(Baggage.GetBaggage()); Assert.Equal(list, Baggage.GetBaggage(Baggage.Current)); @@ -58,7 +60,7 @@ public void SetAndGetTest() Assert.Null(Baggage.GetBaggage("NO_KEY")); Assert.Equal(V2, Baggage.Current.GetBaggage(K2)); - Assert.Throws(() => Baggage.GetBaggage(null)); + Assert.Throws(() => Baggage.GetBaggage(null)); } [Fact] @@ -145,6 +147,7 @@ public void ContextFlowTest() { var baggage = Baggage.SetBaggage(K1, V1); var baggage2 = Baggage.Current.SetBaggage(K2, V2); + Baggage.Current = baggage2; var baggage3 = Baggage.SetBaggage(K3, V3); Assert.Equal(1, baggage.Count); @@ -269,5 +272,45 @@ public void GetHashCodeTests() Assert.Equal(expectedBaggage.GetHashCode(), baggage.GetHashCode()); } + + [Fact] + public async Task AsyncLocalTests() + { + Baggage.SetBaggage("key1", "value1"); + + await InnerTask().ConfigureAwait(false); + + Baggage.SetBaggage("key4", "value4"); + + Assert.Equal(4, Baggage.Current.Count); + Assert.Equal("value1", Baggage.GetBaggage("key1")); + Assert.Equal("value2", Baggage.GetBaggage("key2")); + Assert.Equal("value3", Baggage.GetBaggage("key3")); + Assert.Equal("value4", Baggage.GetBaggage("key4")); + + static async Task InnerTask() + { + Baggage.SetBaggage("key2", "value2"); + + await Task.Yield(); + + Baggage.SetBaggage("key3", "value3"); + + // key2 & key3 changes don't flow backward automatically + } + } + + [Fact] + public void ThreadSafetyTest() + { + Baggage.SetBaggage("rootKey", "rootValue"); // Note: Required to establish a root ExecutionContext containing the BaggageHolder we use as a lock + + Parallel.For(0, 100, (i) => + { + Baggage.SetBaggage($"key{i}", $"value{i}"); + }); + + Assert.Equal(101, Baggage.Current.Count); + } } } diff --git a/test/OpenTelemetry.Tests/Context/RuntimeContextTest.cs b/test/OpenTelemetry.Tests/Context/RuntimeContextTest.cs index f3bcccf365e..d39578bdd58 100644 --- a/test/OpenTelemetry.Tests/Context/RuntimeContextTest.cs +++ b/test/OpenTelemetry.Tests/Context/RuntimeContextTest.cs @@ -58,7 +58,7 @@ public void GetSlotReturnsNullForNonExistingSlot() public void GetSlotReturnsNullWhenTypeNotMatchingExistingSlot() { RuntimeContext.RegisterSlot("testslot"); - Assert.Throws(() => RuntimeContext.GetSlot("testslot")); + Assert.Throws(() => RuntimeContext.GetSlot("testslot")); } [Fact] diff --git a/test/OpenTelemetry.Tests/Instrumentation/DiagnosticSourceListenerTest.cs b/test/OpenTelemetry.Tests/Instrumentation/DiagnosticSourceListenerTest.cs index 9878cb5ae3b..7e9b6fc2de7 100644 --- a/test/OpenTelemetry.Tests/Instrumentation/DiagnosticSourceListenerTest.cs +++ b/test/OpenTelemetry.Tests/Instrumentation/DiagnosticSourceListenerTest.cs @@ -21,9 +21,9 @@ namespace OpenTelemetry.Instrumentation.Tests public class DiagnosticSourceListenerTest { private const string TestSourceName = "TestSourceName"; - private DiagnosticSource diagnosticSource; - private TestListenerHandler testListenerHandler; - private DiagnosticSourceSubscriber testDiagnosticSourceSubscriber; + private readonly DiagnosticSource diagnosticSource; + private readonly TestListenerHandler testListenerHandler; + private readonly DiagnosticSourceSubscriber testDiagnosticSourceSubscriber; public DiagnosticSourceListenerTest() { diff --git a/test/OpenTelemetry.Tests/Instrumentation/PropertyFetcherTest.cs b/test/OpenTelemetry.Tests/Instrumentation/PropertyFetcherTest.cs index 9793c104e37..89714dc46e8 100644 --- a/test/OpenTelemetry.Tests/Instrumentation/PropertyFetcherTest.cs +++ b/test/OpenTelemetry.Tests/Instrumentation/PropertyFetcherTest.cs @@ -50,5 +50,35 @@ public void FetchNullProperty() var fetch = new PropertyFetcher("null"); Assert.False(fetch.TryFetch(null, out _)); } + + [Fact] + public void FetchPropertyMultiplePayloadTypes() + { + var fetch = new PropertyFetcher("Property"); + + Assert.True(fetch.TryFetch(new PayloadTypeA(), out string propertyValue)); + Assert.Equal("A", propertyValue); + + Assert.True(fetch.TryFetch(new PayloadTypeB(), out propertyValue)); + Assert.Equal("B", propertyValue); + + Assert.False(fetch.TryFetch(new PayloadTypeC(), out _)); + + Assert.False(fetch.TryFetch(null, out _)); + } + + private class PayloadTypeA + { + public string Property { get; set; } = "A"; + } + + private class PayloadTypeB + { + public string Property { get; set; } = "B"; + } + + private class PayloadTypeC + { + } } } diff --git a/test/OpenTelemetry.Tests/Internal/EnvironmentVariableHelperTests.cs b/test/OpenTelemetry.Tests/Internal/EnvironmentVariableHelperTests.cs new file mode 100644 index 00000000000..f5a4618f601 --- /dev/null +++ b/test/OpenTelemetry.Tests/Internal/EnvironmentVariableHelperTests.cs @@ -0,0 +1,126 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using Xunit; + +namespace OpenTelemetry.Internal.Tests +{ + public class EnvironmentVariableHelperTests : IDisposable + { + private const string EnvVar = "OTEL_EXAMPLE_VARIABLE"; + + public EnvironmentVariableHelperTests() + { + Environment.SetEnvironmentVariable(EnvVar, null); + } + + public void Dispose() + { + Environment.SetEnvironmentVariable(EnvVar, null); + } + + [Fact] + public void LoadString() + { + const string value = "something"; + Environment.SetEnvironmentVariable(EnvVar, value); + + bool actualBool = EnvironmentVariableHelper.LoadString(EnvVar, out string actualValue); + + Assert.True(actualBool); + Assert.Equal(value, actualValue); + } + + [Fact] + public void LoadString_NoValue() + { + bool actualBool = EnvironmentVariableHelper.LoadString(EnvVar, out string actualValue); + + Assert.False(actualBool); + Assert.Null(actualValue); + } + + [Theory] + [InlineData("123", 123)] + [InlineData("0", 0)] + public void LoadNumeric(string value, int expectedValue) + { + Environment.SetEnvironmentVariable(EnvVar, value); + + bool actualBool = EnvironmentVariableHelper.LoadNumeric(EnvVar, out int actualValue); + + Assert.True(actualBool); + Assert.Equal(expectedValue, actualValue); + } + + [Fact] + public void LoadNumeric_NoValue() + { + bool actualBool = EnvironmentVariableHelper.LoadNumeric(EnvVar, out int actualValue); + + Assert.False(actualBool); + Assert.Equal(0, actualValue); + } + + [Theory] + [InlineData("something")] // NaN + [InlineData("-12")] // negative number not allowed + [InlineData("-0")] // sign not allowed + [InlineData("-1")] // -1 is not allowed + [InlineData(" 123 ")] // whitespaces not allowed + [InlineData("0xFF")] // only decimal number allowed + public void LoadNumeric_Invalid(string value) + { + Environment.SetEnvironmentVariable(EnvVar, value); + + Assert.Throws(() => EnvironmentVariableHelper.LoadNumeric(EnvVar, out int _)); + } + + [Theory] + [InlineData("http://www.example.com", "http://www.example.com/")] + [InlineData("http://www.example.com/space%20here.html", "http://www.example.com/space here.html")] // characters are converted + [InlineData("http://www.example.com/space here.html", "http://www.example.com/space here.html")] // characters are escaped + public void LoadUri(string value, string expectedValue) + { + Environment.SetEnvironmentVariable(EnvVar, value); + + bool actualBool = EnvironmentVariableHelper.LoadUri(EnvVar, out Uri actualValue); + + Assert.True(actualBool); + Assert.Equal(expectedValue, actualValue.ToString()); + } + + [Fact] + public void LoadUri_NoValue() + { + bool actualBool = EnvironmentVariableHelper.LoadUri(EnvVar, out Uri actualValue); + + Assert.False(actualBool); + Assert.Null(actualValue); + } + + [Theory] + [InlineData("invalid")] // invalid format + [InlineData(" ")] // whitespace + public void LoadUri_Invalid(string value) + { + Environment.SetEnvironmentVariable(EnvVar, value); + + Assert.Throws(() => EnvironmentVariableHelper.LoadUri(EnvVar, out Uri _)); + } + } +} diff --git a/test/OpenTelemetry.Tests/Internal/GuardTest.cs b/test/OpenTelemetry.Tests/Internal/GuardTest.cs new file mode 100644 index 00000000000..66807540905 --- /dev/null +++ b/test/OpenTelemetry.Tests/Internal/GuardTest.cs @@ -0,0 +1,143 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Threading; +using OpenTelemetry.Internal; +using Xunit; + +namespace OpenTelemetry.Tests.Internal +{ + public class GuardTest + { + [Fact] + public void NullTest() + { + // Valid + Guard.ThrowIfNull(1); + Guard.ThrowIfNull(1.0); + Guard.ThrowIfNull(new object()); + Guard.ThrowIfNull("hello"); + + // Invalid + var ex1 = Assert.Throws(() => Guard.ThrowIfNull(null, "null")); + Assert.Contains("Must not be null", ex1.Message); + } + + [Fact] + public void NullOrEmptyTest() + { + // Valid + Guard.ThrowIfNullOrEmpty("a"); + Guard.ThrowIfNullOrEmpty(" "); + + // Invalid + var ex1 = Assert.Throws(() => Guard.ThrowIfNullOrEmpty(null)); + Assert.Contains("Must not be null or empty", ex1.Message); + + var ex2 = Assert.Throws(() => Guard.ThrowIfNullOrEmpty(string.Empty)); + Assert.Contains("Must not be null or empty", ex2.Message); + } + + [Fact] + public void NullOrWhitespaceTest() + { + // Valid + Guard.ThrowIfNullOrWhitespace("a"); + + // Invalid + var ex1 = Assert.Throws(() => Guard.ThrowIfNullOrWhitespace(null)); + Assert.Contains("Must not be null or whitespace", ex1.Message); + + var ex2 = Assert.Throws(() => Guard.ThrowIfNullOrWhitespace(string.Empty)); + Assert.Contains("Must not be null or whitespace", ex2.Message); + + var ex3 = Assert.Throws(() => Guard.ThrowIfNullOrWhitespace(" \t\n\r")); + Assert.Contains("Must not be null or whitespace", ex3.Message); + } + + [Fact] + public void InvalidTimeoutTest() + { + // Valid + Guard.ThrowIfInvalidTimeout(Timeout.Infinite); + Guard.ThrowIfInvalidTimeout(0); + Guard.ThrowIfInvalidTimeout(100); + + // Invalid + var ex1 = Assert.Throws(() => Guard.ThrowIfInvalidTimeout(-100)); + Assert.Contains("Must be non-negative or 'Timeout.Infinite'", ex1.Message); + } + + [Fact] + public void RangeIntTest() + { + // Valid + Guard.ThrowIfOutOfRange(0); + Guard.ThrowIfOutOfRange(0, min: 0, max: 0); + Guard.ThrowIfOutOfRange(5, min: -10, max: 10); + Guard.ThrowIfOutOfRange(int.MinValue, min: int.MinValue, max: 10); + Guard.ThrowIfOutOfRange(int.MaxValue, min: 10, max: int.MaxValue); + + // Invalid + var ex1 = Assert.Throws(() => Guard.ThrowIfOutOfRange(-1, min: 0, max: 100, minName: "empty", maxName: "full")); + Assert.Contains("Must be in the range: [0: empty, 100: full]", ex1.Message); + + var ex2 = Assert.Throws(() => Guard.ThrowIfOutOfRange(-1, min: 0, max: 100, message: "error")); + Assert.Contains("error", ex2.Message); + } + + [Fact] + public void RangeDoubleTest() + { + // Valid + Guard.ThrowIfOutOfRange(1.0, min: 1.0, max: 1.0); + Guard.ThrowIfOutOfRange(double.MinValue, min: double.MinValue, max: 10.0); + Guard.ThrowIfOutOfRange(double.MaxValue, min: 10.0, max: double.MaxValue); + + // Invalid + var ex3 = Assert.Throws(() => Guard.ThrowIfOutOfRange(-1.1, min: 0.1, max: 99.9, minName: "empty", maxName: "full")); + Assert.Contains("Must be in the range: [0.1: empty, 99.9: full]", ex3.Message); + + var ex4 = Assert.Throws(() => Guard.ThrowIfOutOfRange(-1.1, min: 0.0, max: 100.0)); + Assert.Contains("Must be in the range: [0, 100]", ex4.Message); + } + + [Fact] + public void TypeTest() + { + // Valid + Guard.ThrowIfNotOfType(0); + Guard.ThrowIfNotOfType(new object()); + Guard.ThrowIfNotOfType("hello"); + + // Invalid + var ex1 = Assert.Throws(() => Guard.ThrowIfNotOfType(100)); + Assert.Equal("Cannot cast 'N/A' from 'Int32' to 'Double'", ex1.Message); + } + + [Fact] + public void ZeroTest() + { + // Valid + Guard.ThrowIfZero(1); + + // Invalid + var ex1 = Assert.Throws(() => Guard.ThrowIfZero(0)); + Assert.Contains("Must not be zero", ex1.Message); + } + } +} diff --git a/test/OpenTelemetry.Tests/Metrics/AggregatorTest.cs b/test/OpenTelemetry.Tests/Metrics/AggregatorTest.cs index e31fe0214a3..8a276f5d618 100644 --- a/test/OpenTelemetry.Tests/Metrics/AggregatorTest.cs +++ b/test/OpenTelemetry.Tests/Metrics/AggregatorTest.cs @@ -15,8 +15,6 @@ // using System; -using System.Collections.Generic; -using System.Diagnostics.Metrics; using Xunit; namespace OpenTelemetry.Metrics.Tests @@ -24,101 +22,114 @@ namespace OpenTelemetry.Metrics.Tests public class AggregatorTest { [Fact] - public void HistogramDistributeToAllBuckets() + public void HistogramDistributeToAllBucketsDefault() { - using var meter = new Meter("TestMeter", "0.0.1"); - - var hist = new HistogramMetricAggregator("test", "desc", "1", meter, DateTimeOffset.UtcNow, new KeyValuePair[0]); - - hist.Update(-1); - hist.Update(0); - hist.Update(5); - hist.Update(10); - hist.Update(25); - hist.Update(50); - hist.Update(75); - hist.Update(100); - hist.Update(250); - hist.Update(500); - hist.Update(1000); - var metric = hist.Collect(DateTimeOffset.UtcNow, false); - - Assert.NotNull(metric); - Assert.IsType(metric); - - if (metric is HistogramMetric agg) + var histogramPoint = new MetricPoint(AggregationType.Histogram, DateTimeOffset.Now, null, null, Metric.DefaultHistogramBounds); + histogramPoint.Update(-1); + histogramPoint.Update(0); + histogramPoint.Update(2); + histogramPoint.Update(5); + histogramPoint.Update(8); + histogramPoint.Update(10); + histogramPoint.Update(11); + histogramPoint.Update(25); + histogramPoint.Update(40); + histogramPoint.Update(50); + histogramPoint.Update(70); + histogramPoint.Update(75); + histogramPoint.Update(99); + histogramPoint.Update(100); + histogramPoint.Update(246); + histogramPoint.Update(250); + histogramPoint.Update(499); + histogramPoint.Update(500); + histogramPoint.Update(999); + histogramPoint.Update(1000); + histogramPoint.Update(1001); + histogramPoint.Update(10000000); + histogramPoint.TakeSnapshot(true); + + var count = histogramPoint.GetHistogramCount(); + + Assert.Equal(22, count); + + int actualCount = 0; + foreach (var histogramMeasurement in histogramPoint.GetHistogramBuckets()) { - int len = 0; - foreach (var bucket in agg.Buckets) - { - Assert.Equal(1, bucket.Count); - len++; - } - - Assert.Equal(11, len); + Assert.Equal(2, histogramMeasurement.BucketCount); + actualCount++; } } [Fact] - public void HistogramCustomBoundaries() + public void HistogramDistributeToAllBucketsCustom() { - using var meter = new Meter("TestMeter", "0.0.1"); + var boundaries = new double[] { 10, 20 }; + var histogramPoint = new MetricPoint(AggregationType.Histogram, DateTimeOffset.Now, null, null, boundaries); - var hist = new HistogramMetricAggregator("test", "desc", "1", meter, DateTimeOffset.UtcNow, new KeyValuePair[0], new double[] { 0 }); + // 5 recordings <=10 + histogramPoint.Update(-10); + histogramPoint.Update(0); + histogramPoint.Update(1); + histogramPoint.Update(9); + histogramPoint.Update(10); - hist.Update(-1); - hist.Update(0); - var metric = hist.Collect(DateTimeOffset.UtcNow, false); + // 2 recordings >10, <=20 + histogramPoint.Update(11); + histogramPoint.Update(19); - Assert.NotNull(metric); - Assert.IsType(metric); + histogramPoint.TakeSnapshot(true); - if (metric is HistogramMetric agg) + var count = histogramPoint.GetHistogramCount(); + var sum = histogramPoint.GetHistogramSum(); + + // Sum of all recordings + Assert.Equal(40, sum); + + // Count = # of recordings + Assert.Equal(7, count); + + int index = 0; + int actualCount = 0; + var expectedBucketCounts = new long[] { 5, 2, 0 }; + foreach (var histogramMeasurement in histogramPoint.GetHistogramBuckets()) { - int len = 0; - foreach (var bucket in agg.Buckets) - { - Assert.Equal(1, bucket.Count); - len++; - } - - Assert.Equal(2, len); + Assert.Equal(expectedBucketCounts[index], histogramMeasurement.BucketCount); + index++; + actualCount++; } + + Assert.Equal(boundaries.Length + 1, actualCount); } [Fact] - public void HistogramWithEmptyBuckets() + public void HistogramWithOnlySumCount() { - using var meter = new Meter("TestMeter", "0.0.1"); + var boundaries = new double[] { }; + var histogramPoint = new MetricPoint(AggregationType.HistogramSumCount, DateTimeOffset.Now, null, null, boundaries); - var hist = new HistogramMetricAggregator("test", "desc", "1", meter, DateTimeOffset.UtcNow, new KeyValuePair[0], new double[] { 0, 5, 10 }); + histogramPoint.Update(-10); + histogramPoint.Update(0); + histogramPoint.Update(1); + histogramPoint.Update(9); + histogramPoint.Update(10); + histogramPoint.Update(11); + histogramPoint.Update(19); - hist.Update(-3); - hist.Update(-2); - hist.Update(-1); - hist.Update(6); - hist.Update(7); - hist.Update(12); - var metric = hist.Collect(DateTimeOffset.UtcNow, false); + histogramPoint.TakeSnapshot(true); - Assert.NotNull(metric); - Assert.IsType(metric); + var count = histogramPoint.GetHistogramCount(); + var sum = histogramPoint.GetHistogramSum(); - if (metric is HistogramMetric agg) - { - var expectedCounts = new int[] { 3, 0, 2, 1 }; - int len = 0; - foreach (var bucket in agg.Buckets) - { - if (len < expectedCounts.Length) - { - Assert.Equal(expectedCounts[len], bucket.Count); - len++; - } - } - - Assert.Equal(4, len); - } + // Sum of all recordings + Assert.Equal(40, sum); + + // Count = # of recordings + Assert.Equal(7, count); + + // There should be no enumeration of BucketCounts and ExplicitBounds for HistogramSumCount + var enumerator = histogramPoint.GetHistogramBuckets().GetEnumerator(); + Assert.False(enumerator.MoveNext()); } } } diff --git a/test/OpenTelemetry.Tests/Metrics/InMemoryExporterTests.cs b/test/OpenTelemetry.Tests/Metrics/InMemoryExporterTests.cs new file mode 100644 index 00000000000..5f5914be28e --- /dev/null +++ b/test/OpenTelemetry.Tests/Metrics/InMemoryExporterTests.cs @@ -0,0 +1,69 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Collections.Generic; +using System.Diagnostics.Metrics; +using OpenTelemetry.Exporter; +using OpenTelemetry.Tests; +using Xunit; + +namespace OpenTelemetry.Metrics.Tests +{ + public class InMemoryExporterTests + { + [Fact(Skip = "To be run after https://github.com/open-telemetry/opentelemetry-dotnet/issues/2361 is fixed")] + public void InMemoryExporterShouldDeepCopyMetricPoints() + { + var exportedItems = new List(); + + using var meter = new Meter(Utils.GetCurrentMethodName()); + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddReader(new BaseExportingMetricReader(new InMemoryExporter(exportedItems)) + { + Temporality = AggregationTemporality.Delta, + }) + .Build(); + + var counter = meter.CreateCounter("meter"); + + // Emit 10 for the MetricPoint with a single key-vaue pair: ("tag1", "value1") + counter.Add(10, new KeyValuePair("tag1", "value1")); + + meterProvider.ForceFlush(); + + var metric = exportedItems[0]; // Only one Metric object is added to the collection at this point + var metricPointsEnumerator = metric.GetMetricPoints().GetEnumerator(); + Assert.True(metricPointsEnumerator.MoveNext()); // One MetricPoint is emitted for the Metric + ref readonly var metricPointForFirstExport = ref metricPointsEnumerator.Current; + Assert.Equal(10, metricPointForFirstExport.GetSumLong()); + + // Emit 25 for the MetricPoint with a single key-vaue pair: ("tag1", "value1") + counter.Add(25, new KeyValuePair("tag1", "value1")); + + meterProvider.ForceFlush(); + + metric = exportedItems[1]; // Second Metric object is added to the collection at this point + metricPointsEnumerator = metric.GetMetricPoints().GetEnumerator(); + Assert.True(metricPointsEnumerator.MoveNext()); // One MetricPoint is emitted for the Metric + var metricPointForSecondExport = metricPointsEnumerator.Current; + Assert.Equal(25, metricPointForSecondExport.GetSumLong()); + + // MetricPoint.LongValue for the first exporter metric should still be 10 + Assert.Equal(10, metricPointForFirstExport.GetSumLong()); + } + } +} diff --git a/test/OpenTelemetry.Tests/Metrics/MemoryEfficiencyTests.cs b/test/OpenTelemetry.Tests/Metrics/MemoryEfficiencyTests.cs new file mode 100644 index 00000000000..c241519b95e --- /dev/null +++ b/test/OpenTelemetry.Tests/Metrics/MemoryEfficiencyTests.cs @@ -0,0 +1,63 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Collections.Generic; +using System.Diagnostics.Metrics; +using OpenTelemetry.Exporter; +using OpenTelemetry.Tests; +using Xunit; + +namespace OpenTelemetry.Metrics.Tests +{ + public class MemoryEfficiencyTests + { + [Theory] + [InlineData(AggregationTemporality.Cumulative)] + [InlineData(AggregationTemporality.Delta)] + public void ExportOnlyWhenPointChanged(AggregationTemporality temporality) + { + using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{temporality}"); + + var exportedItems = new List(); + + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddReader( + new BaseExportingMetricReader(new InMemoryExporter(exportedItems)) + { + Temporality = temporality, + }) + .Build(); + + var counter = meter.CreateCounter("meter"); + + counter.Add(10, new KeyValuePair("tag1", "value1")); + meterProvider.ForceFlush(); + Assert.Single(exportedItems); + + exportedItems.Clear(); + meterProvider.ForceFlush(); + if (temporality == AggregationTemporality.Cumulative) + { + Assert.Single(exportedItems); + } + else + { + Assert.Empty(exportedItems); + } + } + } +} diff --git a/test/OpenTelemetry.Tests/Metrics/MetricAPITest.cs b/test/OpenTelemetry.Tests/Metrics/MetricAPITest.cs index 425576bd400..199ab273b63 100644 --- a/test/OpenTelemetry.Tests/Metrics/MetricAPITest.cs +++ b/test/OpenTelemetry.Tests/Metrics/MetricAPITest.cs @@ -19,6 +19,7 @@ using System.Diagnostics; using System.Diagnostics.Metrics; using System.Threading; +using OpenTelemetry.Exporter; using OpenTelemetry.Tests; using Xunit; using Xunit.Abstractions; @@ -27,8 +28,10 @@ namespace OpenTelemetry.Metrics.Tests { public class MetricApiTest { + private const int MaxTimeToAllowForFlush = 10000; private static int numberOfThreads = Environment.ProcessorCount; - private static long deltaValueUpdatedByEachCall = 10; + private static long deltaLongValueUpdatedByEachCall = 10; + private static double deltaDoubleValueUpdatedByEachCall = 11.987; private static int numberOfMetricUpdateByEachThread = 100000; private readonly ITestOutputHelper output; @@ -38,91 +41,877 @@ public MetricApiTest(ITestOutputHelper output) } [Fact] - public void SimpleTest() + public void ObserverCallbackTest() { - var metricItems = new List(); - var metricExporter = new TestExporter(ProcessExport); - void ProcessExport(Batch batch) + using var meter = new Meter(Utils.GetCurrentMethodName()); + var exportedItems = new List(); + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems) + .Build(); + + var measurement = new Measurement(100, new("name", "apple"), new("color", "red")); + meter.CreateObservableGauge("myGauge", () => measurement); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Single(exportedItems); + var metric = exportedItems[0]; + Assert.Equal("myGauge", metric.Name); + List metricPoints = new List(); + foreach (ref readonly var mp in metric.GetMetricPoints()) { - foreach (var metricItem in batch) + metricPoints.Add(mp); + } + + Assert.Single(metricPoints); + var metricPoint = metricPoints[0]; + Assert.Equal(100, metricPoint.GetGaugeLastValueLong()); + Assert.True(metricPoint.Tags.Count > 0); + } + + [Fact] + public void ObserverCallbackExceptionTest() + { + using var meter = new Meter(Utils.GetCurrentMethodName()); + var exportedItems = new List(); + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems) + .Build(); + + var measurement = new Measurement(100, new("name", "apple"), new("color", "red")); + meter.CreateObservableGauge("myGauge", () => measurement); + meter.CreateObservableGauge("myBadGauge", observeValues: () => throw new Exception("gauge read error")); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Single(exportedItems); + var metric = exportedItems[0]; + Assert.Equal("myGauge", metric.Name); + List metricPoints = new List(); + foreach (ref readonly var mp in metric.GetMetricPoints()) + { + metricPoints.Add(mp); + } + + Assert.Single(metricPoints); + var metricPoint = metricPoints[0]; + Assert.Equal(100, metricPoint.GetGaugeLastValueLong()); + Assert.True(metricPoint.Tags.Count > 0); + } + + [Theory] + [InlineData(AggregationTemporality.Cumulative, true)] + [InlineData(AggregationTemporality.Cumulative, false)] + [InlineData(AggregationTemporality.Delta, true)] + [InlineData(AggregationTemporality.Delta, false)] + public void DuplicateInstrumentNamesFromSameMeterAreNotAllowed(AggregationTemporality temporality, bool hasView) + { + var exportedItems = new List(); + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{temporality}"); + var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddReader(new BaseExportingMetricReader(new InMemoryExporter(exportedItems)) { - metricItems.Add(metricItem); - } + Temporality = temporality, + }); + + if (hasView) + { + meterProviderBuilder.AddView("name1", new MetricStreamConfiguration() { Description = "description" }); } - var pullProcessor = new PullMetricProcessor(metricExporter, true); + using var meterProvider = meterProviderBuilder.Build(); + + // Expecting one metric stream. + var counterLong = meter.CreateCounter("name1"); + counterLong.Add(10); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Single(exportedItems); + + // The following will be ignored as + // metric of same name exists. + // Metric stream will remain one. + var anotherCounterSameName = meter.CreateCounter("name1"); + anotherCounterSameName.Add(10); + counterLong.Add(10); + exportedItems.Clear(); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Single(exportedItems); + } + + [Theory] + [InlineData(AggregationTemporality.Cumulative, true)] + [InlineData(AggregationTemporality.Cumulative, false)] + [InlineData(AggregationTemporality.Delta, true)] + [InlineData(AggregationTemporality.Delta, false)] + public void DuplicateInstrumentNamesFromDifferentMetersAreAllowed(AggregationTemporality temporality, bool hasView) + { + var exportedItems = new List(); + + using var meter1 = new Meter($"{Utils.GetCurrentMethodName()}.1.{temporality}"); + using var meter2 = new Meter($"{Utils.GetCurrentMethodName()}.2.{temporality}"); + var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter1.Name) + .AddMeter(meter2.Name) + .AddReader(new BaseExportingMetricReader(new InMemoryExporter(exportedItems)) + { + Temporality = temporality, + }); + + if (hasView) + { + meterProviderBuilder.AddView("name1", new MetricStreamConfiguration() { Description = "description" }); + } + + using var meterProvider = meterProviderBuilder.Build(); + + // Expecting one metric stream. + var counterLong = meter1.CreateCounter("name1"); + counterLong.Add(10); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Single(exportedItems); + + // The following will not be ignored + // as it is the same metric name but different meter. + var anotherCounterSameNameDiffMeter = meter2.CreateCounter("name1"); + anotherCounterSameNameDiffMeter.Add(10); + counterLong.Add(10); + exportedItems.Clear(); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Equal(2, exportedItems.Count); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void MeterSourcesWildcardSupportMatchTest(bool hasView) + { + using var meter1 = new Meter("AbcCompany.XyzProduct.ComponentA"); + using var meter2 = new Meter("abcCompany.xYzProduct.componentC"); // Wildcard match is case insensitive. + using var meter3 = new Meter("DefCompany.AbcProduct.ComponentC"); + using var meter4 = new Meter("DefCompany.XyzProduct.ComponentC"); // Wildcard match supports matching multiple patterns. + using var meter5 = new Meter("GhiCompany.qweProduct.ComponentN"); + using var meter6 = new Meter("SomeCompany.SomeProduct.SomeComponent"); + + var exportedItems = new List(); + var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() + .AddMeter("AbcCompany.XyzProduct.*") + .AddMeter("DefCompany.*.ComponentC") + .AddMeter("GhiCompany.qweProduct.ComponentN") // Mixing of non-wildcard meter name and wildcard meter name. + .AddInMemoryExporter(exportedItems); + + if (hasView) + { + meterProviderBuilder.AddView("myGauge1", "newName"); + } - var meter = new Meter("TestMeter"); + using var meterProvider = meterProviderBuilder.Build(); + + var measurement = new Measurement(100, new("name", "apple"), new("color", "red")); + meter1.CreateObservableGauge("myGauge1", () => measurement); + meter2.CreateObservableGauge("myGauge2", () => measurement); + meter3.CreateObservableGauge("myGauge3", () => measurement); + meter4.CreateObservableGauge("myGauge4", () => measurement); + meter5.CreateObservableGauge("myGauge5", () => measurement); + meter6.CreateObservableGauge("myGauge6", () => measurement); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + Assert.True(exportedItems.Count == 5); // "SomeCompany.SomeProduct.SomeComponent" will not be subscribed. + + if (hasView) + { + Assert.Equal("newName", exportedItems[0].Name); + } + else + { + Assert.Equal("myGauge1", exportedItems[0].Name); + } + + Assert.Equal("myGauge2", exportedItems[1].Name); + Assert.Equal("myGauge3", exportedItems[2].Name); + Assert.Equal("myGauge4", exportedItems[3].Name); + Assert.Equal("myGauge5", exportedItems[4].Name); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void MeterSourcesWildcardSupportNegativeTestNoMeterAdded(bool hasView) + { + using var meter1 = new Meter($"AbcCompany.XyzProduct.ComponentA.{hasView}"); + using var meter2 = new Meter($"abcCompany.xYzProduct.componentC.{hasView}"); + + var exportedItems = new List(); + var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() + .AddInMemoryExporter(exportedItems); + + if (hasView) + { + meterProviderBuilder.AddView("gauge1", "renamed"); + } + + using var meterProvider = meterProviderBuilder.Build(); + var measurement = new Measurement(100, new("name", "apple"), new("color", "red")); + + meter1.CreateObservableGauge("myGauge1", () => measurement); + meter2.CreateObservableGauge("myGauge2", () => measurement); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.True(exportedItems.Count == 0); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void CounterAggregationTest(bool exportDelta) + { + var exportedItems = new List(); + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{exportDelta}"); var counterLong = meter.CreateCounter("mycounter"); - var meterProvider = Sdk.CreateMeterProviderBuilder() - .AddSource("TestMeter") - .AddMetricProcessor(pullProcessor) + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddReader(new BaseExportingMetricReader(new InMemoryExporter(exportedItems)) + { + Temporality = exportDelta ? AggregationTemporality.Delta : AggregationTemporality.Cumulative, + }) .Build(); - // setup args to threads. - var mreToBlockUpdateThreads = new ManualResetEvent(false); - var mreToEnsureAllThreadsStarted = new ManualResetEvent(false); + counterLong.Add(10); + counterLong.Add(10); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + long sumReceived = GetLongSum(exportedItems); + Assert.Equal(20, sumReceived); - var argToThread = new UpdateThreadArguments(); - argToThread.Counter = counterLong; - argToThread.ThreadsStartedCount = 0; - argToThread.MreToBlockUpdateThread = mreToBlockUpdateThreads; - argToThread.MreToEnsureAllThreadsStart = mreToEnsureAllThreadsStarted; + exportedItems.Clear(); + counterLong.Add(10); + counterLong.Add(10); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + sumReceived = GetLongSum(exportedItems); + if (exportDelta) + { + Assert.Equal(20, sumReceived); + } + else + { + Assert.Equal(40, sumReceived); + } - Thread[] t = new Thread[numberOfThreads]; - for (int i = 0; i < numberOfThreads; i++) + exportedItems.Clear(); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + sumReceived = GetLongSum(exportedItems); + if (exportDelta) { - t[i] = new Thread(CounterUpdateThread); - t[i].Start(argToThread); + Assert.Equal(0, sumReceived); + } + else + { + Assert.Equal(40, sumReceived); } - // Block until all threads started. - mreToEnsureAllThreadsStarted.WaitOne(); + exportedItems.Clear(); + counterLong.Add(40); + counterLong.Add(20); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + sumReceived = GetLongSum(exportedItems); + if (exportDelta) + { + Assert.Equal(60, sumReceived); + } + else + { + Assert.Equal(100, sumReceived); + } + } - Stopwatch sw = new Stopwatch(); - sw.Start(); + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ObservableCounterAggregationTest(bool exportDelta) + { + var exportedItems = new List(); - // unblock all the threads. - // (i.e let them start counter.Add) - mreToBlockUpdateThreads.Set(); + using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{exportDelta}"); + int i = 1; + var counterLong = meter.CreateObservableCounter( + "observable-counter", + () => + { + return new List>() + { + new Measurement(i++ * 10), + }; + }); - for (int i = 0; i < numberOfThreads; i++) + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddReader(new BaseExportingMetricReader(new InMemoryExporter(exportedItems)) + { + Temporality = exportDelta ? AggregationTemporality.Delta : AggregationTemporality.Cumulative, + }) + .Build(); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + long sumReceived = GetLongSum(exportedItems); + Assert.Equal(10, sumReceived); + + exportedItems.Clear(); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + sumReceived = GetLongSum(exportedItems); + if (exportDelta) { - // wait for all threads to complete - t[i].Join(); + Assert.Equal(10, sumReceived); + } + else + { + Assert.Equal(20, sumReceived); + } + + exportedItems.Clear(); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + sumReceived = GetLongSum(exportedItems); + if (exportDelta) + { + Assert.Equal(10, sumReceived); + } + else + { + Assert.Equal(30, sumReceived); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void DimensionsAreOrderInsensitiveWithSortedKeysFirst(bool exportDelta) + { + var exportedItems = new List(); + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{exportDelta}"); + var counterLong = meter.CreateCounter("Counter"); + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddReader(new BaseExportingMetricReader(new InMemoryExporter(exportedItems)) + { + Temporality = exportDelta ? AggregationTemporality.Delta : AggregationTemporality.Cumulative, + }) + .Build(); + + // Emit the first metric with the sorted order of tag keys + counterLong.Add(5, new("Key1", "Value1"), new("Key2", "Value2"), new("Key3", "Value3")); + counterLong.Add(10, new("Key1", "Value1"), new("Key3", "Value3"), new("Key2", "Value2")); + counterLong.Add(10, new("Key2", "Value20"), new("Key1", "Value10"), new("Key3", "Value30")); + + // Emit a metric with different set of keys but the same set of values as one of the previous metric points + counterLong.Add(25, new("Key4", "Value1"), new("Key5", "Value3"), new("Key6", "Value2")); + counterLong.Add(25, new("Key4", "Value1"), new("Key6", "Value3"), new("Key5", "Value2")); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + List> expectedTagsForFirstMetricPoint = new List>() + { + new("Key1", "Value1"), + new("Key2", "Value2"), + new("Key3", "Value3"), + }; + + List> expectedTagsForSecondMetricPoint = new List>() + { + new("Key1", "Value10"), + new("Key2", "Value20"), + new("Key3", "Value30"), + }; + + List> expectedTagsForThirdMetricPoint = new List>() + { + new("Key4", "Value1"), + new("Key5", "Value3"), + new("Key6", "Value2"), + }; + + List> expectedTagsForFourthMetricPoint = new List>() + { + new("Key4", "Value1"), + new("Key5", "Value2"), + new("Key6", "Value3"), + }; + + Assert.Equal(4, GetNumberOfMetricPoints(exportedItems)); + CheckTagsForNthMetricPoint(exportedItems, expectedTagsForFirstMetricPoint, 1); + CheckTagsForNthMetricPoint(exportedItems, expectedTagsForSecondMetricPoint, 2); + CheckTagsForNthMetricPoint(exportedItems, expectedTagsForThirdMetricPoint, 3); + CheckTagsForNthMetricPoint(exportedItems, expectedTagsForFourthMetricPoint, 4); + long sumReceived = GetLongSum(exportedItems); + Assert.Equal(75, sumReceived); + + exportedItems.Clear(); + + counterLong.Add(5, new("Key2", "Value2"), new("Key1", "Value1"), new("Key3", "Value3")); + counterLong.Add(5, new("Key2", "Value2"), new("Key1", "Value1"), new("Key3", "Value3")); + counterLong.Add(10, new("Key2", "Value2"), new("Key3", "Value3"), new("Key1", "Value1")); + counterLong.Add(10, new("Key2", "Value20"), new("Key3", "Value30"), new("Key1", "Value10")); + counterLong.Add(20, new("Key4", "Value1"), new("Key6", "Value2"), new("Key5", "Value3")); + counterLong.Add(20, new("Key4", "Value1"), new("Key5", "Value2"), new("Key6", "Value3")); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + Assert.Equal(4, GetNumberOfMetricPoints(exportedItems)); + CheckTagsForNthMetricPoint(exportedItems, expectedTagsForFirstMetricPoint, 1); + CheckTagsForNthMetricPoint(exportedItems, expectedTagsForSecondMetricPoint, 2); + CheckTagsForNthMetricPoint(exportedItems, expectedTagsForThirdMetricPoint, 3); + CheckTagsForNthMetricPoint(exportedItems, expectedTagsForFourthMetricPoint, 4); + sumReceived = GetLongSum(exportedItems); + if (exportDelta) + { + Assert.Equal(70, sumReceived); + } + else + { + Assert.Equal(145, sumReceived); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void DimensionsAreOrderInsensitiveWithUnsortedKeysFirst(bool exportDelta) + { + var exportedItems = new List(); + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{exportDelta}"); + var counterLong = meter.CreateCounter("Counter"); + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddReader(new BaseExportingMetricReader(new InMemoryExporter(exportedItems)) + { + Temporality = exportDelta ? AggregationTemporality.Delta : AggregationTemporality.Cumulative, + }) + .Build(); + + // Emit the first metric with the unsorted order of tag keys + counterLong.Add(5, new("Key1", "Value1"), new("Key3", "Value3"), new("Key2", "Value2")); + counterLong.Add(10, new("Key1", "Value1"), new("Key2", "Value2"), new("Key3", "Value3")); + counterLong.Add(10, new("Key2", "Value20"), new("Key1", "Value10"), new("Key3", "Value30")); + + // Emit a metric with different set of keys but the same set of values as one of the previous metric points + counterLong.Add(25, new("Key4", "Value1"), new("Key5", "Value3"), new("Key6", "Value2")); + counterLong.Add(25, new("Key4", "Value1"), new("Key6", "Value3"), new("Key5", "Value2")); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + List> expectedTagsForFirstMetricPoint = new List>() + { + new("Key1", "Value1"), + new("Key2", "Value2"), + new("Key3", "Value3"), + }; + + List> expectedTagsForSecondMetricPoint = new List>() + { + new("Key1", "Value10"), + new("Key2", "Value20"), + new("Key3", "Value30"), + }; + + List> expectedTagsForThirdMetricPoint = new List>() + { + new("Key4", "Value1"), + new("Key5", "Value3"), + new("Key6", "Value2"), + }; + + List> expectedTagsForFourthMetricPoint = new List>() + { + new("Key4", "Value1"), + new("Key5", "Value2"), + new("Key6", "Value3"), + }; + + Assert.Equal(4, GetNumberOfMetricPoints(exportedItems)); + CheckTagsForNthMetricPoint(exportedItems, expectedTagsForFirstMetricPoint, 1); + CheckTagsForNthMetricPoint(exportedItems, expectedTagsForSecondMetricPoint, 2); + CheckTagsForNthMetricPoint(exportedItems, expectedTagsForThirdMetricPoint, 3); + CheckTagsForNthMetricPoint(exportedItems, expectedTagsForFourthMetricPoint, 4); + long sumReceived = GetLongSum(exportedItems); + Assert.Equal(75, sumReceived); + + exportedItems.Clear(); + + counterLong.Add(5, new("Key2", "Value2"), new("Key1", "Value1"), new("Key3", "Value3")); + counterLong.Add(5, new("Key2", "Value2"), new("Key1", "Value1"), new("Key3", "Value3")); + counterLong.Add(10, new("Key2", "Value2"), new("Key3", "Value3"), new("Key1", "Value1")); + counterLong.Add(10, new("Key2", "Value20"), new("Key3", "Value30"), new("Key1", "Value10")); + counterLong.Add(20, new("Key4", "Value1"), new("Key6", "Value2"), new("Key5", "Value3")); + counterLong.Add(20, new("Key4", "Value1"), new("Key5", "Value2"), new("Key6", "Value3")); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + Assert.Equal(4, GetNumberOfMetricPoints(exportedItems)); + CheckTagsForNthMetricPoint(exportedItems, expectedTagsForFirstMetricPoint, 1); + CheckTagsForNthMetricPoint(exportedItems, expectedTagsForSecondMetricPoint, 2); + CheckTagsForNthMetricPoint(exportedItems, expectedTagsForThirdMetricPoint, 3); + CheckTagsForNthMetricPoint(exportedItems, expectedTagsForFourthMetricPoint, 4); + sumReceived = GetLongSum(exportedItems); + if (exportDelta) + { + Assert.Equal(70, sumReceived); + } + else + { + Assert.Equal(145, sumReceived); + } + } + + [Theory] + [InlineData(AggregationTemporality.Cumulative)] + [InlineData(AggregationTemporality.Delta)] + public void TestInstrumentDisposal(AggregationTemporality temporality) + { + var exportedItems = new List(); + + var meter1 = new Meter($"{Utils.GetCurrentMethodName()}.{temporality}.1"); + var meter2 = new Meter($"{Utils.GetCurrentMethodName()}.{temporality}.2"); + var counter1 = meter1.CreateCounter("counterFromMeter1"); + var counter2 = meter2.CreateCounter("counterFromMeter2"); + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter1.Name) + .AddMeter(meter2.Name) + .AddReader(new BaseExportingMetricReader(new InMemoryExporter(exportedItems)) + { + Temporality = temporality, + }) + .Build(); + + counter1.Add(10, new KeyValuePair("key", "value")); + counter2.Add(10, new KeyValuePair("key", "value")); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Equal(2, exportedItems.Count); + exportedItems.Clear(); + + counter1.Add(10, new KeyValuePair("key", "value")); + counter2.Add(10, new KeyValuePair("key", "value")); + meter1.Dispose(); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Equal(2, exportedItems.Count); + exportedItems.Clear(); + + counter1.Add(10, new KeyValuePair("key", "value")); + counter2.Add(10, new KeyValuePair("key", "value")); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Single(exportedItems); + exportedItems.Clear(); + + counter1.Add(10, new KeyValuePair("key", "value")); + counter2.Add(10, new KeyValuePair("key", "value")); + meter2.Dispose(); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Single(exportedItems); + exportedItems.Clear(); + + counter1.Add(10, new KeyValuePair("key", "value")); + counter2.Add(10, new KeyValuePair("key", "value")); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Empty(exportedItems); + } + + [Theory] + [InlineData(AggregationTemporality.Cumulative)] + [InlineData(AggregationTemporality.Delta)] + public void TestMetricPointCap(AggregationTemporality temporality) + { + var exportedItems = new List(); + + int MetricPointCount() + { + var count = 0; + + foreach (var metric in exportedItems) + { + foreach (ref readonly var metricPoint in metric.GetMetricPoints()) + { + count++; + } + } + + return count; + } + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{temporality}"); + var counterLong = meter.CreateCounter("mycounterCapTest"); + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddReader(new BaseExportingMetricReader(new InMemoryExporter(exportedItems)) + { + Temporality = temporality, + }) + .Build(); + + // Make one Add with no tags. + // as currently we reserve 0th index + // for no tag point! + // This may be changed later. + counterLong.Add(10); + for (int i = 0; i < MeterProviderBuilderBase.MaxMetricPointsPerMetricDefault + 1; i++) + { + counterLong.Add(10, new KeyValuePair("key", "value" + i)); + } + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Equal(MeterProviderBuilderBase.MaxMetricPointsPerMetricDefault, MetricPointCount()); + + exportedItems.Clear(); + counterLong.Add(10); + for (int i = 0; i < MeterProviderBuilderBase.MaxMetricPointsPerMetricDefault + 1; i++) + { + counterLong.Add(10, new KeyValuePair("key", "value" + i)); + } + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Equal(MeterProviderBuilderBase.MaxMetricPointsPerMetricDefault, MetricPointCount()); + + counterLong.Add(10); + for (int i = 0; i < MeterProviderBuilderBase.MaxMetricPointsPerMetricDefault + 1; i++) + { + counterLong.Add(10, new KeyValuePair("key", "value" + i)); + } + + // These updates would be dropped. + counterLong.Add(10, new KeyValuePair("key", "valueA")); + counterLong.Add(10, new KeyValuePair("key", "valueB")); + counterLong.Add(10, new KeyValuePair("key", "valueC")); + exportedItems.Clear(); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Equal(MeterProviderBuilderBase.MaxMetricPointsPerMetricDefault, MetricPointCount()); + } + + [Fact] + public void MultithreadedLongCounterTest() + { + this.MultithreadedCounterTest(deltaLongValueUpdatedByEachCall); + } + + [Fact] + public void MultithreadedDoubleCounterTest() + { + this.MultithreadedCounterTest(deltaDoubleValueUpdatedByEachCall); + } + + [Fact] + public void MultithreadedLongHistogramTest() + { + var expected = new long[11]; + for (var i = 0; i < expected.Length; i++) + { + expected[i] = numberOfThreads * numberOfMetricUpdateByEachThread; + } + + // Metric.DefaultHistogramBounds: 0, 5, 10, 25, 50, 75, 100, 250, 500, 1000 + var values = new long[] { -1, 1, 6, 20, 40, 60, 80, 200, 300, 600, 1001 }; + + this.MultithreadedHistogramTest(expected, values); + } + + [Fact] + public void MultithreadedDoubleHistogramTest() + { + var expected = new long[11]; + for (var i = 0; i < expected.Length; i++) + { + expected[i] = numberOfThreads * numberOfMetricUpdateByEachThread; } - var timeTakenInMilliseconds = sw.ElapsedMilliseconds; - this.output.WriteLine($"Took {timeTakenInMilliseconds} msecs. Total threads: {numberOfThreads}, each thread doing {numberOfMetricUpdateByEachThread} recordings."); + // Metric.DefaultHistogramBounds: 0, 5, 10, 25, 50, 75, 100, 250, 500, 1000 + var values = new double[] { -1.0, 1.0, 6.0, 20.0, 40.0, 60.0, 80.0, 200.0, 300.0, 600.0, 1001.0 }; + + this.MultithreadedHistogramTest(expected, values); + } + + [Theory] + [MemberData(nameof(MetricTestData.InvalidInstrumentNames), MemberType = typeof(MetricTestData))] + public void InstrumentWithInvalidNameIsIgnoredTest(string instrumentName) + { + var exportedItems = new List(); + + using var meter = new Meter("InstrumentWithInvalidNameIsIgnoredTest"); + + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems) + .Build(); + + var counterLong = meter.CreateCounter(instrumentName); + counterLong.Add(10); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + // instrument should have been ignored + // as its name does not comply with the specification + Assert.Empty(exportedItems); + } + + [Theory] + [MemberData(nameof(MetricTestData.ValidInstrumentNames), MemberType = typeof(MetricTestData))] + public void InstrumentWithValidNameIsExportedTest(string name) + { + var exportedItems = new List(); + + using var meter = new Meter("InstrumentValidNameIsExportedTest"); + + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddInMemoryExporter(exportedItems) + .Build(); + + var counterLong = meter.CreateCounter(name); + counterLong.Add(10); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + // Expecting one metric stream. + Assert.Single(exportedItems); + var metric = exportedItems[0]; + Assert.Equal(name, metric.Name); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void SetupSdkProviderWithNoReader(bool hasViews) + { + // This test ensures that MeterProviderSdk can be set up without any reader + using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{hasViews}"); + var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name); + + if (hasViews) + { + meterProviderBuilder.AddView("counter", "renamedCounter"); + } + + using var meterProvider = meterProviderBuilder.Build(); + + var counter = meter.CreateCounter("counter"); + + counter.Add(10, new KeyValuePair("key", "value")); + } + + private static long GetLongSum(List metrics) + { + long sum = 0; + foreach (var metric in metrics) + { + foreach (ref readonly var metricPoint in metric.GetMetricPoints()) + { + if (metric.MetricType.IsSum()) + { + sum += metricPoint.GetSumLong(); + } + else + { + sum += metricPoint.GetGaugeLastValueLong(); + } + } + } + + return sum; + } + + private static double GetDoubleSum(List metrics) + { + double sum = 0; + foreach (var metric in metrics) + { + foreach (ref readonly var metricPoint in metric.GetMetricPoints()) + { + if (metric.MetricType.IsSum()) + { + sum += metricPoint.GetSumDouble(); + } + else + { + sum += metricPoint.GetGaugeLastValueDouble(); + } + } + } - meterProvider.Dispose(); - pullProcessor.PullRequest(); + return sum; + } - long sumReceived = 0; - foreach (var metricItem in metricItems) + private static int GetNumberOfMetricPoints(List metrics) + { + int count = 0; + foreach (var metric in metrics) { - var metrics = metricItem.Metrics; - foreach (var metric in metrics) + foreach (ref readonly var metricPoint in metric.GetMetricPoints()) { - sumReceived += (metric as ISumMetricLong).LongSum; + count++; } } - var expectedSum = deltaValueUpdatedByEachCall * numberOfMetricUpdateByEachThread * numberOfThreads; - Assert.Equal(expectedSum, sumReceived); + return count; + } + + // Provide tags input sorted by Key + private static void CheckTagsForNthMetricPoint(List metrics, List> tags, int n) + { + var metric = metrics[0]; + var metricPointEnumerator = metric.GetMetricPoints().GetEnumerator(); + + for (int i = 0; i < n; i++) + { + Assert.True(metricPointEnumerator.MoveNext()); + } + + int index = 0; + var metricPoint = metricPointEnumerator.Current; + foreach (var tag in metricPoint.Tags) + { + Assert.Equal(tags[index].Key, tag.Key); + Assert.Equal(tags[index].Value, tag.Value); + index++; + } + } + + private static void CounterUpdateThread(object obj) + where T : struct, IComparable + { + if (obj is not UpdateThreadArguments arguments) + { + throw new Exception("Invalid args"); + } + + var mre = arguments.MreToBlockUpdateThread; + var mreToEnsureAllThreadsStart = arguments.MreToEnsureAllThreadsStart; + var counter = arguments.Instrument as Counter; + var valueToUpdate = arguments.ValuesToRecord[0]; + if (Interlocked.Increment(ref arguments.ThreadsStartedCount) == numberOfThreads) + { + mreToEnsureAllThreadsStart.Set(); + } + + // Wait until signalled to start calling update on aggregator + mre.WaitOne(); + + for (int i = 0; i < numberOfMetricUpdateByEachThread; i++) + { + counter.Add(valueToUpdate, new KeyValuePair("verb", "GET")); + } } - private static void CounterUpdateThread(object obj) + private static void HistogramUpdateThread(object obj) + where T : struct, IComparable { - var arguments = obj as UpdateThreadArguments; - if (arguments == null) + if (obj is not UpdateThreadArguments arguments) { throw new Exception("Invalid args"); } var mre = arguments.MreToBlockUpdateThread; var mreToEnsureAllThreadsStart = arguments.MreToEnsureAllThreadsStart; - var counter = arguments.Counter; + var histogram = arguments.Instrument as Histogram; if (Interlocked.Increment(ref arguments.ThreadsStartedCount) == numberOfThreads) { @@ -134,16 +923,127 @@ private static void CounterUpdateThread(object obj) for (int i = 0; i < numberOfMetricUpdateByEachThread; i++) { - counter.Add(deltaValueUpdatedByEachCall, new KeyValuePair("verb", "GET")); + for (int j = 0; j < arguments.ValuesToRecord.Length; j++) + { + histogram.Record(arguments.ValuesToRecord[j]); + } } } - private class UpdateThreadArguments + private void MultithreadedCounterTest(T deltaValueUpdatedByEachCall) + where T : struct, IComparable + { + var metricItems = new List(); + var metricReader = new BaseExportingMetricReader(new InMemoryExporter(metricItems)); + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{typeof(T).Name}.{deltaValueUpdatedByEachCall}"); + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddReader(metricReader) + .Build(); + + var argToThread = new UpdateThreadArguments + { + ValuesToRecord = new T[] { deltaValueUpdatedByEachCall }, + Instrument = meter.CreateCounter("counter"), + MreToBlockUpdateThread = new ManualResetEvent(false), + MreToEnsureAllThreadsStart = new ManualResetEvent(false), + }; + + Thread[] t = new Thread[numberOfThreads]; + for (int i = 0; i < numberOfThreads; i++) + { + t[i] = new Thread(CounterUpdateThread); + t[i].Start(argToThread); + } + + argToThread.MreToEnsureAllThreadsStart.WaitOne(); + Stopwatch sw = Stopwatch.StartNew(); + argToThread.MreToBlockUpdateThread.Set(); + + for (int i = 0; i < numberOfThreads; i++) + { + t[i].Join(); + } + + this.output.WriteLine($"Took {sw.ElapsedMilliseconds} msecs. Total threads: {numberOfThreads}, each thread doing {numberOfMetricUpdateByEachThread} recordings."); + + metricReader.Collect(); + + if (typeof(T) == typeof(long)) + { + var sumReceived = GetLongSum(metricItems); + var expectedSum = deltaLongValueUpdatedByEachCall * numberOfMetricUpdateByEachThread * numberOfThreads; + Assert.Equal(expectedSum, sumReceived); + } + else if (typeof(T) == typeof(double)) + { + var sumReceived = GetDoubleSum(metricItems); + var expectedSum = deltaDoubleValueUpdatedByEachCall * numberOfMetricUpdateByEachThread * numberOfThreads; + Assert.Equal(expectedSum, sumReceived, 2); + } + } + + private void MultithreadedHistogramTest(long[] expected, T[] values) + where T : struct, IComparable + { + var bucketCounts = new long[11]; + var metricReader = new BaseExportingMetricReader(new TestExporter(batch => + { + foreach (var metric in batch) + { + foreach (var metricPoint in metric.GetMetricPoints()) + { + bucketCounts = metricPoint.GetHistogramBuckets().RunningBucketCounts; + } + } + })); + + using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{typeof(T).Name}"); + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddReader(metricReader) + .Build(); + + var argsToThread = new UpdateThreadArguments + { + Instrument = meter.CreateHistogram("histogram"), + MreToBlockUpdateThread = new ManualResetEvent(false), + MreToEnsureAllThreadsStart = new ManualResetEvent(false), + ValuesToRecord = values, + }; + + Thread[] t = new Thread[numberOfThreads]; + for (int i = 0; i < numberOfThreads; i++) + { + t[i] = new Thread(HistogramUpdateThread); + t[i].Start(argsToThread); + } + + argsToThread.MreToEnsureAllThreadsStart.WaitOne(); + Stopwatch sw = Stopwatch.StartNew(); + argsToThread.MreToBlockUpdateThread.Set(); + + for (int i = 0; i < numberOfThreads; i++) + { + t[i].Join(); + } + + this.output.WriteLine($"Took {sw.ElapsedMilliseconds} msecs. Total threads: {numberOfThreads}, each thread doing {numberOfMetricUpdateByEachThread * values.Length} recordings."); + + metricReader.Collect(); + + Assert.Equal(expected, bucketCounts); + } + + private class UpdateThreadArguments + where T : struct, IComparable { public ManualResetEvent MreToBlockUpdateThread; public ManualResetEvent MreToEnsureAllThreadsStart; public int ThreadsStartedCount; - public Counter Counter; + public Instrument Instrument; + public T[] ValuesToRecord; } } } diff --git a/test/OpenTelemetry.Tests/Metrics/MetricExporterTests.cs b/test/OpenTelemetry.Tests/Metrics/MetricExporterTests.cs new file mode 100644 index 00000000000..6416551ed9b --- /dev/null +++ b/test/OpenTelemetry.Tests/Metrics/MetricExporterTests.cs @@ -0,0 +1,103 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using Xunit; + +namespace OpenTelemetry.Metrics.Tests +{ + public class MetricExporterTests + { + [Theory] + [InlineData(ExportModes.Push)] + [InlineData(ExportModes.Pull)] + [InlineData(ExportModes.Pull | ExportModes.Push)] + public void FlushMetricExporterTest(ExportModes mode) + { + BaseExporter exporter = null; + + switch (mode) + { + case ExportModes.Push: + exporter = new PushOnlyMetricExporter(); + break; + case ExportModes.Pull: + exporter = new PullOnlyMetricExporter(); + break; + case ExportModes.Pull | ExportModes.Push: + exporter = new PushPullMetricExporter(); + break; + } + + var reader = new BaseExportingMetricReader(exporter); + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddReader(reader) + .Build(); + + switch (mode) + { + case ExportModes.Push: + Assert.True(reader.Collect()); + Assert.True(meterProvider.ForceFlush()); + break; + case ExportModes.Pull: + Assert.False(reader.Collect()); + Assert.False(meterProvider.ForceFlush()); + Assert.True((exporter as IPullMetricExporter).Collect(-1)); + break; + case ExportModes.Pull | ExportModes.Push: + Assert.True(reader.Collect()); + Assert.True(meterProvider.ForceFlush()); + break; + } + } + + [ExportModes(ExportModes.Push)] + private class PushOnlyMetricExporter : BaseExporter + { + public override ExportResult Export(in Batch batch) + { + return ExportResult.Success; + } + } + + [ExportModes(ExportModes.Pull)] + private class PullOnlyMetricExporter : BaseExporter, IPullMetricExporter + { + private Func funcCollect; + + public Func Collect + { + get => this.funcCollect; + set { this.funcCollect = value; } + } + + public override ExportResult Export(in Batch batch) + { + return ExportResult.Success; + } + } + + [ExportModes(ExportModes.Pull | ExportModes.Push)] + private class PushPullMetricExporter : BaseExporter + { + public override ExportResult Export(in Batch batch) + { + return ExportResult.Success; + } + } + } +} diff --git a/test/OpenTelemetry.Tests/Metrics/MetricTestData.cs b/test/OpenTelemetry.Tests/Metrics/MetricTestData.cs new file mode 100644 index 00000000000..0664dc18c00 --- /dev/null +++ b/test/OpenTelemetry.Tests/Metrics/MetricTestData.cs @@ -0,0 +1,53 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Collections.Generic; + +namespace OpenTelemetry.Metrics.Tests +{ + public class MetricTestData + { + public static IEnumerable InvalidInstrumentNames + => new List + { + new object[] { " " }, + new object[] { "-first-char-not-alphabetic" }, + new object[] { "1first-char-not-alphabetic" }, + new object[] { "invalid+separator" }, + new object[] { new string('m', 64) }, + }; + + public static IEnumerable ValidInstrumentNames + => new List + { + new object[] { "m" }, + new object[] { "first-char-alphabetic" }, + new object[] { "my-2-instrument" }, + new object[] { "my.metric" }, + new object[] { "my_metric2" }, + new object[] { new string('m', 63) }, + }; + + 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 } }, + }; + } +} diff --git a/test/OpenTelemetry.Tests/Metrics/MetricViewTests.cs b/test/OpenTelemetry.Tests/Metrics/MetricViewTests.cs new file mode 100644 index 00000000000..2d75c883ac5 --- /dev/null +++ b/test/OpenTelemetry.Tests/Metrics/MetricViewTests.cs @@ -0,0 +1,581 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using System.Diagnostics.Metrics; +using OpenTelemetry.Tests; +using Xunit; +using Xunit.Abstractions; + +namespace OpenTelemetry.Metrics.Tests +{ + public class MetricViewTests + { + private const int MaxTimeToAllowForFlush = 10000; + private readonly ITestOutputHelper output; + + public MetricViewTests(ITestOutputHelper output) + { + this.output = output; + } + + [Fact] + public void ViewToRenameMetric() + { + using var meter = new Meter(Utils.GetCurrentMethodName()); + var exportedItems = new List(); + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddView("name1", "renamed") + .AddInMemoryExporter(exportedItems) + .Build(); + + // Expecting one metric stream. + var counterLong = meter.CreateCounter("name1"); + counterLong.Add(10); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Single(exportedItems); + var metric = exportedItems[0]; + Assert.Equal("renamed", metric.Name); + } + + [Theory] + [MemberData(nameof(MetricTestData.InvalidInstrumentNames), MemberType = typeof(MetricTestData))] + public void AddViewWithInvalidNameThrowsArgumentException(string viewNewName) + { + var exportedItems = new List(); + + using var meter1 = new Meter("AddViewWithInvalidNameThrowsArgumentException"); + + var ex = Assert.Throws(() => Sdk.CreateMeterProviderBuilder() + .AddMeter(meter1.Name) + .AddView("name1", viewNewName) + .AddInMemoryExporter(exportedItems) + .Build()); + + Assert.Contains($"Custom view name {viewNewName} is invalid.", ex.Message); + + ex = Assert.Throws(() => Sdk.CreateMeterProviderBuilder() + .AddMeter(meter1.Name) + .AddView("name1", new MetricStreamConfiguration { Name = viewNewName }) + .AddInMemoryExporter(exportedItems) + .Build()); + + Assert.Contains($"Custom view name {viewNewName} is invalid.", ex.Message); + } + + [Fact] + public void AddViewWithNullMetricStreamConfigurationThrowsArgumentnullException() + { + var exportedItems = new List(); + + using var meter1 = new Meter("AddViewWithInvalidNameThrowsArgumentException"); + + Assert.Throws(() => Sdk.CreateMeterProviderBuilder() + .AddMeter(meter1.Name) + .AddView("name1", (MetricStreamConfiguration)null) + .AddInMemoryExporter(exportedItems) + .Build()); + } + + [Fact] + public void AddViewWithNameThrowsInvalidArgumentExceptionWhenConflict() + { + var exportedItems = new List(); + + using var meter1 = new Meter("AddViewWithGuaranteedConflictThrowsInvalidArgumentException"); + + Assert.Throws(() => Sdk.CreateMeterProviderBuilder() + .AddMeter(meter1.Name) + .AddView("instrumenta.*", name: "newname") + .AddInMemoryExporter(exportedItems) + .Build()); + } + + [Fact] + public void AddViewWithNameInMetricStreamConfigurationThrowsInvalidArgumentExceptionWhenConflict() + { + var exportedItems = new List(); + + using var meter1 = new Meter("AddViewWithGuaranteedConflictThrowsInvalidArgumentException"); + + Assert.Throws(() => Sdk.CreateMeterProviderBuilder() + .AddMeter(meter1.Name) + .AddView("instrumenta.*", new MetricStreamConfiguration() { Name = "newname" }) + .AddInMemoryExporter(exportedItems) + .Build()); + } + + [Theory] + [MemberData(nameof(MetricTestData.InvalidHistogramBoundaries), MemberType = typeof(MetricTestData))] + public void AddViewWithInvalidHistogramBoundsThrowsArgumentException(double[] boundaries) + { + var ex = Assert.Throws(() => Sdk.CreateMeterProviderBuilder() + .AddView("name1", new ExplicitBucketHistogramConfiguration { Boundaries = boundaries })); + + Assert.Contains("Histogram boundaries must be in ascending order with distinct values", ex.Message); + } + + [Theory] + [MemberData(nameof(MetricTestData.ValidInstrumentNames), MemberType = typeof(MetricTestData))] + public void ViewWithValidNameExported(string viewNewName) + { + var exportedItems = new List(); + + using var meter1 = new Meter("ViewWithInvalidNameIgnoredTest"); + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter1.Name) + .AddView("name1", viewNewName) + .AddInMemoryExporter(exportedItems) + .Build(); + + var counterLong = meter1.CreateCounter("name1"); + counterLong.Add(10); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + // Expecting one metric stream. + Assert.Single(exportedItems); + var metric = exportedItems[0]; + Assert.Equal(viewNewName, metric.Name); + } + + [Fact] + public void ViewToRenameMetricConditionally() + { + using var meter1 = new Meter($"{Utils.GetCurrentMethodName()}.1"); + using var meter2 = new Meter($"{Utils.GetCurrentMethodName()}.2"); + + var exportedItems = new List(); + + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter1.Name) + .AddMeter(meter2.Name) + .AddView((instrument) => + { + if (instrument.Meter.Name.Equals(meter2.Name, StringComparison.OrdinalIgnoreCase) + && instrument.Name.Equals("name1", StringComparison.OrdinalIgnoreCase)) + { + return new MetricStreamConfiguration() { Name = "name1_Renamed", Description = "new description" }; + } + else + { + return null; + } + }) + .AddInMemoryExporter(exportedItems) + .Build(); + + // Without views only 1 stream would be + // exported (the 2nd one gets dropped due to + // name conflict). Due to renaming with Views, + // we expect 2 metric streams here. + var counter1 = meter1.CreateCounter("name1", "unit", "original_description"); + var counter2 = meter2.CreateCounter("name1", "unit", "original_description"); + counter1.Add(10); + counter2.Add(10); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Equal(2, exportedItems.Count); + Assert.Equal("name1", exportedItems[0].Name); + Assert.Equal("name1_Renamed", exportedItems[1].Name); + Assert.Equal("original_description", exportedItems[0].Description); + Assert.Equal("new description", exportedItems[1].Description); + } + + [Theory] + [MemberData(nameof(MetricTestData.InvalidInstrumentNames), MemberType = typeof(MetricTestData))] + public void ViewWithInvalidNameIgnoredConditionally(string viewNewName) + { + using var meter1 = new Meter("ViewToRenameMetricConditionallyTest"); + var exportedItems = new List(); + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter1.Name) + + // since here it's a func, we can't validate the name right away + // so the view is allowed to be added, but upon instrument creation it's going to be ignored. + .AddView((instrument) => + { + if (instrument.Meter.Name.Equals(meter1.Name, StringComparison.OrdinalIgnoreCase) + && instrument.Name.Equals("name1", StringComparison.OrdinalIgnoreCase)) + { + // invalid instrument name as per the spec + return new MetricStreamConfiguration() { Name = viewNewName, Description = "new description" }; + } + else + { + return null; + } + }) + .AddInMemoryExporter(exportedItems) + .Build(); + + // We should expect 1 metric here, + // but because the MetricStreamName passed is invalid, the instrument is ignored + var counter1 = meter1.CreateCounter("name1", "unit", "original_description"); + counter1.Add(10); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + Assert.Empty(exportedItems); + } + + [Theory] + [MemberData(nameof(MetricTestData.ValidInstrumentNames), MemberType = typeof(MetricTestData))] + public void ViewWithValidNameConditionally(string viewNewName) + { + using var meter1 = new Meter("ViewToRenameMetricConditionallyTest"); + var exportedItems = new List(); + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter1.Name) + .AddView((instrument) => + { + if (instrument.Meter.Name.Equals(meter1.Name, StringComparison.OrdinalIgnoreCase) + && instrument.Name.Equals("name1", StringComparison.OrdinalIgnoreCase)) + { + // invalid instrument name as per the spec + return new MetricStreamConfiguration() { Name = viewNewName, Description = "new description" }; + } + else + { + return null; + } + }) + .AddInMemoryExporter(exportedItems) + .Build(); + + // Expecting one metric stream. + var counter1 = meter1.CreateCounter("name1", "unit", "original_description"); + counter1.Add(10); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + // Expecting one metric stream. + Assert.Single(exportedItems); + var metric = exportedItems[0]; + Assert.Equal(viewNewName, metric.Name); + } + + [Fact] + public void ViewWithNullCustomNameTakesInstrumentName() + { + var exportedItems = new List(); + + using var meter = new Meter("ViewToRenameMetricConditionallyTest"); + + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddView((instrument) => + { + if (instrument.Name.Equals("name1", StringComparison.OrdinalIgnoreCase)) + { + // null View name + return new MetricStreamConfiguration() { }; + } + else + { + return null; + } + }) + .AddInMemoryExporter(exportedItems) + .Build(); + + // Expecting one metric stream. + // Since the View name was null, the instrument name was used instead + var counter1 = meter.CreateCounter("name1", "unit", "original_description"); + counter1.Add(10); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + + // Expecting one metric stream. + Assert.Single(exportedItems); + var metric = exportedItems[0]; + Assert.Equal(counter1.Name, metric.Name); + } + + [Fact] + public void ViewToProduceMultipleStreamsFromInstrument() + { + using var meter = new Meter(Utils.GetCurrentMethodName()); + var exportedItems = new List(); + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddView("name1", "renamedStream1") + .AddView("name1", "renamedStream2") + .AddInMemoryExporter(exportedItems) + .Build(); + + // Expecting two metric stream. + var counterLong = meter.CreateCounter("name1"); + counterLong.Add(10); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Equal(2, exportedItems.Count); + Assert.Equal("renamedStream1", exportedItems[0].Name); + Assert.Equal("renamedStream2", exportedItems[1].Name); + } + + [Fact] + public void ViewToProduceMultipleStreamsWithDuplicatesFromInstrument() + { + using var meter = new Meter(Utils.GetCurrentMethodName()); + var exportedItems = new List(); + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddView("name1", "renamedStream1") + .AddView("name1", "renamedStream2") + .AddView("name1", "renamedStream2") + .AddInMemoryExporter(exportedItems) + .Build(); + + // Expecting two metric stream. + // the .AddView("name1", "renamedStream2") + // won't produce new Metric as the name + // conflicts. + var counterLong = meter.CreateCounter("name1"); + counterLong.Add(10); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Equal(2, exportedItems.Count); + Assert.Equal("renamedStream1", exportedItems[0].Name); + Assert.Equal("renamedStream2", exportedItems[1].Name); + } + + [Fact] + public void ViewToProduceCustomHistogramBound() + { + using var meter = new Meter(Utils.GetCurrentMethodName()); + var exportedItems = new List(); + var boundaries = new double[] { 10, 20 }; + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddView("MyHistogram", new ExplicitBucketHistogramConfiguration() { Name = "MyHistogramDefaultBound" }) + .AddView("MyHistogram", new ExplicitBucketHistogramConfiguration() { Boundaries = boundaries }) + .AddInMemoryExporter(exportedItems) + .Build(); + + var histogram = meter.CreateHistogram("MyHistogram"); + histogram.Record(-10); + histogram.Record(0); + histogram.Record(1); + histogram.Record(9); + histogram.Record(10); + histogram.Record(11); + histogram.Record(19); + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Equal(2, exportedItems.Count); + var metricDefault = exportedItems[0]; + var metricCustom = exportedItems[1]; + + Assert.Equal("MyHistogramDefaultBound", metricDefault.Name); + Assert.Equal("MyHistogram", metricCustom.Name); + + List metricPointsDefault = new List(); + foreach (ref readonly var mp in metricDefault.GetMetricPoints()) + { + metricPointsDefault.Add(mp); + } + + Assert.Single(metricPointsDefault); + var histogramPoint = metricPointsDefault[0]; + + var count = histogramPoint.GetHistogramCount(); + var sum = histogramPoint.GetHistogramSum(); + + Assert.Equal(40, sum); + Assert.Equal(7, count); + + int index = 0; + int actualCount = 0; + var expectedBucketCounts = new long[] { 2, 1, 2, 2, 0, 0, 0, 0, 0, 0, 0 }; + foreach (var histogramMeasurement in histogramPoint.GetHistogramBuckets()) + { + Assert.Equal(expectedBucketCounts[index], histogramMeasurement.BucketCount); + index++; + actualCount++; + } + + Assert.Equal(Metric.DefaultHistogramBounds.Length + 1, actualCount); + + List metricPointsCustom = new List(); + foreach (ref readonly var mp in metricCustom.GetMetricPoints()) + { + metricPointsCustom.Add(mp); + } + + Assert.Single(metricPointsCustom); + histogramPoint = metricPointsCustom[0]; + + count = histogramPoint.GetHistogramCount(); + sum = histogramPoint.GetHistogramSum(); + + Assert.Equal(40, sum); + Assert.Equal(7, count); + + index = 0; + actualCount = 0; + expectedBucketCounts = new long[] { 5, 2, 0 }; + foreach (var histogramMeasurement in histogramPoint.GetHistogramBuckets()) + { + Assert.Equal(expectedBucketCounts[index], histogramMeasurement.BucketCount); + index++; + actualCount++; + } + + Assert.Equal(boundaries.Length + 1, actualCount); + } + + [Fact] + public void ViewToSelectTagKeys() + { + using var meter = new Meter(Utils.GetCurrentMethodName()); + var exportedItems = new List(); + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddView("FruitCounter", new MetricStreamConfiguration() + { TagKeys = new string[] { "name" }, Name = "NameOnly" }) + .AddView("FruitCounter", new MetricStreamConfiguration() + { TagKeys = new string[] { "size" }, Name = "SizeOnly" }) + .AddView("FruitCounter", new MetricStreamConfiguration() + { TagKeys = new string[] { }, Name = "NoTags" }) + .AddInMemoryExporter(exportedItems) + .Build(); + + var counter = meter.CreateCounter("FruitCounter"); + counter.Add(10, new("name", "apple"), new("color", "red"), new("size", "small")); + counter.Add(10, new("name", "apple"), new("color", "red"), new("size", "small")); + + counter.Add(10, new("name", "apple"), new("color", "red"), new("size", "medium")); + counter.Add(10, new("name", "apple"), new("color", "red"), new("size", "medium")); + + counter.Add(10, new("name", "apple"), new("color", "red"), new("size", "large")); + counter.Add(10, new("name", "apple"), new("color", "red"), new("size", "large")); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Equal(3, exportedItems.Count); + var metric = exportedItems[0]; + Assert.Equal("NameOnly", metric.Name); + List metricPoints = new List(); + foreach (ref readonly var mp in metric.GetMetricPoints()) + { + metricPoints.Add(mp); + } + + // Only one point expected "apple" + Assert.Single(metricPoints); + + metric = exportedItems[1]; + Assert.Equal("SizeOnly", metric.Name); + metricPoints.Clear(); + foreach (ref readonly var mp in metric.GetMetricPoints()) + { + metricPoints.Add(mp); + } + + // 3 points small,medium,large expected + Assert.Equal(3, metricPoints.Count); + + metric = exportedItems[2]; + Assert.Equal("NoTags", metric.Name); + metricPoints.Clear(); + foreach (ref readonly var mp in metric.GetMetricPoints()) + { + metricPoints.Add(mp); + } + + // Single point expected. + Assert.Single(metricPoints); + } + + [Fact] + public void ViewToDropSingleInstrument() + { + using var meter = new Meter(Utils.GetCurrentMethodName()); + var exportedItems = new List(); + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddView("counterNotInteresting", new MetricStreamConfiguration() { Aggregation = Aggregation.Drop }) + .AddInMemoryExporter(exportedItems) + .Build(); + + // Expecting one metric stream. + var counterInteresting = meter.CreateCounter("counterInteresting"); + var counterNotInteresting = meter.CreateCounter("counterNotInteresting"); + counterInteresting.Add(10); + counterNotInteresting.Add(10); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Single(exportedItems); + var metric = exportedItems[0]; + Assert.Equal("counterInteresting", metric.Name); + } + + [Fact] + public void ViewToDropMultipleInstruments() + { + using var meter = new Meter(Utils.GetCurrentMethodName()); + var exportedItems = new List(); + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddView("server*", new MetricStreamConfiguration() { Aggregation = Aggregation.Drop }) + .AddInMemoryExporter(exportedItems) + .Build(); + + // Expecting two client metric streams as both server* are dropped. + var serverRequests = meter.CreateCounter("server.requests"); + var serverExceptions = meter.CreateCounter("server.exceptions"); + var clientRequests = meter.CreateCounter("client.requests"); + var clientExceptions = meter.CreateCounter("client.exceptions"); + serverRequests.Add(10); + serverExceptions.Add(10); + clientRequests.Add(10); + clientExceptions.Add(10); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Equal(2, exportedItems.Count); + Assert.Equal("client.requests", exportedItems[0].Name); + Assert.Equal("client.exceptions", exportedItems[1].Name); + } + + [Fact] + public void ViewToDropAndRetainInstrument() + { + using var meter = new Meter(Utils.GetCurrentMethodName()); + var exportedItems = new List(); + using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddView("server.requests", MetricStreamConfiguration.Drop) + .AddView("server.requests", "server.request_renamed") + .AddInMemoryExporter(exportedItems) + .Build(); + + // Expecting one metric stream even though a View is asking + // to drop the instrument, because another View is matching + // the instrument, which asks to aggregate with defaults + // and a use a new name for the resulting metric. + var serverRequests = meter.CreateCounter("server.requests"); + serverRequests.Add(10); + + meterProvider.ForceFlush(MaxTimeToAllowForFlush); + Assert.Single(exportedItems); + Assert.Equal("server.request_renamed", exportedItems[0].Name); + } + + [Fact] + public void MetricStreamConfigurationForDropMustNotAllowOverriding() + { + MetricStreamConfiguration.Drop.Aggregation = Aggregation.Histogram; + Assert.Equal(Aggregation.Drop, MetricStreamConfiguration.Drop.Aggregation); + } + } +} diff --git a/test/OpenTelemetry.Tests/Metrics/MultipleReadersTests.cs b/test/OpenTelemetry.Tests/Metrics/MultipleReadersTests.cs new file mode 100644 index 00000000000..ba08b0644a5 --- /dev/null +++ b/test/OpenTelemetry.Tests/Metrics/MultipleReadersTests.cs @@ -0,0 +1,125 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Collections.Generic; +using System.Diagnostics.Metrics; +using OpenTelemetry.Exporter; +using OpenTelemetry.Tests; +using Xunit; + +namespace OpenTelemetry.Metrics.Tests +{ + public class MultipleReadersTests + { + [Theory] + [InlineData(AggregationTemporality.Delta, false)] + [InlineData(AggregationTemporality.Delta, true)] + [InlineData(AggregationTemporality.Cumulative, false)] + [InlineData(AggregationTemporality.Cumulative, true)] + public void SdkSupportsMultipleReaders(AggregationTemporality aggregationTemporality, bool hasViews) + { + var exportedItems1 = new List(); + using var deltaExporter1 = new InMemoryExporter(exportedItems1); + using var deltaReader1 = new BaseExportingMetricReader(deltaExporter1) + { + Temporality = AggregationTemporality.Delta, + }; + + var exportedItems2 = new List(); + using var deltaExporter2 = new InMemoryExporter(exportedItems2); + using var deltaReader2 = new BaseExportingMetricReader(deltaExporter2) + { + Temporality = aggregationTemporality, + }; + using var meter = new Meter($"{Utils.GetCurrentMethodName()}.{aggregationTemporality}.{hasViews}"); + + var counter = meter.CreateCounter("counter"); + + int index = 0; + var values = new long[] { 100, 200, 300, 400 }; + long GetValue() => values[index++]; + var gauge = meter.CreateObservableGauge("gauge", () => GetValue()); + + var meterProviderBuilder = Sdk.CreateMeterProviderBuilder() + .AddMeter(meter.Name) + .AddReader(deltaReader1) + .AddReader(deltaReader2); + + if (hasViews) + { + meterProviderBuilder.AddView("counter", "renamedCounter"); + } + + using var meterProvider = meterProviderBuilder.Build(); + + counter.Add(10, new KeyValuePair("key", "value")); + + meterProvider.ForceFlush(); + + Assert.Equal(2, exportedItems1.Count); + Assert.Equal(2, exportedItems2.Count); + + // Check value exported for Counter + this.AssertLongSumValueForMetric(exportedItems1[0], 10); + this.AssertLongSumValueForMetric(exportedItems2[0], 10); + + // Check value exported for Gauge + this.AssertLongSumValueForMetric(exportedItems1[1], 100); + this.AssertLongSumValueForMetric(exportedItems2[1], 200); + + exportedItems1.Clear(); + exportedItems2.Clear(); + + counter.Add(15, new KeyValuePair("key", "value")); + + meterProvider.ForceFlush(); + + Assert.Equal(2, exportedItems1.Count); + Assert.Equal(2, exportedItems2.Count); + + // Check value exported for Counter + this.AssertLongSumValueForMetric(exportedItems1[0], 15); + if (aggregationTemporality == AggregationTemporality.Delta) + { + this.AssertLongSumValueForMetric(exportedItems2[0], 15); + } + else + { + this.AssertLongSumValueForMetric(exportedItems2[0], 25); + } + + // Check value exported for Gauge + this.AssertLongSumValueForMetric(exportedItems1[1], 300); + this.AssertLongSumValueForMetric(exportedItems2[1], 400); + } + + private void AssertLongSumValueForMetric(Metric metric, long value) + { + var metricPoints = metric.GetMetricPoints(); + var metricPointsEnumerator = metricPoints.GetEnumerator(); + Assert.True(metricPointsEnumerator.MoveNext()); // One MetricPoint is emitted for the Metric + ref readonly var metricPointForFirstExport = ref metricPointsEnumerator.Current; + if (metric.MetricType.IsSum()) + { + Assert.Equal(value, metricPointForFirstExport.GetSumLong()); + } + else + { + Assert.Equal(value, metricPointForFirstExport.GetGaugeLastValueLong()); + } + } + } +} diff --git a/test/OpenTelemetry.Tests/OpenTelemetry.Tests.csproj b/test/OpenTelemetry.Tests/OpenTelemetry.Tests.csproj index b61e43c0417..1934c7df9dc 100644 --- a/test/OpenTelemetry.Tests/OpenTelemetry.Tests.csproj +++ b/test/OpenTelemetry.Tests/OpenTelemetry.Tests.csproj @@ -1,7 +1,7 @@  Unit test project for OpenTelemetry - netcoreapp3.1;net5.0 + netcoreapp3.1;net5.0;net6.0 $(TargetFrameworks);net461 $(NoWarn),CS0618 diff --git a/test/OpenTelemetry.Tests/Resources/ResourceTest.cs b/test/OpenTelemetry.Tests/Resources/ResourceTest.cs index cc5bc3bc237..98b4851006f 100644 --- a/test/OpenTelemetry.Tests/Resources/ResourceTest.cs +++ b/test/OpenTelemetry.Tests/Resources/ResourceTest.cs @@ -518,9 +518,7 @@ private static void ValidateAttributes(IEnumerable> var endInd = endIndex == 0 ? keyValuePairs.Length - 1 : endIndex; for (var i = startIndex; i <= endInd; ++i) { - Assert.Contains( - new KeyValuePair( - $"{KeyName}{i}", $"{ValueName}{i}"), keyValuePairs); + Assert.Contains(new KeyValuePair($"{KeyName}{i}", $"{ValueName}{i}"), keyValuePairs); } } diff --git a/test/OpenTelemetry.Tests/Shared/EventSourceTestHelper.cs b/test/OpenTelemetry.Tests/Shared/EventSourceTestHelper.cs index 0fb75f879e3..bb5dfdb8feb 100644 --- a/test/OpenTelemetry.Tests/Shared/EventSourceTestHelper.cs +++ b/test/OpenTelemetry.Tests/Shared/EventSourceTestHelper.cs @@ -37,8 +37,7 @@ private static void VerifyMethodImplementation(EventSource eventSource, MethodIn { using (var listener = new TestEventListener()) { - const long AllKeywords = -1; - listener.EnableEvents(eventSource, EventLevel.Verbose, (EventKeywords)AllKeywords); + listener.EnableEvents(eventSource, EventLevel.Verbose, EventKeywords.All); try { object[] eventArguments = GenerateEventArguments(eventMethod); diff --git a/test/OpenTelemetry.Tests/Shared/TestHttpServer.cs b/test/OpenTelemetry.Tests/Shared/TestHttpServer.cs index 1f56b2d76af..921502f6f97 100644 --- a/test/OpenTelemetry.Tests/Shared/TestHttpServer.cs +++ b/test/OpenTelemetry.Tests/Shared/TestHttpServer.cs @@ -100,7 +100,8 @@ public void Dispose() { try { - this.listener?.Stop(); + this.listener.Close(); + this.httpListenerTask?.Wait(); } catch (ObjectDisposedException) { diff --git a/src/OpenTelemetry/Metrics/DataPoint/IExemplar.cs b/test/OpenTelemetry.Tests/Shared/Utils.cs similarity index 61% rename from src/OpenTelemetry/Metrics/DataPoint/IExemplar.cs rename to test/OpenTelemetry.Tests/Shared/Utils.cs index 96aa7eb45ac..43a6c1ee85c 100644 --- a/src/OpenTelemetry/Metrics/DataPoint/IExemplar.cs +++ b/test/OpenTelemetry.Tests/Shared/Utils.cs @@ -1,4 +1,4 @@ -// +// // Copyright The OpenTelemetry Authors // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,20 +14,18 @@ // limitations under the License. // -using System; -using System.Collections.Generic; using System.Diagnostics; +using System.Runtime.CompilerServices; -namespace OpenTelemetry.Metrics +namespace OpenTelemetry.Tests { - public interface IExemplar : IDataValue + internal class Utils { - DateTimeOffset Timestamp { get; } - - KeyValuePair[] FilteredTags { get; } - - ActivityTraceId TraceId { get; } - - ActivitySpanId SpanId { get; } + [MethodImpl(MethodImplOptions.NoInlining)] + public static string GetCurrentMethodName() + { + var method = new StackFrame(1).GetMethod(); + return $"{method.DeclaringType.FullName}.{method.Name}"; + } } } diff --git a/test/OpenTelemetry.Tests/Trace/ActivityExtensionsTest.cs b/test/OpenTelemetry.Tests/Trace/ActivityExtensionsTest.cs index a0590ebccd6..b67dc80333e 100644 --- a/test/OpenTelemetry.Tests/Trace/ActivityExtensionsTest.cs +++ b/test/OpenTelemetry.Tests/Trace/ActivityExtensionsTest.cs @@ -30,7 +30,7 @@ public class ActivityExtensionsTest [Fact] public void SetStatus() { - using var openTelemetrySdk = Sdk.CreateTracerProviderBuilder() + using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource(ActivitySourceName) .Build(); @@ -45,7 +45,7 @@ public void SetStatus() [Fact] public void SetStatusWithDescription() { - using var openTelemetrySdk = Sdk.CreateTracerProviderBuilder() + using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource(ActivitySourceName) .Build(); @@ -62,7 +62,7 @@ public void SetStatusWithDescription() [Fact] public void SetStatusWithDescriptionTwice() { - using var openTelemetrySdk = Sdk.CreateTracerProviderBuilder() + using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource(ActivitySourceName) .Build(); @@ -80,7 +80,7 @@ public void SetStatusWithDescriptionTwice() [Fact] public void SetStatusWithIgnoredDescription() { - using var openTelemetrySdk = Sdk.CreateTracerProviderBuilder() + using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource(ActivitySourceName) .Build(); @@ -97,7 +97,7 @@ public void SetStatusWithIgnoredDescription() [Fact] public void SetCancelledStatus() { - using var openTelemetrySdk = Sdk.CreateTracerProviderBuilder() + using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource(ActivitySourceName) .Build(); @@ -112,7 +112,7 @@ public void SetCancelledStatus() [Fact] public void GetStatusWithNoStatusInActivity() { - using var openTelemetrySdk = Sdk.CreateTracerProviderBuilder() + using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource(ActivitySourceName) .Build(); @@ -126,7 +126,7 @@ public void GetStatusWithNoStatusInActivity() [Fact] public void LastSetStatusWins() { - using var openTelemetrySdk = Sdk.CreateTracerProviderBuilder() + using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource(ActivitySourceName) .Build(); @@ -260,7 +260,7 @@ public void EnumerateLinksEmpty() [Fact] public void EnumerateLinksNonempty() { - using var openTelemetrySdk = Sdk.CreateTracerProviderBuilder() + using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource(ActivitySourceName) .Build(); diff --git a/test/OpenTelemetry.Tests/Trace/BatchExportActivityProcessorOptionsTest.cs b/test/OpenTelemetry.Tests/Trace/BatchExportActivityProcessorOptionsTest.cs new file mode 100644 index 00000000000..dc834b51d2d --- /dev/null +++ b/test/OpenTelemetry.Tests/Trace/BatchExportActivityProcessorOptionsTest.cs @@ -0,0 +1,99 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using Xunit; + +namespace OpenTelemetry.Trace.Tests +{ + public class BatchExportActivityProcessorOptionsTest : IDisposable + { + public BatchExportActivityProcessorOptionsTest() + { + this.ClearEnvVars(); + } + + public void Dispose() + { + this.ClearEnvVars(); + } + + [Fact] + public void BatchExportProcessorOptions_Defaults() + { + var options = new BatchExportActivityProcessorOptions(); + + Assert.Equal(30000, options.ExporterTimeoutMilliseconds); + Assert.Equal(512, options.MaxExportBatchSize); + Assert.Equal(2048, options.MaxQueueSize); + Assert.Equal(5000, options.ScheduledDelayMilliseconds); + } + + [Fact] + public void BatchExportProcessorOptions_EnvironmentVariableOverride() + { + Environment.SetEnvironmentVariable(BatchExportActivityProcessorOptions.ExporterTimeoutEnvVarKey, "1"); + Environment.SetEnvironmentVariable(BatchExportActivityProcessorOptions.MaxExportBatchSizeEnvVarKey, "2"); + Environment.SetEnvironmentVariable(BatchExportActivityProcessorOptions.MaxQueueSizeEnvVarKey, "3"); + Environment.SetEnvironmentVariable(BatchExportActivityProcessorOptions.ScheduledDelayEnvVarKey, "4"); + + var options = new BatchExportActivityProcessorOptions(); + + Assert.Equal(1, options.ExporterTimeoutMilliseconds); + Assert.Equal(2, options.MaxExportBatchSize); + Assert.Equal(3, options.MaxQueueSize); + Assert.Equal(4, options.ScheduledDelayMilliseconds); + } + + [Fact] + public void BatchExportProcessorOptions_InvalidPortEnvironmentVariableOverride() + { + Environment.SetEnvironmentVariable(BatchExportActivityProcessorOptions.ExporterTimeoutEnvVarKey, "invalid"); + + Assert.Throws(() => new BatchExportActivityProcessorOptions()); + } + + [Fact] + public void BatchExportProcessorOptions_SetterOverridesEnvironmentVariable() + { + Environment.SetEnvironmentVariable(BatchExportActivityProcessorOptions.ExporterTimeoutEnvVarKey, "123"); + + var options = new BatchExportActivityProcessorOptions + { + ExporterTimeoutMilliseconds = 89000, + }; + + Assert.Equal(89000, options.ExporterTimeoutMilliseconds); + } + + [Fact] + public void BatchExportProcessorOptions_EnvironmentVariableNames() + { + Assert.Equal("OTEL_BSP_EXPORT_TIMEOUT", BatchExportActivityProcessorOptions.ExporterTimeoutEnvVarKey); + Assert.Equal("OTEL_BSP_MAX_EXPORT_BATCH_SIZE", BatchExportActivityProcessorOptions.MaxExportBatchSizeEnvVarKey); + Assert.Equal("OTEL_BSP_MAX_QUEUE_SIZE", BatchExportActivityProcessorOptions.MaxQueueSizeEnvVarKey); + Assert.Equal("OTEL_BSP_SCHEDULE_DELAY", BatchExportActivityProcessorOptions.ScheduledDelayEnvVarKey); + } + + private void ClearEnvVars() + { + Environment.SetEnvironmentVariable(BatchExportActivityProcessorOptions.ExporterTimeoutEnvVarKey, null); + Environment.SetEnvironmentVariable(BatchExportActivityProcessorOptions.MaxExportBatchSizeEnvVarKey, null); + Environment.SetEnvironmentVariable(BatchExportActivityProcessorOptions.MaxQueueSizeEnvVarKey, null); + Environment.SetEnvironmentVariable(BatchExportActivityProcessorOptions.ScheduledDelayEnvVarKey, null); + } + } +} diff --git a/test/OpenTelemetry.Tests/Trace/BatchTest.cs b/test/OpenTelemetry.Tests/Trace/BatchTest.cs index 010077a7424..77bfc35b918 100644 --- a/test/OpenTelemetry.Tests/Trace/BatchTest.cs +++ b/test/OpenTelemetry.Tests/Trace/BatchTest.cs @@ -25,8 +25,9 @@ public class BatchTest [Fact] public void CheckConstructorExceptions() { - Assert.Throws(() => new Batch(null)); - Assert.Throws(() => new Batch(null, 1)); + Assert.Throws(() => new Batch((string[])null, 0)); + Assert.Throws(() => new Batch(Array.Empty(), -1)); + Assert.Throws(() => new Batch(Array.Empty(), 1)); } [Fact] @@ -131,6 +132,34 @@ public void CheckEnumeratorResetException() Assert.Throws(() => enumerator.Reset()); } + [Fact] + public void DrainIntoNewBatchTest() + { + var circularBuffer = new CircularBuffer(100); + circularBuffer.Add("a"); + circularBuffer.Add("b"); + + Batch batch = new Batch(circularBuffer, 10); + + Assert.Equal(2, batch.Count); + + string[] storage = new string[10]; + int selectedItemCount = 0; + foreach (string item in batch) + { + if (item == "b") + { + storage[selectedItemCount++] = item; + } + } + + batch = new Batch(storage, selectedItemCount); + + Assert.Equal(1, batch.Count); + + this.ValidateEnumerator(batch.GetEnumerator(), "b"); + } + private void ValidateEnumerator(Batch.Enumerator enumerator, string expected) { if (enumerator.Current != null) diff --git a/test/OpenTelemetry.Tests/Trace/CompositeActivityProcessorTests.cs b/test/OpenTelemetry.Tests/Trace/CompositeActivityProcessorTests.cs index 9446a1258a5..9d99bc1f681 100644 --- a/test/OpenTelemetry.Tests/Trace/CompositeActivityProcessorTests.cs +++ b/test/OpenTelemetry.Tests/Trace/CompositeActivityProcessorTests.cs @@ -101,16 +101,8 @@ public void CompositeActivityProcessor_ForceFlush(int timeout) { processor.ForceFlush(timeout); - if (timeout != 0) - { - Assert.True(p1.ForceFlushCalled); - Assert.True(p2.ForceFlushCalled); - } - else - { - Assert.False(p1.ForceFlushCalled); - Assert.False(p2.ForceFlushCalled); - } + Assert.True(p1.ForceFlushCalled); + Assert.True(p2.ForceFlushCalled); } } } diff --git a/test/OpenTelemetry.Tests/Trace/ExportProcessorTest.cs b/test/OpenTelemetry.Tests/Trace/ExportProcessorTest.cs index 6dd537b0be5..879e377edce 100644 --- a/test/OpenTelemetry.Tests/Trace/ExportProcessorTest.cs +++ b/test/OpenTelemetry.Tests/Trace/ExportProcessorTest.cs @@ -14,7 +14,9 @@ // limitations under the License. // +using System.Collections.Generic; using System.Diagnostics; +using OpenTelemetry.Exporter; using OpenTelemetry.Tests; using Xunit; @@ -28,9 +30,10 @@ public class ExportProcessorTest public void ExportProcessorIgnoresActivityWhenDropped() { var sampler = new AlwaysOffSampler(); - var processor = new TestActivityExportProcessor(new TestExporter(_ => { })); + var exportedItems = new List(); + var processor = new TestActivityExportProcessor(new InMemoryExporter(exportedItems)); using var activitySource = new ActivitySource(ActivitySourceName); - using var sdk = Sdk.CreateTracerProviderBuilder() + using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource(ActivitySourceName) .SetSampler(sampler) .AddProcessor(processor) @@ -49,9 +52,10 @@ public void ExportProcessorIgnoresActivityWhenDropped() public void ExportProcessorIgnoresActivityMarkedAsRecordOnly() { var sampler = new RecordOnlySampler(); - var processor = new TestActivityExportProcessor(new TestExporter(_ => { })); + var exportedItems = new List(); + var processor = new TestActivityExportProcessor(new InMemoryExporter(exportedItems)); using var activitySource = new ActivitySource(ActivitySourceName); - using var sdk = Sdk.CreateTracerProviderBuilder() + using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource(ActivitySourceName) .SetSampler(sampler) .AddProcessor(processor) @@ -70,9 +74,10 @@ public void ExportProcessorIgnoresActivityMarkedAsRecordOnly() public void ExportProcessorExportsActivityMarkedAsRecordAndSample() { var sampler = new AlwaysOnSampler(); - var processor = new TestActivityExportProcessor(new TestExporter(_ => { })); + var exportedItems = new List(); + var processor = new TestActivityExportProcessor(new InMemoryExporter(exportedItems)); using var activitySource = new ActivitySource(ActivitySourceName); - using var sdk = Sdk.CreateTracerProviderBuilder() + using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource(ActivitySourceName) .SetSampler(sampler) .AddProcessor(processor) diff --git a/test/OpenTelemetry.Tests/Trace/TelemetrySpanTest.cs b/test/OpenTelemetry.Tests/Trace/TelemetrySpanTest.cs index 70b5f4a35a8..edaab9bf75c 100644 --- a/test/OpenTelemetry.Tests/Trace/TelemetrySpanTest.cs +++ b/test/OpenTelemetry.Tests/Trace/TelemetrySpanTest.cs @@ -67,5 +67,21 @@ public void CheckRecordExceptionEmpty() telemetrySpan.RecordException(null); Assert.Empty(activity.Events); } + + [Fact] + public void ParentIds() + { + using var parentActivity = new Activity("parentOperation"); + parentActivity.Start(); // can't generate the Id until the operation is started + using var parentSpan = new TelemetrySpan(parentActivity); + + // ParentId should be unset + Assert.Equal(default, parentSpan.ParentSpanId); + + using var childActivity = new Activity("childOperation").SetParentId(parentActivity.Id); + using var childSpan = new TelemetrySpan(childActivity); + + Assert.Equal(parentSpan.Context.SpanId, childSpan.ParentSpanId); + } } } diff --git a/test/OpenTelemetry.Tests/Trace/TracerProviderSdkTest.cs b/test/OpenTelemetry.Tests/Trace/TracerProviderSdkTest.cs index 8d6c0069047..2fcf829cd34 100644 --- a/test/OpenTelemetry.Tests/Trace/TracerProviderSdkTest.cs +++ b/test/OpenTelemetry.Tests/Trace/TracerProviderSdkTest.cs @@ -34,12 +34,12 @@ public TracerProviderSdkTest() Activity.DefaultIdFormat = ActivityIdFormat.W3C; } - [Fact(Skip = "Get around GitHub failure")] + [Fact] public void TracerProviderSdkInvokesSamplingWithCorrectParameters() { var testSampler = new TestSampler(); using var activitySource = new ActivitySource(ActivitySourceName); - using var sdk = Sdk.CreateTracerProviderBuilder() + using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource(ActivitySourceName) .SetSampler(testSampler) .Build(); @@ -101,10 +101,15 @@ public void TracerProviderSdkInvokesSamplingWithCorrectParameters() using (var fromInvalidW3CIdParent = activitySource.StartActivity("customContext", ActivityKind.Client, "InvalidW3CIdParent")) { - // OpenTelemetry ActivityContext does not support - // non W3C Ids. Starting activity with non W3C Ids - // will result in no activity being created. - Assert.Null(fromInvalidW3CIdParent); + // Verify that StartActivity returns an instance of Activity. + Assert.NotNull(fromInvalidW3CIdParent); + + // Verify that the TestSampler was invoked and received the correct params. + Assert.Equal(fromInvalidW3CIdParent.TraceId, testSampler.LatestSamplingParameters.TraceId); + + // OpenTelemetry ActivityContext does not support non W3C Ids. + Assert.Null(fromInvalidW3CIdParent.ParentId); + Assert.Equal(default(ActivitySpanId), fromInvalidW3CIdParent.ParentSpanId); } } @@ -123,7 +128,7 @@ public void TracerProviderSdkSamplerAttributesAreAppliedToActivity(SamplingDecis }; using var activitySource = new ActivitySource(ActivitySourceName); - using var sdk = Sdk.CreateTracerProviderBuilder() + using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource(ActivitySourceName) .SetSampler(testSampler) .Build(); @@ -144,7 +149,7 @@ public void TracerSdkSetsActivitySamplingResultBasedOnSamplingDecision() { var testSampler = new TestSampler(); using var activitySource = new ActivitySource(ActivitySourceName); - using var sdk = Sdk.CreateTracerProviderBuilder() + using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource(ActivitySourceName) .SetSampler(testSampler) .Build(); @@ -204,7 +209,7 @@ public void TracerSdkSetsActivitySamplingResultToNoneWhenSuppressInstrumentation var testSampler = new TestSampler(); using var activitySource = new ActivitySource(ActivitySourceName); - using var sdk = Sdk.CreateTracerProviderBuilder() + using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource(ActivitySourceName) .SetSampler(testSampler) .Build(); @@ -648,6 +653,11 @@ public void SdkSamplesLegacyActivityWithAlwaysOnSampler() Assert.True(activity.IsAllDataRequested); Assert.True(activity.ActivityTraceFlags.HasFlag(ActivityTraceFlags.Recorded)); + // Validating ActivityTraceFlags is not enough as it does not get reflected on + // Id, If the Id is accessed before the sampler runs. + // https://github.com/open-telemetry/opentelemetry-dotnet/issues/2700 + Assert.EndsWith("-01", activity.Id); + activity.Stop(); } @@ -666,6 +676,11 @@ public void SdkSamplesLegacyActivityWithAlwaysOffSampler() Assert.False(activity.IsAllDataRequested); Assert.False(activity.ActivityTraceFlags.HasFlag(ActivityTraceFlags.Recorded)); + // Validating ActivityTraceFlags is not enough as it does not get reflected on + // Id, If the Id is accessed before the sampler runs. + // https://github.com/open-telemetry/opentelemetry-dotnet/issues/2700 + Assert.EndsWith("-00", activity.Id); + activity.Stop(); } @@ -689,6 +704,11 @@ public void SdkSamplesLegacyActivityWithCustomSampler(SamplingDecision samplingD Assert.Equal(isAllDataRequested, activity.IsAllDataRequested); Assert.Equal(hasRecordedFlag, activity.ActivityTraceFlags.HasFlag(ActivityTraceFlags.Recorded)); + // Validating ActivityTraceFlags is not enough as it does not get reflected on + // Id, If the Id is accessed before the sampler runs. + // https://github.com/open-telemetry/opentelemetry-dotnet/issues/2700 + Assert.EndsWith(hasRecordedFlag ? "-01" : "-00", activity.Id); + activity.Stop(); } @@ -762,6 +782,11 @@ public void SdkSamplesLegacyActivityWithRemoteParentWithCustomSampler(SamplingDe activity.Start(); Assert.Equal(expectedIsAllDataRequested, activity.IsAllDataRequested); Assert.Equal(hasRecordedFlag, activity.ActivityTraceFlags.HasFlag(ActivityTraceFlags.Recorded)); + + // Validating ActivityTraceFlags is not enough as it does not get reflected on + // Id, If the Id is accessed before the sampler runs. + // https://github.com/open-telemetry/opentelemetry-dotnet/issues/2700 + Assert.EndsWith(hasRecordedFlag ? "-01" : "-00", activity.Id); activity.Stop(); } @@ -792,6 +817,11 @@ public void SdkSamplesLegacyActivityWithRemoteParentWithAlwaysOnSampler(Activity activity.Start(); Assert.True(activity.IsAllDataRequested); Assert.True(activity.ActivityTraceFlags.HasFlag(ActivityTraceFlags.Recorded)); + + // Validating ActivityTraceFlags is not enough as it does not get reflected on + // Id, If the Id is accessed before the sampler runs. + // https://github.com/open-telemetry/opentelemetry-dotnet/issues/2700 + Assert.EndsWith("-01", activity.Id); activity.Stop(); } @@ -822,6 +852,11 @@ public void SdkSamplesLegacyActivityWithRemoteParentWithAlwaysOffSampler(Activit activity.Start(); Assert.False(activity.IsAllDataRequested); Assert.False(activity.ActivityTraceFlags.HasFlag(ActivityTraceFlags.Recorded)); + + // Validating ActivityTraceFlags is not enough as it does not get reflected on + // Id, If the Id is accessed before the sampler runs. + // https://github.com/open-telemetry/opentelemetry-dotnet/issues/2700 + Assert.EndsWith("-00", activity.Id); activity.Stop(); } @@ -949,6 +984,81 @@ public void TracerProviderSdkFlushesProcessorForcibly() Assert.True(testActivityProcessor.ForceFlushCalled); } + [Fact] + public void SdkSamplesAndProcessesLegacySourceWhenAddLegacySourceIsCalledWithWildcardValue() + { + var sampledActivities = new List(); + var sampler = new TestSampler + { + SamplingAction = + (samplingParameters) => + { + sampledActivities.Add(samplingParameters.Name); + return new SamplingResult(SamplingDecision.RecordAndSample); + }, + }; + + using TestActivityProcessor testActivityProcessor = new TestActivityProcessor(); + + var onStartProcessedActivities = new List(); + var onStopProcessedActivities = new List(); + testActivityProcessor.StartAction = + (a) => + { + Assert.Contains(a.OperationName, sampledActivities); + Assert.False(Sdk.SuppressInstrumentation); + Assert.True(a.IsAllDataRequested); // If Proccessor.OnStart is called, activity's IsAllDataRequested is set to true + onStartProcessedActivities.Add(a.OperationName); + }; + + testActivityProcessor.EndAction = + (a) => + { + Assert.False(Sdk.SuppressInstrumentation); + Assert.True(a.IsAllDataRequested); // If Processor.OnEnd is called, activity's IsAllDataRequested is set to true + onStopProcessedActivities.Add(a.OperationName); + }; + + var legacySourceNamespaces = new[] { "LegacyNamespace.*", "Namespace.*.Operation" }; + using var activitySource = new ActivitySource(ActivitySourceName); + + // AddLegacyOperationName chained to TracerProviderBuilder + using var tracerProvider = Sdk.CreateTracerProviderBuilder() + .SetSampler(sampler) + .AddProcessor(testActivityProcessor) + .AddLegacySource(legacySourceNamespaces[0]) + .AddLegacySource(legacySourceNamespaces[1]) + .AddSource(ActivitySourceName) + .Build(); + + foreach (var ns in legacySourceNamespaces) + { + var startOpName = ns.Replace("*", "Start"); + Activity startOperation = new Activity(startOpName); + startOperation.Start(); + startOperation.Stop(); + + Assert.Contains(startOpName, onStartProcessedActivities); // Processor.OnStart is called since we added a legacy OperationName + Assert.Contains(startOpName, onStopProcessedActivities); // Processor.OnEnd is called since we added a legacy OperationName + + var stopOpName = ns.Replace("*", "Stop"); + Activity stopOperation = new Activity(stopOpName); + stopOperation.Start(); + stopOperation.Stop(); + + Assert.Contains(stopOpName, onStartProcessedActivities); // Processor.OnStart is called since we added a legacy OperationName + Assert.Contains(stopOpName, onStopProcessedActivities); // Processor.OnEnd is called since we added a legacy OperationName + } + + // Ensure we can still process "normal" activities when in legacy wildcard mode. + Activity nonLegacyActivity = activitySource.StartActivity("TestActivity"); + nonLegacyActivity.Start(); + nonLegacyActivity.Stop(); + + Assert.Contains(nonLegacyActivity.OperationName, onStartProcessedActivities); // Processor.OnStart is called since we added a legacy OperationName + Assert.Contains(nonLegacyActivity.OperationName, onStopProcessedActivities); // Processor.OnEnd is called since we added a legacy OperationName + } + public void Dispose() { GC.SuppressFinalize(this); diff --git a/test/OpenTelemetry.Tests/Trace/TracerTest.cs b/test/OpenTelemetry.Tests/Trace/TracerTest.cs index ae37588ff89..5df66f9138d 100644 --- a/test/OpenTelemetry.Tests/Trace/TracerTest.cs +++ b/test/OpenTelemetry.Tests/Trace/TracerTest.cs @@ -60,7 +60,7 @@ public void TracerStartReturnsNoopSpanWhenNoSdk() [Fact] public void Tracer_StartRootSpan_BadArgs_NullSpanName() { - using var openTelemetry = Sdk.CreateTracerProviderBuilder() + using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource("tracername") .Build(); @@ -77,7 +77,7 @@ public void Tracer_StartRootSpan_BadArgs_NullSpanName() [Fact] public void Tracer_StartSpan_BadArgs_NullSpanName() { - using var openTelemetry = Sdk.CreateTracerProviderBuilder() + using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource("tracername") .Build(); @@ -94,7 +94,7 @@ public void Tracer_StartSpan_BadArgs_NullSpanName() [Fact] public void Tracer_StartActiveSpan_BadArgs_NullSpanName() { - using var openTelemetry = Sdk.CreateTracerProviderBuilder() + using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource("tracername") .Build(); @@ -111,7 +111,7 @@ public void Tracer_StartActiveSpan_BadArgs_NullSpanName() [Fact] public void Tracer_StartSpan_FromParent_BadArgs_NullSpanName() { - using var openTelemetry = Sdk.CreateTracerProviderBuilder() + using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource("tracername") .Build(); @@ -125,7 +125,7 @@ public void Tracer_StartSpan_FromParent_BadArgs_NullSpanName() [Fact] public void Tracer_StartSpan_FromParentContext_BadArgs_NullSpanName() { - using var openTelemetry = Sdk.CreateTracerProviderBuilder() + using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource("tracername") .Build(); @@ -141,7 +141,7 @@ public void Tracer_StartSpan_FromParentContext_BadArgs_NullSpanName() [Fact] public void Tracer_StartActiveSpan_FromParent_BadArgs_NullSpanName() { - using var openTelemetry = Sdk.CreateTracerProviderBuilder() + using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource("tracername") .Build(); @@ -155,7 +155,7 @@ public void Tracer_StartActiveSpan_FromParent_BadArgs_NullSpanName() [Fact] public void Tracer_StartActiveSpan_FromParentContext_BadArgs_NullSpanName() { - using var openTelemetry = Sdk.CreateTracerProviderBuilder() + using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource("tracername") .Build(); @@ -171,7 +171,7 @@ public void Tracer_StartActiveSpan_FromParentContext_BadArgs_NullSpanName() [Fact] public void Tracer_StartActiveSpan_CreatesActiveSpan() { - using var openTelemetry = Sdk.CreateTracerProviderBuilder() + using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource("tracername") .Build(); @@ -195,7 +195,7 @@ public void Tracer_StartActiveSpan_CreatesActiveSpan() [Fact] public void GetCurrentSpanBlank() { - using var openTelemetry = Sdk.CreateTracerProviderBuilder() + using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource("tracername") .Build(); Assert.False(Tracer.CurrentSpan.Context.IsValid); @@ -204,7 +204,7 @@ public void GetCurrentSpanBlank() [Fact] public void GetCurrentSpanBlankWontThrowOnEnd() { - using var openTelemetry = Sdk.CreateTracerProviderBuilder() + using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource("tracername") .Build(); var current = Tracer.CurrentSpan; @@ -214,7 +214,7 @@ public void GetCurrentSpanBlankWontThrowOnEnd() [Fact] public void GetCurrentSpan() { - using var openTelemetry = Sdk.CreateTracerProviderBuilder() + using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource("tracername") .Build(); @@ -228,7 +228,7 @@ public void GetCurrentSpan() [Fact] public void CreateSpan_Sampled() { - using var openTelemetry = Sdk.CreateTracerProviderBuilder() + using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource("tracername") .Build(); var span = this.tracer.StartSpan("foo"); @@ -238,7 +238,7 @@ public void CreateSpan_Sampled() [Fact] public void CreateSpan_NotSampled() { - using var openTelemetry = Sdk.CreateTracerProviderBuilder() + using var tracerProvider = Sdk.CreateTracerProviderBuilder() .AddSource("tracername") .SetSampler(new AlwaysOffSampler()) .Build(); diff --git a/src/OpenTelemetry/Metrics/Processors/MetricItem.cs b/test/TestApp.AspNetCore.6.0/AssemblyInfo.cs similarity index 67% rename from src/OpenTelemetry/Metrics/Processors/MetricItem.cs rename to test/TestApp.AspNetCore.6.0/AssemblyInfo.cs index 8dd9a450ada..e953a800309 100644 --- a/src/OpenTelemetry/Metrics/Processors/MetricItem.cs +++ b/test/TestApp.AspNetCore.6.0/AssemblyInfo.cs @@ -1,4 +1,4 @@ -// +// // Copyright The OpenTelemetry Authors // // Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,16 +14,11 @@ // limitations under the License. // -using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; -namespace OpenTelemetry.Metrics -{ - public class MetricItem - { - public List Metrics = new List(); - - internal MetricItem() - { - } - } -} +[assembly: SuppressMessage( + "StyleCop.CSharp.NamingRules", + "SA1300", + Justification = "Reviewed.", + Scope = "namespaceanddescendants", + Target = "TestApp.AspNetCore._6._0")] diff --git a/test/TestApp.AspNetCore.6.0/CallbackMiddleware.cs b/test/TestApp.AspNetCore.6.0/CallbackMiddleware.cs new file mode 100644 index 00000000000..af1456e4dd7 --- /dev/null +++ b/test/TestApp.AspNetCore.6.0/CallbackMiddleware.cs @@ -0,0 +1,49 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace TestApp.AspNetCore._6._0 +{ + public class CallbackMiddleware + { + private readonly CallbackMiddlewareImpl impl; + private readonly RequestDelegate next; + + public CallbackMiddleware(RequestDelegate next, CallbackMiddlewareImpl impl) + { + this.next = next; + this.impl = impl; + } + + public async Task InvokeAsync(HttpContext context) + { + if (this.impl == null || await this.impl.ProcessAsync(context)) + { + await this.next(context); + } + } + + public class CallbackMiddlewareImpl + { + public virtual async Task ProcessAsync(HttpContext context) + { + return await Task.FromResult(true); + } + } + } +} diff --git a/test/TestApp.AspNetCore.6.0/Controllers/ChildActivityController.cs b/test/TestApp.AspNetCore.6.0/Controllers/ChildActivityController.cs new file mode 100644 index 00000000000..aa5cc96c878 --- /dev/null +++ b/test/TestApp.AspNetCore.6.0/Controllers/ChildActivityController.cs @@ -0,0 +1,46 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Collections.Generic; +using System.Diagnostics; +using Microsoft.AspNetCore.Mvc; +using OpenTelemetry; + +namespace TestApp.AspNetCore._6._0.Controllers +{ + public class ChildActivityController : Controller + { + [Route("api/GetChildActivityTraceContext")] + public Dictionary GetChildActivityTraceContext() + { + var result = new Dictionary(); + var activity = new Activity("ActivityInsideHttpRequest"); + activity.Start(); + result["TraceId"] = activity.Context.TraceId.ToString(); + result["ParentSpanId"] = activity.ParentSpanId.ToString(); + result["TraceState"] = activity.Context.TraceState; + activity.Stop(); + return result; + } + + [Route("api/GetChildActivityBaggageContext")] + public IReadOnlyDictionary GetChildActivityBaggageContext() + { + var result = Baggage.Current.GetBaggage(); + return result; + } + } +} diff --git a/test/TestApp.AspNetCore.6.0/Controllers/ForwardController.cs b/test/TestApp.AspNetCore.6.0/Controllers/ForwardController.cs new file mode 100644 index 00000000000..36290e144c9 --- /dev/null +++ b/test/TestApp.AspNetCore.6.0/Controllers/ForwardController.cs @@ -0,0 +1,72 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json; + +namespace TestApp.AspNetCore._6._0.Controllers +{ + [Route("api/[controller]")] + public class ForwardController : Controller + { + private readonly HttpClient httpClient; + + public ForwardController(HttpClient httpClient) + { + this.httpClient = httpClient; + } + + // POST api/test + [HttpPost] + public async Task Post([FromBody] Data[] data) + { + var result = string.Empty; + + if (data != null) + { + foreach (var argument in data) + { + var request = new HttpRequestMessage(HttpMethod.Post, argument.Url) + { + Content = new StringContent( + JsonConvert.SerializeObject(argument.Arguments), + Encoding.UTF8, + "application/json"), + }; + await this.httpClient.SendAsync(request); + } + } + else + { + result = "done"; + } + + return result; + } + + public class Data + { + [JsonProperty("url")] + public string Url { get; set; } + + [JsonProperty("arguments")] + public Data[] Arguments { get; set; } + } + } +} diff --git a/test/TestApp.AspNetCore.6.0/Controllers/ValuesController.cs b/test/TestApp.AspNetCore.6.0/Controllers/ValuesController.cs new file mode 100644 index 00000000000..2bb407a8134 --- /dev/null +++ b/test/TestApp.AspNetCore.6.0/Controllers/ValuesController.cs @@ -0,0 +1,56 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc; + +namespace TestApp.AspNetCore._6._0.Controllers +{ + [Route("api/[controller]")] + public class ValuesController : Controller + { + // GET api/values + [HttpGet] + public IEnumerable Get() + { + return new string[] { "value1", "value2" }; + } + + // GET api/values/5 + [HttpGet("{id}")] + public string Get(int id) + { + return "value"; + } + + // POST api/values + [HttpPost] + public void Post([FromBody] string value) + { + } + + // PUT api/values/5 + [HttpPut("{id}")] + public void Put(int id, [FromBody] string value) + { + } + + // DELETE api/values/5 + [HttpDelete("{id}")] + public void Delete(int id) + { + } + } +} diff --git a/test/TestApp.AspNetCore.6.0/Program.cs b/test/TestApp.AspNetCore.6.0/Program.cs new file mode 100644 index 00000000000..73ce4974a7b --- /dev/null +++ b/test/TestApp.AspNetCore.6.0/Program.cs @@ -0,0 +1,33 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; + +namespace TestApp.AspNetCore._6._0 +{ + public class Program + { + public static void Main(string[] args) + { + CreateWebHostBuilder(args).Build().Run(); + } + + public static IWebHostBuilder CreateWebHostBuilder(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup(); + } +} diff --git a/test/TestApp.AspNetCore.6.0/Startup.cs b/test/TestApp.AspNetCore.6.0/Startup.cs new file mode 100644 index 00000000000..1da70536444 --- /dev/null +++ b/test/TestApp.AspNetCore.6.0/Startup.cs @@ -0,0 +1,63 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Net.Http; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace TestApp.AspNetCore._6._0 +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + this.Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddMvc(); + services.AddSingleton(); + services.AddSingleton( + new CallbackMiddleware.CallbackMiddlewareImpl()); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseMiddleware(); + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + } + } +} diff --git a/test/TestApp.AspNetCore.6.0/TestApp.AspNetCore.6.0.csproj b/test/TestApp.AspNetCore.6.0/TestApp.AspNetCore.6.0.csproj new file mode 100644 index 00000000000..e7095cbd18c --- /dev/null +++ b/test/TestApp.AspNetCore.6.0/TestApp.AspNetCore.6.0.csproj @@ -0,0 +1,25 @@ + + + + net6.0 + + + + + + + + + + + + + + + + + + + + + diff --git a/test/TestApp.AspNetCore.6.0/appsettings.Development.json b/test/TestApp.AspNetCore.6.0/appsettings.Development.json new file mode 100644 index 00000000000..fa8ce71a97a --- /dev/null +++ b/test/TestApp.AspNetCore.6.0/appsettings.Development.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "IncludeScopes": false, + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/test/TestApp.AspNetCore.6.0/appsettings.json b/test/TestApp.AspNetCore.6.0/appsettings.json new file mode 100644 index 00000000000..26bb0ac7ac6 --- /dev/null +++ b/test/TestApp.AspNetCore.6.0/appsettings.json @@ -0,0 +1,15 @@ +{ + "Logging": { + "IncludeScopes": false, + "Debug": { + "LogLevel": { + "Default": "Warning" + } + }, + "Console": { + "LogLevel": { + "Default": "Warning" + } + } + } +}