diff --git a/.editorconfig b/.editorconfig
index 0e96dd4..d8bb20c 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -1,197 +1,161 @@
-# Based on Roslyn's .editorconfig
-# https://github.com/dotnet/roslyn/blob/master/.editorconfig
-
-# top-most EditorConfig file
+# Remove the line below if you want to inherit .editorconfig settings from higher directories
root = true
-# Don't use tabs for indentation.
+# All files
[*]
-indent_style = space
-# (Please don't specify an indent_size here; that has too many unintended consequences.)
-
-# Code files
-[*.{cs,csx,vb,vbx}]
-indent_size = 4
-insert_final_newline = true
charset = utf-8-bom
+insert_final_newline = true
+trim_trailing_whitespace = true
-# XML project files
-[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}]
-indent_size = 4
-
-# XML config files
-[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}]
-indent_size = 2
-
-# JSON files
-[*.json]
-indent_size = 2
+# C# files
+[*.cs]
-# Powershell files
-[*.ps1]
-indent_size = 2
+#### Core EditorConfig Options ####
-# Shell script files
-[*.sh]
-end_of_line = lf
-indent_size = 2
+# Indentation and spacing
+indent_size = 4
+indent_style = space
+tab_width = 4
-# Dotnet code style settings:
-[*.{cs,vb}]
+# New line preferences
+end_of_line = crlf
+insert_final_newline = true
-# IDE0055: Fix formatting
-dotnet_diagnostic.IDE0055.severity = warning
+#### .NET Coding Conventions ####
-# Sort using and Import directives with System.* appearing first
-dotnet_sort_system_directives_first = true
+# Organize usings
dotnet_separate_import_directive_groups = false
-# Avoid "this." and "Me." if not necessary
-dotnet_style_qualification_for_field = false:silent
-dotnet_style_qualification_for_property = false:silent
-dotnet_style_qualification_for_method = false:silent
-dotnet_style_qualification_for_event = false:silent
-
-# Use language keywords instead of framework type names for type references
-dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
-dotnet_style_predefined_type_for_member_access = true:suggestion
-
-# Suggest more modern language features when available
-dotnet_style_object_initializer = true:suggestion
-dotnet_style_collection_initializer = true:suggestion
-dotnet_style_coalesce_expression = true:suggestion
-dotnet_style_null_propagation = true:suggestion
-dotnet_style_explicit_tuple_names = true:suggestion
-
-# Non-private static fields are PascalCase
-dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.severity = suggestion
-dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non_private_static_fields
-dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style
-
-dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field
-dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected
-dotnet_naming_symbols.non_private_static_fields.required_modifiers = static
-
-dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case
-
-# Non-private readonly fields are PascalCase
-dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.severity = suggestion
-dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.symbols = non_private_readonly_fields
-dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.style = non_private_static_field_style
-
-dotnet_naming_symbols.non_private_readonly_fields.applicable_kinds = field
-dotnet_naming_symbols.non_private_readonly_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected
-dotnet_naming_symbols.non_private_readonly_fields.required_modifiers = readonly
-
-dotnet_naming_style.non_private_readonly_field_style.capitalization = pascal_case
-
-# Constants are PascalCase
-dotnet_naming_rule.constants_should_be_pascal_case.severity = suggestion
-dotnet_naming_rule.constants_should_be_pascal_case.symbols = constants
-dotnet_naming_rule.constants_should_be_pascal_case.style = non_private_static_field_style
-
-dotnet_naming_symbols.constants.applicable_kinds = field, local
-dotnet_naming_symbols.constants.required_modifiers = const
-
-dotnet_naming_style.constant_style.capitalization = pascal_case
-
-# Static fields are camelCase and start with s_
-dotnet_naming_rule.static_fields_should_be_camel_case.severity = suggestion
-dotnet_naming_rule.static_fields_should_be_camel_case.symbols = static_fields
-dotnet_naming_rule.static_fields_should_be_camel_case.style = static_field_style
-
-dotnet_naming_symbols.static_fields.applicable_kinds = field
-dotnet_naming_symbols.static_fields.required_modifiers = static
-
-dotnet_naming_style.static_field_style.capitalization = camel_case
-dotnet_naming_style.static_field_style.required_prefix = s_
-
-# Instance fields are camelCase and start with _
-dotnet_naming_rule.instance_fields_should_be_camel_case.severity = suggestion
-dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields
-dotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_style
-
-dotnet_naming_symbols.instance_fields.applicable_kinds = field
-
-dotnet_naming_style.instance_field_style.capitalization = camel_case
-dotnet_naming_style.instance_field_style.required_prefix = _
-
-# Locals and parameters are camelCase
-dotnet_naming_rule.locals_should_be_camel_case.severity = suggestion
-dotnet_naming_rule.locals_should_be_camel_case.symbols = locals_and_parameters
-dotnet_naming_rule.locals_should_be_camel_case.style = camel_case_style
-
-dotnet_naming_symbols.locals_and_parameters.applicable_kinds = parameter, local
-
-dotnet_naming_style.camel_case_style.capitalization = camel_case
-
-# Local functions are PascalCase
-dotnet_naming_rule.local_functions_should_be_pascal_case.severity = suggestion
-dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = local_functions
-dotnet_naming_rule.local_functions_should_be_pascal_case.style = non_private_static_field_style
-
-dotnet_naming_symbols.local_functions.applicable_kinds = local_function
-
-dotnet_naming_style.local_function_style.capitalization = pascal_case
-
-# By default, name items with PascalCase
-dotnet_naming_rule.members_should_be_pascal_case.severity = suggestion
-dotnet_naming_rule.members_should_be_pascal_case.symbols = all_members
-dotnet_naming_rule.members_should_be_pascal_case.style = non_private_static_field_style
+dotnet_sort_system_directives_first = true
+file_header_template = unset
+
+# this. and Me. preferences
+dotnet_style_qualification_for_event = false
+dotnet_style_qualification_for_field = false
+dotnet_style_qualification_for_method = false
+dotnet_style_qualification_for_property = false
+
+# Language keywords vs BCL types preferences
+dotnet_style_predefined_type_for_locals_parameters_members = true
+dotnet_style_predefined_type_for_member_access = true
+
+# Parentheses preferences
+dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity
+dotnet_style_parentheses_in_other_binary_operators = always_for_clarity
+dotnet_style_parentheses_in_other_operators = never_if_unnecessary
+dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity
+
+# Modifier preferences
+dotnet_style_require_accessibility_modifiers = for_non_interface_members
+
+# Expression-level preferences
+dotnet_style_coalesce_expression = true
+dotnet_style_collection_initializer = true
+dotnet_style_explicit_tuple_names = true
+dotnet_style_namespace_match_folder = true
+dotnet_style_null_propagation = true
+dotnet_style_object_initializer = true
+dotnet_style_operator_placement_when_wrapping = beginning_of_line
+dotnet_style_prefer_auto_properties = true
+dotnet_style_prefer_collection_expression = when_types_loosely_match
+dotnet_style_prefer_compound_assignment = true
+dotnet_style_prefer_conditional_expression_over_assignment = true
+dotnet_style_prefer_conditional_expression_over_return = true
+dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed
+dotnet_style_prefer_inferred_anonymous_type_member_names = true
+dotnet_style_prefer_inferred_tuple_names = true
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true
+dotnet_style_prefer_simplified_boolean_expressions = true
+dotnet_style_prefer_simplified_interpolation = true
+
+# Field preferences
+dotnet_style_readonly_field = true
+
+# Parameter preferences
+dotnet_code_quality_unused_parameters = all
+
+# Suppression preferences
+dotnet_remove_unnecessary_suppression_exclusions = none
+
+# New line preferences
+dotnet_style_allow_multiple_blank_lines_experimental = true
+dotnet_style_allow_statement_immediately_after_block_experimental = true
+
+#### C# Coding Conventions ####
+
+# var preferences
+csharp_style_var_elsewhere = false:suggestion
+csharp_style_var_for_built_in_types = true:suggestion
+csharp_style_var_when_type_is_apparent = true:suggestion
-dotnet_naming_symbols.all_members.applicable_kinds = *
+# Expression-bodied members
+csharp_style_expression_bodied_accessors = true:warning
+csharp_style_expression_bodied_constructors = true:warning
+csharp_style_expression_bodied_indexers = true:warning
+csharp_style_expression_bodied_lambdas = true:warning
+csharp_style_expression_bodied_local_functions = true:warning
+csharp_style_expression_bodied_methods = true:warning
+csharp_style_expression_bodied_operators = true:warning
+csharp_style_expression_bodied_properties = true:warning
+
+# Pattern matching preferences
+csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
+csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
+csharp_style_prefer_extended_property_pattern = true:suggestion
+csharp_style_prefer_not_pattern = true:suggestion
+csharp_style_prefer_pattern_matching = true:warning
+csharp_style_prefer_switch_expression = true:suggestion
-dotnet_naming_style.pascal_case_style.capitalization = pascal_case
+# Null-checking preferences
+csharp_style_conditional_delegate_call = true:suggestion
-# error RS2008: Enable analyzer release tracking for the analyzer project containing rule '{0}'
-dotnet_diagnostic.RS2008.severity = none
+# Modifier preferences
+csharp_prefer_static_local_function = true:suggestion
+csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async
+csharp_style_prefer_readonly_struct = true:suggestion
+csharp_style_prefer_readonly_struct_member = true:suggestion
-# IDE0035: Remove unreachable code
-dotnet_diagnostic.IDE0035.severity = warning
+# Code-block preferences
+csharp_prefer_braces = false:warning
+csharp_prefer_simple_using_statement = true:suggestion
+csharp_style_namespace_declarations = file_scoped:warning
+csharp_style_prefer_method_group_conversion = true:warning
+csharp_style_prefer_primary_constructors = true:suggestion
+csharp_style_prefer_top_level_statements = true:warning
-# IDE0036: Order modifiers
-dotnet_diagnostic.IDE0036.severity = warning
+# Expression-level preferences
+csharp_prefer_simple_default_expression = true:suggestion
+csharp_style_deconstructed_variable_declaration = true:suggestion
+csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion
+csharp_style_inlined_variable_declaration = true:suggestion
+csharp_style_prefer_index_operator = true:suggestion
+csharp_style_prefer_local_over_anonymous_function = true:suggestion
+csharp_style_prefer_null_check_over_type_check = true:suggestion
+csharp_style_prefer_range_operator = true:suggestion
+csharp_style_prefer_tuple_swap = true:suggestion
+csharp_style_prefer_utf8_string_literals = true:suggestion
+csharp_style_throw_expression = true:suggestion
+csharp_style_unused_value_assignment_preference = discard_variable:suggestion
+csharp_style_unused_value_expression_statement_preference = discard_variable:suggestion
-# IDE0043: Format string contains invalid placeholder
-dotnet_diagnostic.IDE0043.severity = warning
+# 'using' directive preferences
+csharp_using_directive_placement = inside_namespace:suggestion
-# IDE0044: Make field readonly
-dotnet_diagnostic.IDE0044.severity = warning
+# New line preferences
+csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:warning
+csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:warning
+csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:warning
+csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:warning
+csharp_style_allow_embedded_statements_on_same_line_experimental = true:warning
-# RS0016: Only enable if API files are present
-dotnet_public_api_analyzer.require_api_files = true
-dotnet_style_operator_placement_when_wrapping = beginning_of_line
-tab_width = 4
-end_of_line = crlf
-dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
-dotnet_style_prefer_auto_properties = true:silent
-dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
-dotnet_style_prefer_conditional_expression_over_assignment = true:silent
-dotnet_style_prefer_conditional_expression_over_return = true:silent
-dotnet_style_prefer_inferred_tuple_names = true:suggestion
-dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
-dotnet_style_prefer_compound_assignment = true:suggestion
-dotnet_style_prefer_simplified_interpolation = true:suggestion
-dotnet_style_namespace_match_folder = true:suggestion
-dotnet_style_readonly_field = true:suggestion
-dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent
-dotnet_style_allow_multiple_blank_lines_experimental = true:silent
-dotnet_style_allow_statement_immediately_after_block_experimental = true:silent
-dotnet_code_quality_unused_parameters = all:suggestion
-dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent
-dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent
-dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent
-dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent
-dotnet_diagnostic.CA1707.severity = silent
+#### C# Formatting Rules ####
-# CSharp code style settings:
-[*.cs]
-# Newline settings
-csharp_new_line_before_open_brace = all
-csharp_new_line_before_else = true
+# New line preferences
csharp_new_line_before_catch = true
+csharp_new_line_before_else = 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_before_members_in_object_initializers = true
+csharp_new_line_before_open_brace = all
csharp_new_line_between_query_expression_clauses = true
# Indentation preferences
@@ -199,30 +163,8 @@ csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_indent_case_contents = true
csharp_indent_case_contents_when_block = true
+csharp_indent_labels = one_less_than_current
csharp_indent_switch_labels = true
-csharp_indent_labels = flush_left
-
-# Prefer "var" everywhere
-csharp_style_var_for_built_in_types = true:suggestion
-csharp_style_var_when_type_is_apparent = true:suggestion
-csharp_style_var_elsewhere = true:suggestion
-
-# Prefer method-like constructs to have a block body
-csharp_style_expression_bodied_methods = false:none
-csharp_style_expression_bodied_constructors = false:none
-csharp_style_expression_bodied_operators = false:none
-
-# Prefer property-like constructs to have an expression-body
-csharp_style_expression_bodied_properties = true:none
-csharp_style_expression_bodied_indexers = true:none
-csharp_style_expression_bodied_accessors = true:none
-
-# Suggest more modern language features when available
-csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
-csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
-csharp_style_inlined_variable_declaration = true:suggestion
-csharp_style_throw_expression = true:suggestion
-csharp_style_conditional_delegate_call = true:suggestion
# Space preferences
csharp_space_after_cast = false
@@ -248,78 +190,134 @@ csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_between_square_brackets = false
-# Blocks are allowed
-csharp_prefer_braces = true:silent
+# Wrapping preferences
csharp_preserve_single_line_blocks = true
csharp_preserve_single_line_statements = true
+#### Naming styles ####
-# 'using' directive preferences
-csharp_using_directive_placement = inside_namespace:warning
-csharp_prefer_simple_using_statement = true:suggestion
-csharp_style_namespace_declarations = file_scoped:silent
-csharp_style_expression_bodied_lambdas = true:silent
-csharp_style_expression_bodied_local_functions = false:silent
-csharp_style_prefer_null_check_over_type_check = true:suggestion
-csharp_prefer_simple_default_expression = true:suggestion
-csharp_style_prefer_local_over_anonymous_function = true:suggestion
-csharp_style_prefer_index_operator = true:suggestion
-csharp_style_prefer_range_operator = true:suggestion
-csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion
-csharp_style_prefer_tuple_swap = true:suggestion
-csharp_style_deconstructed_variable_declaration = true:suggestion
-csharp_style_unused_value_assignment_preference = discard_variable:suggestion
-csharp_style_unused_value_expression_statement_preference = discard_variable:silent
-csharp_prefer_static_local_function = true:suggestion
-csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent
-csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent
-csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent
-csharp_style_prefer_pattern_matching = true:silent
-csharp_style_prefer_switch_expression = true:suggestion
-csharp_style_prefer_not_pattern = true:suggestion
-csharp_style_prefer_extended_property_pattern = true:suggestion
-csharp_style_prefer_method_group_conversion = true:silent
-csharp_style_prefer_top_level_statements = true:silent
-csharp_style_prefer_utf8_string_literals = true:suggestion
+# Naming rules
+
+dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
+dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
+dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
-[src/CodeStyle/**.{cs,vb}]
-# warning RS0005: Do not use generic CodeAction.Create to create CodeAction
-dotnet_diagnostic.RS0005.severity = none
+dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.types_should_be_pascal_case.symbols = types
+dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
-[src/{Analyzers,CodeStyle,Features,Workspaces,EditorFeatures, VisualStudio}/**/*.{cs,vb}]
+dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
+dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
+dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
-# IDE0011: Add braces
-csharp_prefer_braces = when_multiline:warning
-# NOTE: We need the below severity entry for Add Braces due to https://github.com/dotnet/roslyn/issues/44201
-dotnet_diagnostic.IDE0011.severity = warning
+# Symbol specifications
-# IDE0040: Add accessibility modifiers
-dotnet_diagnostic.IDE0040.severity = warning
+dotnet_naming_symbols.interface.applicable_kinds = interface
+dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.interface.required_modifiers =
-# CONSIDER: Are IDE0051 and IDE0052 too noisy to be warnings for IDE editing scenarios? Should they be made build-only warnings?
-# IDE0051: Remove unused private member
-dotnet_diagnostic.IDE0051.severity = warning
+dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
+dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.types.required_modifiers =
-# IDE0052: Remove unread private member
-dotnet_diagnostic.IDE0052.severity = warning
+dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
+dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
+dotnet_naming_symbols.non_field_members.required_modifiers =
-# IDE0059: Unnecessary assignment to a value
-dotnet_diagnostic.IDE0059.severity = warning
+# Naming styles
-# IDE0060: Remove unused parameter
-dotnet_diagnostic.IDE0060.severity = warning
+dotnet_naming_style.pascal_case.required_prefix =
+dotnet_naming_style.pascal_case.required_suffix =
+dotnet_naming_style.pascal_case.word_separator =
+dotnet_naming_style.pascal_case.capitalization = pascal_case
-# CA1822: Make member static
-dotnet_diagnostic.CA1822.severity = warning
+dotnet_naming_style.begins_with_i.required_prefix = I
+dotnet_naming_style.begins_with_i.required_suffix =
+dotnet_naming_style.begins_with_i.word_separator =
+dotnet_naming_style.begins_with_i.capitalization = pascal_case
+dotnet_diagnostic.xUnit2001.severity = warning
+dotnet_diagnostic.CA1200.severity = warning
+dotnet_diagnostic.CA1309.severity = warning
+dotnet_diagnostic.CA1311.severity = warning
+dotnet_diagnostic.CA1805.severity = warning
+csharp_prefer_system_threading_lock = true:suggestion
-# Prefer "var" everywhere
-dotnet_diagnostic.IDE0007.severity = warning
-csharp_style_var_for_built_in_types = true:warning
-csharp_style_var_when_type_is_apparent = true:warning
-csharp_style_var_elsewhere = true:warning
+[*.{cs,vb}]
+dotnet_style_operator_placement_when_wrapping = beginning_of_line
+tab_width = 4
+indent_size = 4
+end_of_line = crlf
+dotnet_style_coalesce_expression = true:suggestion
+dotnet_style_null_propagation = true:suggestion
+dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
+dotnet_style_prefer_auto_properties = true:suggestion
+dotnet_style_object_initializer = true:suggestion
+dotnet_style_collection_initializer = true:suggestion
+dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
+dotnet_style_prefer_conditional_expression_over_assignment = true:warning
+dotnet_style_prefer_conditional_expression_over_return = false:warning
+dotnet_style_explicit_tuple_names = true:suggestion
+dotnet_style_prefer_inferred_tuple_names = true:suggestion
+dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
+dotnet_style_prefer_compound_assignment = true:suggestion
+dotnet_style_prefer_simplified_interpolation = true:suggestion
+dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion
+dotnet_style_namespace_match_folder = true:suggestion
+dotnet_style_readonly_field = true:suggestion
+dotnet_style_predefined_type_for_locals_parameters_members = true:warning
+dotnet_style_predefined_type_for_member_access = true:warning
+dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
+dotnet_style_allow_multiple_blank_lines_experimental = false:warning
+dotnet_style_allow_statement_immediately_after_block_experimental = false:warning
+dotnet_code_quality_unused_parameters = all:suggestion
+dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:warning
+dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:warning
+dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:warning
+dotnet_style_parentheses_in_other_operators = never_if_unnecessary:warning
+dotnet_style_qualification_for_field = false:warning
+dotnet_style_qualification_for_property = false:warning
+dotnet_style_qualification_for_method = false:warning
+dotnet_style_qualification_for_event = false:warning
+dotnet_diagnostic.CA1000.severity = warning
+dotnet_diagnostic.CA1001.severity = warning
+dotnet_diagnostic.CA1010.severity = warning
+dotnet_diagnostic.CA1036.severity = warning
+dotnet_diagnostic.CA1051.severity = warning
+dotnet_diagnostic.CA1304.severity = warning
+dotnet_diagnostic.CA1305.severity = warning
+dotnet_diagnostic.CA1310.severity = warning
+dotnet_diagnostic.CA1707.severity = warning
+dotnet_diagnostic.CA1708.severity = warning
+dotnet_diagnostic.CA1710.severity = warning
+dotnet_diagnostic.CA1711.severity = warning
+dotnet_diagnostic.CA1712.severity = warning
+dotnet_diagnostic.CA1715.severity = warning
+dotnet_diagnostic.CA1716.severity = warning
+dotnet_diagnostic.CA1720.severity = warning
+dotnet_diagnostic.CA1725.severity = warning
+dotnet_diagnostic.CA1727.severity = warning
+dotnet_diagnostic.CA1838.severity = warning
+dotnet_diagnostic.CA1848.severity = warning
+dotnet_diagnostic.CA1852.severity = warning
+dotnet_diagnostic.CA1863.severity = warning
+dotnet_diagnostic.CA3061.severity = warning
+dotnet_diagnostic.CA3075.severity = warning
+dotnet_diagnostic.CA3076.severity = warning
+dotnet_diagnostic.CA3077.severity = warning
+dotnet_diagnostic.CA3147.severity = warning
+dotnet_diagnostic.CA5350.severity = warning
+dotnet_diagnostic.CA5351.severity = warning
+dotnet_diagnostic.CA5359.severity = warning
+dotnet_diagnostic.CA5360.severity = warning
+dotnet_diagnostic.CA5363.severity = warning
+dotnet_diagnostic.CA5364.severity = warning
+
+# Markdown files
+[*.md]
+charset = utf-8-bom
+trim_trailing_whitespace = false
-[src/{VisualStudio}/**/*.{cs,vb}]
-# CA1822: Make member static
-# Not enforced as a build 'warning' for 'VisualStudio' layer due to large number of false positives from https://github.com/dotnet/roslyn-analyzers/issues/3857 and https://github.com/dotnet/roslyn-analyzers/issues/3858
-# Additionally, there is a risk of accidentally breaking an internal API that partners rely on though IVT.
-dotnet_diagnostic.CA1822.severity = suggestion
\ No newline at end of file
+# JSON, XML, YAML files
+[*.{json,xml,yml,yaml}]
+charset = utf-8-bom
+indent_size = 2
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
new file mode 100644
index 0000000..6124173
--- /dev/null
+++ b/.github/copilot-instructions.md
@@ -0,0 +1,6 @@
+# Copilot Instructions
+
+## Package Constraints
+
+- **FluentAssertions**: Do not upgrade beyond major version 7.x due to a licensing change in version 8+.
+- **Swashbuckle.AspNetCore**: Do not upgrade beyond version 6.x due to breaking changes in Microsoft.OpenApi v2.
diff --git a/.gitignore b/.gitignore
index 12976c7..8a30d25 100644
--- a/.gitignore
+++ b/.gitignore
@@ -396,4 +396,3 @@ FodyWeavers.xsd
# JetBrains Rider
*.sln.iml
-/sample/DockerOpenTelemetry/output/logs.json
diff --git a/Directory.Build.props b/Directory.Build.props
index 4ccf3cc..f5f4652 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -8,7 +8,7 @@
en-US
$(MSBuildThisFileDirectory)
$(MSBuildProjectName.EndsWith('.Tests'))
- net8.0
+ net10.0
enable
enable
Latest
diff --git a/Directory.Packages.props b/Directory.Packages.props
index ce40da9..2e8c89d 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -1,33 +1,31 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 9742cc0..63df0cc 100644
--- a/README.md
+++ b/README.md
@@ -43,7 +43,7 @@ By default, a meter named `ServiceLevelIndicator` with instrument name `operatio
"Ok" when the http response status code is in the 2xx range,
"Error" when the http response status code is in the 5xx range,
"Unset" for any other status code.
-- http.response.status_code - The http status code.
+- http.response.status.code - The http status code.
- http.request.method (Optional)- The http request method (GET, POST, etc) is added.
Difference between ServiceLevelIndicator and http.server.request.duration
@@ -304,12 +304,14 @@ An async version `EnrichAsync` is also available.
Try out the sample weather forecast Web API.
-To view the metrics locally.
+To view the metrics locally using the [.NET Aspire Dashboard](https://aspire.dev/dashboard/standalone/):
-1. Run Docker Desktop
-2. Run [sample\DockerOpenTelemetry\run.cmd](sample\DockerOpenTelemetry\run.cmd) to download and run zipkin and prometheus.
-3. Run the sample web API project and call the `GET WeatherForecast` using the Open API UI.
-4. You should see the SLI metrics in prometheus under the meter `operation_duration_milliseconds_bucket` where the `Operation = "GET WeatherForeCase"`, `http.response.status_code = 200`, `LocationId = "ms-loc://az/public/westus2"`, `activity.status_code = Ok`
+1. Start the Aspire dashboard:
+ ```
+ docker run --rm -it -d -p 18888:18888 -p 4317:18889 -e DOTNET_DASHBOARD_UNSECURED_ALLOW_ANONYMOUS=true -e DASHBOARD__OTLP__AUTHMODE=Unsecured --name aspire-dashboard mcr.microsoft.com/dotnet/aspire-dashboard:latest
+ ```
+2. Run the sample web API project and call the `GET WeatherForecast` using the Open API UI.
+3. Open `http://localhost:18888` to view the dashboard. You should see the SLI metrics under the meter `operation_duration_milliseconds_bucket` where the `Operation = "GET WeatherForecast"`, `http.response.status.code = 200`, `LocationId = "ms-loc://az/public/westus2"`, `activity.status.code = Ok`

-5. If you run the sample with API Versioning, you will see something similar to the following.
+4. If you run the sample with API Versioning, you will see something similar to the following.

\ No newline at end of file
diff --git a/ServiceLevelIndicators.Asp.ApiVersioning/src/ApiVersionEnrichment.cs b/ServiceLevelIndicators.Asp.ApiVersioning/src/ApiVersionEnrichment.cs
index 64d8a5c..df34097 100644
--- a/ServiceLevelIndicators.Asp.ApiVersioning/src/ApiVersionEnrichment.cs
+++ b/ServiceLevelIndicators.Asp.ApiVersioning/src/ApiVersionEnrichment.cs
@@ -1,4 +1,5 @@
namespace ServiceLevelIndicators;
+
using System.Threading;
using System.Threading.Tasks;
using Asp.Versioning;
@@ -15,7 +16,7 @@ public ValueTask EnrichAsync(WebEnrichmentContext context, CancellationToken can
private static string GetApiVersion(HttpContext context)
{
- var apiVersioningFeature = context.ApiVersioningFeature();
+ var apiVersioningFeature = context.ApiVersioningFeature;
var versions = apiVersioningFeature.RawRequestedApiVersions;
if (versions.Count == 1)
return apiVersioningFeature.RequestedApiVersion?.ToString() ?? string.Empty;
@@ -26,4 +27,4 @@ private static string GetApiVersion(HttpContext context)
return "Unspecified";
}
-}
+}
\ No newline at end of file
diff --git a/ServiceLevelIndicators.Asp.ApiVersioning/src/ServiceLevelIndicatorServiceCollectionExtensions.cs b/ServiceLevelIndicators.Asp.ApiVersioning/src/ServiceLevelIndicatorServiceCollectionExtensions.cs
index 3b14a3b..18f294c 100644
--- a/ServiceLevelIndicators.Asp.ApiVersioning/src/ServiceLevelIndicatorServiceCollectionExtensions.cs
+++ b/ServiceLevelIndicators.Asp.ApiVersioning/src/ServiceLevelIndicatorServiceCollectionExtensions.cs
@@ -11,4 +11,4 @@ public static IServiceLevelIndicatorBuilder AddApiVersion(this IServiceLevelIndi
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton, ApiVersionEnrichment>());
return builder;
}
-}
+}
\ No newline at end of file
diff --git a/ServiceLevelIndicators.Asp.ApiVersioning/tests/ServiceLevelIndicatorVersionedAspTests.cs b/ServiceLevelIndicators.Asp.ApiVersioning/tests/ServiceLevelIndicatorVersionedAspTests.cs
index f0ed212..4ee8641 100644
--- a/ServiceLevelIndicators.Asp.ApiVersioning/tests/ServiceLevelIndicatorVersionedAspTests.cs
+++ b/ServiceLevelIndicators.Asp.ApiVersioning/tests/ServiceLevelIndicatorVersionedAspTests.cs
@@ -1,14 +1,13 @@
-namespace ServiceLevelIndicators.Asp.ApiVersioning.Tests;
+namespace ServiceLevelIndicators.Asp.ApiVersioning.Tests;
+using System.Diagnostics.Metrics;
+using System.Net;
using global::Asp.Versioning;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
-using System.Diagnostics.Metrics;
-using System.Net;
-using Xunit.Abstractions;
public class ServiceLevelIndicatorVersionedAspTests : IDisposable
{
@@ -57,7 +56,7 @@ public async Task SLI_Metrics_is_emitted_with_API_version_as_query_parameter()
using var host = await CreateHost();
// Act
- var response = await host.GetTestClient().GetAsync("testSingle?api-version=2023-08-29");
+ var response = await host.GetTestClient().GetAsync("testSingle?api-version=2023-08-29", TestContext.Current.CancellationToken);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
@@ -82,7 +81,7 @@ public async Task SLI_Metrics_is_emitted_with_API_version_as_header()
httpClient.DefaultRequestHeaders.Add("api-version", "2023-08-29");
// Act
- var response = await httpClient.GetAsync("testSingle");
+ var response = await httpClient.GetAsync("testSingle", TestContext.Current.CancellationToken);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
@@ -105,7 +104,7 @@ public async Task SLI_Metrics_is_emitted_with_neutral_API_version()
using var host = await CreateHost();
// Act
- var response = await host.GetTestClient().GetAsync("testNeutral");
+ var response = await host.GetTestClient().GetAsync("testNeutral", TestContext.Current.CancellationToken);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
@@ -128,7 +127,7 @@ public async Task SLI_Metrics_is_emitted_with_default_API_version()
using var host = await CreateHostWithDefaultApiVersion();
// Act
- var response = await host.GetTestClient().GetAsync("testSingle");
+ var response = await host.GetTestClient().GetAsync("testSingle", TestContext.Current.CancellationToken);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
@@ -142,7 +141,7 @@ public async Task Middleware_should_not_emit_metrics_for_nonexistent_route()
using var host = await CreateHost();
// Act
- var response = await host.GetTestClient().GetAsync("does-not-exist?api-version=2023-08-29");
+ var response = await host.GetTestClient().GetAsync("does-not-exist?api-version=2023-08-29", TestContext.Current.CancellationToken);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
@@ -167,7 +166,7 @@ public async Task SLI_Metrics_is_emitted_when_api_version_is_invalid(string rout
using var host = await CreateHost();
// Act
- var response = await host.GetTestClient().GetAsync(routeWithVersion);
+ var response = await host.GetTestClient().GetAsync(routeWithVersion, TestContext.Current.CancellationToken);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
@@ -176,9 +175,7 @@ public async Task SLI_Metrics_is_emitted_when_api_version_is_invalid(string rout
private async Task CreateHost() =>
await new HostBuilder()
- .ConfigureWebHost(webBuilder =>
- {
- webBuilder
+ .ConfigureWebHost(webBuilder => webBuilder
.UseTestServer()
.ConfigureServices(services =>
{
@@ -197,25 +194,19 @@ private async Task CreateHost() =>
.AddMvc()
.AddApiVersion();
})
- .Configure(app =>
- {
- app.UseRouting()
+ .Configure(app => app.UseRouting()
.UseServiceLevelIndicator()
.Use(async (context, next) =>
{
await Task.Delay(MillisecondsDelay);
await next(context);
})
- .UseEndpoints(endpoints => endpoints.MapControllers());
- });
- })
+ .UseEndpoints(endpoints => endpoints.MapControllers())))
.StartAsync();
private async Task CreateHostWithDefaultApiVersion() =>
await new HostBuilder()
- .ConfigureWebHost(webBuilder =>
- {
- webBuilder
+ .ConfigureWebHost(webBuilder => webBuilder
.UseTestServer()
.ConfigureServices(services =>
{
@@ -236,18 +227,14 @@ private async Task CreateHostWithDefaultApiVersion() =>
.AddMvc()
.AddApiVersion();
})
- .Configure(app =>
- {
- app.UseRouting()
+ .Configure(app => app.UseRouting()
.UseServiceLevelIndicator()
.Use(async (context, next) =>
{
await Task.Delay(MillisecondsDelay);
await next(context);
})
- .UseEndpoints(endpoints => endpoints.MapControllers());
- });
- })
+ .UseEndpoints(endpoints => endpoints.MapControllers())))
.StartAsync();
private void ValidateMetrics()
@@ -286,4 +273,4 @@ public void Dispose()
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
-}
+}
\ No newline at end of file
diff --git a/ServiceLevelIndicators.Asp.ApiVersioning/tests/TestDoubleController.cs b/ServiceLevelIndicators.Asp.ApiVersioning/tests/TestDoubleController.cs
index c2bdded..34783d3 100644
--- a/ServiceLevelIndicators.Asp.ApiVersioning/tests/TestDoubleController.cs
+++ b/ServiceLevelIndicators.Asp.ApiVersioning/tests/TestDoubleController.cs
@@ -1,7 +1,7 @@
namespace ServiceLevelIndicators.Asp.ApiVersioning.Tests;
-using Microsoft.AspNetCore.Mvc;
using global::Asp.Versioning;
+using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("[controller]")]
@@ -11,4 +11,4 @@ public class TestDoubleController : ControllerBase
{
[HttpGet]
public IActionResult Get() => Ok("Hello World!");
-}
+}
\ No newline at end of file
diff --git a/ServiceLevelIndicators.Asp.ApiVersioning/tests/TestNeutralController.cs b/ServiceLevelIndicators.Asp.ApiVersioning/tests/TestNeutralController.cs
index fa7a2c7..b8b0a00 100644
--- a/ServiceLevelIndicators.Asp.ApiVersioning/tests/TestNeutralController.cs
+++ b/ServiceLevelIndicators.Asp.ApiVersioning/tests/TestNeutralController.cs
@@ -1,7 +1,7 @@
namespace ServiceLevelIndicators.Asp.ApiVersioning.Tests;
-using Microsoft.AspNetCore.Mvc;
using global::Asp.Versioning;
+using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("[controller]")]
@@ -10,4 +10,4 @@ public class TestNeutralController : ControllerBase
{
[HttpGet]
public IActionResult Get() => Ok("Hello World!");
-}
+}
\ No newline at end of file
diff --git a/ServiceLevelIndicators.Asp.ApiVersioning/tests/TestSingleController.cs b/ServiceLevelIndicators.Asp.ApiVersioning/tests/TestSingleController.cs
index 263a508..ce639e2 100644
--- a/ServiceLevelIndicators.Asp.ApiVersioning/tests/TestSingleController.cs
+++ b/ServiceLevelIndicators.Asp.ApiVersioning/tests/TestSingleController.cs
@@ -1,7 +1,7 @@
namespace ServiceLevelIndicators.Asp.ApiVersioning.Tests;
-using Microsoft.AspNetCore.Mvc;
using global::Asp.Versioning;
+using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("[controller]")]
@@ -10,4 +10,4 @@ public class TestSingleController : ControllerBase
{
[HttpGet]
public IActionResult Get() => Ok("Hello World!");
-}
+}
\ No newline at end of file
diff --git a/ServiceLevelIndicators.Asp/src/CustomerResourceIdAttribute.cs b/ServiceLevelIndicators.Asp/src/CustomerResourceIdAttribute.cs
index a191b60..e9747ae 100644
--- a/ServiceLevelIndicators.Asp/src/CustomerResourceIdAttribute.cs
+++ b/ServiceLevelIndicators.Asp/src/CustomerResourceIdAttribute.cs
@@ -1,6 +1,10 @@
namespace ServiceLevelIndicators;
+///
+/// Marks a route parameter as the customer resource identifier for SLI metrics.
+/// Only one parameter per endpoint may have this attribute.
+///
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
public sealed class CustomerResourceIdAttribute : Attribute
{
-}
+}
\ No newline at end of file
diff --git a/ServiceLevelIndicators.Asp/src/CustomerResourceIdMetadata.cs b/ServiceLevelIndicators.Asp/src/CustomerResourceIdMetadata.cs
index f23ba6c..16a2162 100644
--- a/ServiceLevelIndicators.Asp/src/CustomerResourceIdMetadata.cs
+++ b/ServiceLevelIndicators.Asp/src/CustomerResourceIdMetadata.cs
@@ -1,6 +1,12 @@
namespace ServiceLevelIndicators;
-public class CustomerResourceIdMetadata(string routeValueName)
+///
+/// Endpoint metadata indicating which route value supplies the customer resource identifier.
+///
+public sealed class CustomerResourceIdMetadata(string routeValueName)
{
+ ///
+ /// Gets the route value name mapped to the customer resource identifier.
+ ///
public string RouteValueName { get; } = routeValueName;
-}
+}
\ No newline at end of file
diff --git a/ServiceLevelIndicators.Asp/src/EndpointBuilderExtensions.cs b/ServiceLevelIndicators.Asp/src/EndpointBuilderExtensions.cs
index e60e8aa..ebf00f6 100644
--- a/ServiceLevelIndicators.Asp/src/EndpointBuilderExtensions.cs
+++ b/ServiceLevelIndicators.Asp/src/EndpointBuilderExtensions.cs
@@ -3,8 +3,19 @@
using System.Reflection;
using Microsoft.AspNetCore.Builder;
+///
+/// Extension methods for adding SLI metadata to Minimal API endpoints.
+///
public static class EndpointBuilderExtensions
{
+ ///
+ /// Marks a Minimal API endpoint for SLI metric emission and scans handler parameters
+ /// for and .
+ ///
+ /// The endpoint convention builder type.
+ /// The endpoint builder.
+ /// An optional custom operation name; if omitted, the route template is used.
+ /// The for chaining.
public static TBuilder AddServiceLevelIndicator(this TBuilder builder, string? operation = default)
where TBuilder : notnull, IEndpointConventionBuilder
{
@@ -32,6 +43,8 @@ private static void AddSliMetadata(EndpointBuilder endpoint)
switch (attributes[j])
{
case CustomerResourceIdAttribute:
+ if (endpoint.Metadata.OfType().Any())
+ throw new InvalidOperationException("Multiple " + nameof(CustomerResourceIdAttribute) + " defined on endpoint '" + endpoint.DisplayName + "'.");
endpoint.Metadata.Add(new CustomerResourceIdMetadata(parameter.Name!));
break;
case MeasureAttribute measure:
@@ -41,4 +54,4 @@ private static void AddSliMetadata(EndpointBuilder endpoint)
}
}
}
-}
+}
\ No newline at end of file
diff --git a/ServiceLevelIndicators.Asp/src/Enrich.cs b/ServiceLevelIndicators.Asp/src/Enrich.cs
index e162b58..7645e90 100644
--- a/ServiceLevelIndicators.Asp/src/Enrich.cs
+++ b/ServiceLevelIndicators.Asp/src/Enrich.cs
@@ -1,4 +1,5 @@
namespace ServiceLevelIndicators;
+
using System.Threading;
using System.Threading.Tasks;
@@ -13,4 +14,4 @@ public ValueTask EnrichAsync(WebEnrichmentContext context, CancellationToken can
_action(context);
return ValueTask.CompletedTask;
}
-}
+}
\ No newline at end of file
diff --git a/ServiceLevelIndicators.Asp/src/EnrichAsync.cs b/ServiceLevelIndicators.Asp/src/EnrichAsync.cs
index dd6abc9..7718b3c 100644
--- a/ServiceLevelIndicators.Asp/src/EnrichAsync.cs
+++ b/ServiceLevelIndicators.Asp/src/EnrichAsync.cs
@@ -11,4 +11,4 @@ internal sealed class EnrichAsync : IEnrichment
ValueTask IEnrichment.EnrichAsync(WebEnrichmentContext context, CancellationToken cancellationToken)
=> _func(context, cancellationToken);
-}
+}
\ No newline at end of file
diff --git a/ServiceLevelIndicators.Asp/src/HttpContextExtensions.cs b/ServiceLevelIndicators.Asp/src/HttpContextExtensions.cs
index 2220c44..34ef019 100644
--- a/ServiceLevelIndicators.Asp/src/HttpContextExtensions.cs
+++ b/ServiceLevelIndicators.Asp/src/HttpContextExtensions.cs
@@ -1,4 +1,5 @@
namespace ServiceLevelIndicators;
+
using System;
using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Http;
@@ -41,4 +42,4 @@ public static bool TryGetMeasuredOperation(this HttpContext context, [MaybeNullW
measuredOperation = null;
return false;
}
-}
+}
\ No newline at end of file
diff --git a/ServiceLevelIndicators.Asp/src/HttpMethodEnrichment.cs b/ServiceLevelIndicators.Asp/src/HttpMethodEnrichment.cs
index 12d3a17..4357b8c 100644
--- a/ServiceLevelIndicators.Asp/src/HttpMethodEnrichment.cs
+++ b/ServiceLevelIndicators.Asp/src/HttpMethodEnrichment.cs
@@ -1,4 +1,5 @@
namespace ServiceLevelIndicators;
+
using System.Threading.Tasks;
internal sealed class HttpMethodEnrichment
@@ -9,4 +10,4 @@ public ValueTask EnrichAsync(WebEnrichmentContext context, CancellationToken can
context.AddAttribute("http.request.method", context.HttpContext.Request.Method);
return ValueTask.CompletedTask;
}
-}
+}
\ No newline at end of file
diff --git a/ServiceLevelIndicators.Asp/src/IServiceCollectionExtensions.cs b/ServiceLevelIndicators.Asp/src/IServiceCollectionExtensions.cs
index f5e994e..9ed0615 100644
--- a/ServiceLevelIndicators.Asp/src/IServiceCollectionExtensions.cs
+++ b/ServiceLevelIndicators.Asp/src/IServiceCollectionExtensions.cs
@@ -20,4 +20,4 @@ public static IServiceLevelIndicatorBuilder AddServiceLevelIndicator(this IServi
return new ServiceLevelIndicatorBuilder(services);
}
-}
+}
\ No newline at end of file
diff --git a/ServiceLevelIndicators.Asp/src/IServiceLevelIndicatorBuilder.cs b/ServiceLevelIndicators.Asp/src/IServiceLevelIndicatorBuilder.cs
index 029b01d..c255a7a 100644
--- a/ServiceLevelIndicators.Asp/src/IServiceLevelIndicatorBuilder.cs
+++ b/ServiceLevelIndicators.Asp/src/IServiceLevelIndicatorBuilder.cs
@@ -2,7 +2,13 @@
using Microsoft.Extensions.DependencyInjection;
+///
+/// Builder interface for configuring Service Level Indicator services.
+///
public interface IServiceLevelIndicatorBuilder
{
+ ///
+ /// Gets the where SLI services are registered.
+ ///
IServiceCollection Services { get; }
-}
+}
\ No newline at end of file
diff --git a/ServiceLevelIndicators.Asp/src/IServiceLevelIndicatorFeature.cs b/ServiceLevelIndicators.Asp/src/IServiceLevelIndicatorFeature.cs
index 3151336..5f06f02 100644
--- a/ServiceLevelIndicators.Asp/src/IServiceLevelIndicatorFeature.cs
+++ b/ServiceLevelIndicators.Asp/src/IServiceLevelIndicatorFeature.cs
@@ -5,4 +5,4 @@
public interface IServiceLevelIndicatorFeature
{
MeasuredOperation MeasuredOperation { get; }
-}
+}
\ No newline at end of file
diff --git a/ServiceLevelIndicators.Asp/src/MeasureAttribute.cs b/ServiceLevelIndicators.Asp/src/MeasureAttribute.cs
index 27e7b9f..3a43bb8 100644
--- a/ServiceLevelIndicators.Asp/src/MeasureAttribute.cs
+++ b/ServiceLevelIndicators.Asp/src/MeasureAttribute.cs
@@ -1,7 +1,10 @@
namespace ServiceLevelIndicators;
+///
+/// Marks a route parameter as an additional measured attribute emitted with SLI metrics.
+///
[AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = true)]
public sealed class MeasureAttribute(string? name = default) : Attribute
{
public string? Name { get; } = name;
-}
+}
\ No newline at end of file
diff --git a/ServiceLevelIndicators.Asp/src/MeasureMetadata.cs b/ServiceLevelIndicators.Asp/src/MeasureMetadata.cs
index e8fdfa4..11d0351 100644
--- a/ServiceLevelIndicators.Asp/src/MeasureMetadata.cs
+++ b/ServiceLevelIndicators.Asp/src/MeasureMetadata.cs
@@ -5,4 +5,4 @@ public sealed class MeasureMetadata(string routeValueName, string? attributeName
public string RouteValueName { get; } = routeValueName;
public string AttributeName { get; } = attributeName ?? routeValueName;
-}
+}
\ No newline at end of file
diff --git a/ServiceLevelIndicators.Asp/src/ServiceLevelIndicatorApplicationBuilderExtensions.cs b/ServiceLevelIndicators.Asp/src/ServiceLevelIndicatorApplicationBuilderExtensions.cs
index 3b0cb01..efc0e39 100644
--- a/ServiceLevelIndicators.Asp/src/ServiceLevelIndicatorApplicationBuilderExtensions.cs
+++ b/ServiceLevelIndicators.Asp/src/ServiceLevelIndicatorApplicationBuilderExtensions.cs
@@ -1,4 +1,5 @@
namespace ServiceLevelIndicators;
+
using System;
using Microsoft.AspNetCore.Builder;
@@ -17,4 +18,4 @@ public static IApplicationBuilder UseServiceLevelIndicator(this IApplicationBuil
return app.UseMiddleware();
}
-}
+}
\ No newline at end of file
diff --git a/ServiceLevelIndicators.Asp/src/ServiceLevelIndicatorAttribute.cs b/ServiceLevelIndicators.Asp/src/ServiceLevelIndicatorAttribute.cs
index b10800d..53b5cca 100644
--- a/ServiceLevelIndicators.Asp/src/ServiceLevelIndicatorAttribute.cs
+++ b/ServiceLevelIndicators.Asp/src/ServiceLevelIndicatorAttribute.cs
@@ -1,11 +1,16 @@
namespace ServiceLevelIndicators;
+///
+/// Attribute to mark a controller or action as emitting service level indicator metrics.
+/// When is false,
+/// only actions decorated with this attribute will emit metrics.
+///
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
-public class ServiceLevelIndicatorAttribute : Attribute
+public sealed class ServiceLevelIndicatorAttribute : Attribute
{
public ServiceLevelIndicatorAttribute() { }
public ServiceLevelIndicatorAttribute(string operation) => Operation = operation;
public string? Operation { get; set; }
-}
+}
\ No newline at end of file
diff --git a/ServiceLevelIndicators.Asp/src/ServiceLevelIndicatorConvention.cs b/ServiceLevelIndicators.Asp/src/ServiceLevelIndicatorConvention.cs
index 9e92ed0..db6610e 100644
--- a/ServiceLevelIndicators.Asp/src/ServiceLevelIndicatorConvention.cs
+++ b/ServiceLevelIndicators.Asp/src/ServiceLevelIndicatorConvention.cs
@@ -7,15 +7,10 @@ internal sealed class ServiceLevelIndicatorConvention : IParameterModelConventio
public void Apply(ParameterModel parameter)
{
var selectors = parameter.Action.Selectors;
- SelectorModel selector;
if (selectors.Count == 0)
{
- selectors.Add(selector = new());
- }
- else
- {
- selector = selectors[0];
+ selectors.Add(new());
}
for (var i = 0; i < parameter.Attributes.Count; i++)
@@ -23,13 +18,22 @@ public void Apply(ParameterModel parameter)
switch (parameter.Attributes[i])
{
case CustomerResourceIdAttribute:
- // TODO: what happens if there is more than one?
- selector.EndpointMetadata.Add(new CustomerResourceIdMetadata(parameter.Name));
+ foreach (var selector in selectors)
+ {
+ if (selector.EndpointMetadata.OfType().Any())
+ throw new InvalidOperationException("Multiple " + nameof(CustomerResourceIdAttribute) + " defined on action '" + parameter.Action.DisplayName + "'.");
+ selector.EndpointMetadata.Add(new CustomerResourceIdMetadata(parameter.Name));
+ }
+
break;
case MeasureAttribute measure:
- selector.EndpointMetadata.Add(new MeasureMetadata(parameter.Name, measure.Name));
+ foreach (var selector in selectors)
+ {
+ selector.EndpointMetadata.Add(new MeasureMetadata(parameter.Name, measure.Name));
+ }
+
break;
}
}
}
-}
+}
\ No newline at end of file
diff --git a/ServiceLevelIndicators.Asp/src/ServiceLevelIndicatorFeature.cs b/ServiceLevelIndicators.Asp/src/ServiceLevelIndicatorFeature.cs
index 7c3533f..084166a 100644
--- a/ServiceLevelIndicators.Asp/src/ServiceLevelIndicatorFeature.cs
+++ b/ServiceLevelIndicators.Asp/src/ServiceLevelIndicatorFeature.cs
@@ -5,4 +5,4 @@ internal sealed class ServiceLevelIndicatorFeature : IServiceLevelIndicatorFeatu
public ServiceLevelIndicatorFeature(MeasuredOperation measureOperation) => MeasuredOperation = measureOperation;
public MeasuredOperation MeasuredOperation { get; }
-}
+}
\ No newline at end of file
diff --git a/ServiceLevelIndicators.Asp/src/ServiceLevelIndicatorMiddleware.cs b/ServiceLevelIndicators.Asp/src/ServiceLevelIndicatorMiddleware.cs
index d20ade5..9c19d8d 100644
--- a/ServiceLevelIndicators.Asp/src/ServiceLevelIndicatorMiddleware.cs
+++ b/ServiceLevelIndicators.Asp/src/ServiceLevelIndicatorMiddleware.cs
@@ -6,18 +6,21 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Controllers;
+using Microsoft.Extensions.Logging;
-internal sealed class ServiceLevelIndicatorMiddleware
+internal sealed partial class ServiceLevelIndicatorMiddleware
{
private readonly RequestDelegate _next;
private readonly ServiceLevelIndicator _serviceLevelIndicator;
private readonly IEnumerable> _enrichments;
+ private readonly ILogger _logger;
- public ServiceLevelIndicatorMiddleware(RequestDelegate next, ServiceLevelIndicator serviceLevelIndicator, IEnumerable> enrichments)
+ public ServiceLevelIndicatorMiddleware(RequestDelegate next, ServiceLevelIndicator serviceLevelIndicator, IEnumerable> enrichments, ILogger logger)
{
_next = next;
_serviceLevelIndicator = serviceLevelIndicator;
_enrichments = enrichments;
+ _logger = logger;
}
public async Task InvokeAsync(HttpContext context)
@@ -41,11 +44,22 @@ public async Task InvokeAsync(HttpContext context)
foreach (var enrichment in _enrichments)
{
if (context.RequestAborted.IsCancellationRequested) break;
- await enrichment.EnrichAsync(webmeasurementContext, context.RequestAborted);
+ try
+ {
+ await enrichment.EnrichAsync(webmeasurementContext, context.RequestAborted);
+ }
+ catch (Exception ex)
+ {
+ LogEnrichmentFailed(ex, enrichment.GetType().Name);
+ }
}
+
RemoveSliFeatureFromHttpContext(context);
}
+ [LoggerMessage(Level = LogLevel.Warning, Message = "SLI enrichment {EnrichmentType} failed.")]
+ partial void LogEnrichmentFailed(Exception ex, string enrichmentType);
+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void SetCustomerResourceIdFromAttribute(HttpContext context, EndpointMetadataCollection metadata, MeasuredOperation measuredOperation)
{
@@ -67,7 +81,6 @@ private static void UpdateOperationWithResponseStatus(HttpContext context, Measu
measuredOperation.SetActivityStatusCode(activityCode);
}
-
private bool ShouldEmitMetrics(EndpointMetadataCollection metadata) =>
_serviceLevelIndicator.ServiceLevelIndicatorOptions.AutomaticallyEmitted || GetSliAttribute(metadata) is not null;
@@ -93,25 +106,19 @@ private static string GetOperation(HttpContext context, EndpointMetadataCollecti
private static string? GetCustomerResourceIdAttributes(HttpContext context, EndpointMetadataCollection metadata)
{
- var measures = metadata.OfType().ToArray();
- var count = measures.Length;
-
- if (count == 0)
+ var measure = metadata.GetMetadata();
+ if (measure is null)
return null;
- if (count > 1)
- throw new ArgumentException("Multiple " + nameof(CustomerResourceIdAttribute) + " defined.");
-
var values = context.Request.RouteValues;
- var measure = measures[0];
var value = values.TryGetValue(measure.RouteValueName, out var val) ? val : default;
return value?.ToString();
}
private static KeyValuePair[] GetMeasuredAttributes(HttpContext context, EndpointMetadataCollection metadata)
{
- var measures = metadata.OfType().ToArray();
- var count = measures.Length;
+ var measures = metadata.GetOrderedMetadata();
+ var count = measures.Count;
if (count == 0)
return [];
@@ -140,4 +147,4 @@ private void AddSliFeatureToHttpContext(HttpContext context, MeasuredOperation m
private static void RemoveSliFeatureFromHttpContext(HttpContext context) =>
context.Features.Set(null);
-}
+}
\ No newline at end of file
diff --git a/ServiceLevelIndicators.Asp/src/ServiceLevelIndicatorServiceCollectionExtensions.cs b/ServiceLevelIndicators.Asp/src/ServiceLevelIndicatorServiceCollectionExtensions.cs
index 1c528e5..175907c 100644
--- a/ServiceLevelIndicators.Asp/src/ServiceLevelIndicatorServiceCollectionExtensions.cs
+++ b/ServiceLevelIndicators.Asp/src/ServiceLevelIndicatorServiceCollectionExtensions.cs
@@ -42,4 +42,4 @@ public static IServiceLevelIndicatorBuilder EnrichAsync(this IServiceLevelIndica
internal sealed class ServiceLevelIndicatorBuilder(IServiceCollection services) : IServiceLevelIndicatorBuilder
{
public IServiceCollection Services => services;
-}
+}
\ No newline at end of file
diff --git a/ServiceLevelIndicators.Asp/src/WebEnrichmentContext.cs b/ServiceLevelIndicators.Asp/src/WebEnrichmentContext.cs
index 973e7e1..bac6322 100644
--- a/ServiceLevelIndicators.Asp/src/WebEnrichmentContext.cs
+++ b/ServiceLevelIndicators.Asp/src/WebEnrichmentContext.cs
@@ -1,6 +1,11 @@
namespace ServiceLevelIndicators;
+
using Microsoft.AspNetCore.Http;
+///
+/// ASP.NET Core enrichment context providing access to the
+/// and the current .
+///
public class WebEnrichmentContext : IEnrichmentContext
{
private readonly MeasuredOperation _operation;
@@ -13,8 +18,7 @@ public WebEnrichmentContext(MeasuredOperation operation, HttpContext httpContext
}
public string Operation => _operation.Operation;
-
- public void AddAttribute(string name, object value) => _operation.AddAttribute(name, value);
+ public void AddAttribute(string name, object? value) => _operation.AddAttribute(name, value);
public void SetCustomerResourceId(string id) => _operation.CustomerResourceId = id;
-}
+}
\ No newline at end of file
diff --git a/ServiceLevelIndicators.Asp/tests/IServiceCollectionExtensions.cs b/ServiceLevelIndicators.Asp/tests/IServiceCollectionExtensions.cs
index 6796ef2..8a3afa6 100644
--- a/ServiceLevelIndicators.Asp/tests/IServiceCollectionExtensions.cs
+++ b/ServiceLevelIndicators.Asp/tests/IServiceCollectionExtensions.cs
@@ -1,4 +1,5 @@
namespace ServiceLevelIndicators.Asp.Tests;
+
using System;
using Microsoft.Extensions.DependencyInjection;
@@ -10,4 +11,4 @@ public static IServiceLevelIndicatorBuilder AddTestEnrichment(this IServiceLevel
builder.Services.AddSingleton>(new TestEnrichment(key, value));
return builder;
}
-}
+}
\ No newline at end of file
diff --git a/ServiceLevelIndicators.Asp/tests/MultipleCustomerResourceIdController.cs b/ServiceLevelIndicators.Asp/tests/MultipleCustomerResourceIdController.cs
new file mode 100644
index 0000000..98bf34c
--- /dev/null
+++ b/ServiceLevelIndicators.Asp/tests/MultipleCustomerResourceIdController.cs
@@ -0,0 +1,28 @@
+namespace ServiceLevelIndicators.Asp.Tests;
+
+using System.Reflection;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.ApplicationParts;
+using Microsoft.AspNetCore.Mvc.Controllers;
+
+///
+/// Controller with two [CustomerResourceId] parameters on a single action.
+/// Marked [NonController] so it is not auto-discovered by the default test hosts.
+///
+[NonController]
+[Route("[controller]")]
+public class MultipleCustomerResourceIdController : ControllerBase
+{
+ [HttpGet("{a}/{b}")]
+ public IActionResult Get([CustomerResourceId] string a, [CustomerResourceId] string b) => Ok(a + b);
+}
+
+///
+/// A feature provider that adds a single controller type, regardless of
+/// whether it has .
+///
+internal sealed class SingleControllerFeatureProvider(Type controllerType) : IApplicationFeatureProvider
+{
+ public void PopulateFeature(IEnumerable parts, ControllerFeature feature) =>
+ feature.Controllers.Add(controllerType.GetTypeInfo());
+}
diff --git a/ServiceLevelIndicators.Asp/tests/ServiceLevelIndicatorAspTests.cs b/ServiceLevelIndicators.Asp/tests/ServiceLevelIndicatorAspTests.cs
index 4cda279..879dd97 100644
--- a/ServiceLevelIndicators.Asp/tests/ServiceLevelIndicatorAspTests.cs
+++ b/ServiceLevelIndicators.Asp/tests/ServiceLevelIndicatorAspTests.cs
@@ -1,10 +1,14 @@
-namespace ServiceLevelIndicators.Asp.Tests;
-using Microsoft.AspNetCore.TestHost;
-using Newtonsoft.Json.Linq;
+namespace ServiceLevelIndicators.Asp.Tests;
+
using System;
using System.Diagnostics.Metrics;
using System.Net;
-using Xunit.Abstractions;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Mvc.ApplicationParts;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
public class ServiceLevelIndicatorAspTests : IDisposable
{
@@ -13,6 +17,9 @@ public class ServiceLevelIndicatorAspTests : IDisposable
private readonly ITestOutputHelper _output;
private bool _callbackCalled;
private bool _disposedValue;
+ private KeyValuePair[] _actualTags = [];
+ private Instrument? _instrument;
+ private long _measurement;
public ServiceLevelIndicatorAspTests(ITestOutputHelper output)
{
@@ -27,261 +34,187 @@ public ServiceLevelIndicatorAspTests(ITestOutputHelper output)
listener.EnableMeasurementEvents(instrument);
}
};
+ _meterListener.SetMeasurementEventCallback(OnMeasurementRecorded);
+ _meterListener.Start();
}
[Fact]
public async Task SLI_Metrics_is_emitted_for_successful_API_call()
{
- _meterListener.SetMeasurementEventCallback(OnMeasurementRecorded);
- _meterListener.Start();
-
using var host = await TestHostBuilder.CreateHostWithSli(_meter);
- var response = await host.GetTestClient().GetAsync("test");
+ var response = await host.GetTestClient().GetAsync("test", TestContext.Current.CancellationToken);
response.StatusCode.Should().Be(HttpStatusCode.OK);
- void OnMeasurementRecorded(Instrument instrument, long measurement, ReadOnlySpan> tags, object? state)
+ var expectedTags = new KeyValuePair[]
{
- var expectedTags = new KeyValuePair[]
- {
- new("CustomerResourceId", "TestCustomerResourceId"),
- new("LocationId", "ms-loc://az/public/West US 3"),
- new("Operation", "GET Test"),
- new("activity.status.code", "Ok"),
- new("http.response.status.code", 200),
- };
-
- ValidateMetrics(instrument, measurement, tags, expectedTags);
- }
+ new("CustomerResourceId", "TestCustomerResourceId"),
+ new("LocationId", "ms-loc://az/public/West US 3"),
+ new("Operation", "GET Test"),
+ new("activity.status.code", "Ok"),
+ new("http.response.status.code", 200),
+ };
- _callbackCalled.Should().BeTrue();
+ ValidateMetrics(expectedTags);
}
[Fact]
public async Task SLI_Metrics_is_emitted_for_successful_POST_API_call()
{
- _meterListener.SetMeasurementEventCallback(OnMeasurementRecorded);
- _meterListener.Start();
-
using var host = await TestHostBuilder.CreateHostWithSli(_meter);
- var response = await host.GetTestClient().PostAsync("test", new StringContent("Hi"));
+ var response = await host.GetTestClient().PostAsync("test", new StringContent("Hi"), TestContext.Current.CancellationToken);
response.StatusCode.Should().Be(HttpStatusCode.OK);
- void OnMeasurementRecorded(Instrument instrument, long measurement, ReadOnlySpan> tags, object? state)
+ var expectedTags = new KeyValuePair[]
{
- var expectedTags = new KeyValuePair[]
- {
- new("CustomerResourceId", "TestCustomerResourceId"),
- new("LocationId", "ms-loc://az/public/West US 3"),
- new("Operation", "POST Test"),
- new("activity.status.code", "Ok"),
- new("http.response.status.code", 200),
- };
-
- ValidateMetrics(instrument, measurement, tags, expectedTags);
- }
+ new("CustomerResourceId", "TestCustomerResourceId"),
+ new("LocationId", "ms-loc://az/public/West US 3"),
+ new("Operation", "POST Test"),
+ new("activity.status.code", "Ok"),
+ new("http.response.status.code", 200),
+ };
- _callbackCalled.Should().BeTrue();
+ ValidateMetrics(expectedTags);
}
[Fact]
public async Task SLI_Metrics_is_emitted_for_failed_API_call()
{
- _meterListener.SetMeasurementEventCallback(OnMeasurementRecorded);
- _meterListener.Start();
-
using var host = await TestHostBuilder.CreateHostWithSli(_meter);
- var response = await host.GetTestClient().GetAsync("test/bad_request");
+ var response = await host.GetTestClient().GetAsync("test/bad_request", TestContext.Current.CancellationToken);
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
- void OnMeasurementRecorded(Instrument instrument, long measurement, ReadOnlySpan> tags, object? state)
+ var expectedTags = new KeyValuePair[]
{
- var expectedTags = new KeyValuePair[]
- {
- new("CustomerResourceId", "TestCustomerResourceId"),
- new("LocationId", "ms-loc://az/public/West US 3"),
- new("Operation", "GET Test/bad_request"),
- new("activity.status.code", "Unset"),
- new("http.response.status.code", 400),
- };
-
- ValidateMetrics(instrument, measurement, tags, expectedTags);
- }
+ new("CustomerResourceId", "TestCustomerResourceId"),
+ new("LocationId", "ms-loc://az/public/West US 3"),
+ new("Operation", "GET Test/bad_request"),
+ new("activity.status.code", "Unset"),
+ new("http.response.status.code", 400),
+ };
- _callbackCalled.Should().BeTrue();
+ ValidateMetrics(expectedTags);
}
[Fact]
public async Task SLI_Metrics_is_emitted_with_enriched_data()
{
- _meterListener.SetMeasurementEventCallback(OnMeasurementRecorded);
- _meterListener.Start();
HttpRequestMessage request = new(HttpMethod.Get, "test");
request.Headers.Add("from", "xavier@somewhere.com");
using var host = await TestHostBuilder.CreateHostWithSliEnriched(_meter);
- var response = await host.GetTestClient().SendAsync(request);
+ var response = await host.GetTestClient().SendAsync(request, TestContext.Current.CancellationToken);
response.StatusCode.Should().Be(HttpStatusCode.OK);
- void OnMeasurementRecorded(Instrument instrument, long measurement, ReadOnlySpan> tags, object? state)
+ var expectedTags = new KeyValuePair[]
{
- var expectedTags = new KeyValuePair[]
- {
- new("CustomerResourceId", "xavier@somewhere.com"),
- new("LocationId", "ms-loc://az/public/West US 3"),
- new("Operation", "GET Test"),
- new("activity.status.code", "Ok"),
- new("http.request.method", "GET"),
- new("http.response.status.code", 200),
- new("foo", "bar"),
- new("test", "again"),
- new("enrichAsync", "async"),
- };
-
- ValidateMetrics(instrument, measurement, tags, expectedTags);
- }
+ new("CustomerResourceId", "xavier@somewhere.com"),
+ new("LocationId", "ms-loc://az/public/West US 3"),
+ new("Operation", "GET Test"),
+ new("activity.status.code", "Ok"),
+ new("http.request.method", "GET"),
+ new("http.response.status.code", 200),
+ new("foo", "bar"),
+ new("test", "again"),
+ new("enrichAsync", "async"),
+ };
- _callbackCalled.Should().BeTrue();
+ ValidateMetrics(expectedTags);
}
[Fact]
public async Task Override_Operation_name()
{
- _meterListener.SetMeasurementEventCallback(OnMeasurementRecorded);
- _meterListener.Start();
-
using var host = await TestHostBuilder.CreateHostWithSli(_meter);
- var response = await host.GetTestClient().GetAsync("test/operation");
+ var response = await host.GetTestClient().GetAsync("test/operation", TestContext.Current.CancellationToken);
response.StatusCode.Should().Be(HttpStatusCode.OK);
- void OnMeasurementRecorded(Instrument instrument, long measurement, ReadOnlySpan> tags, object? state)
+ var expectedTags = new KeyValuePair[]
{
- var expectedTags = new KeyValuePair[]
- {
- new("CustomerResourceId", "TestCustomerResourceId"),
- new("LocationId", "ms-loc://az/public/West US 3"),
- new("Operation", "TestOperation"),
- new("activity.status.code", "Ok"),
- new("http.response.status.code", 200),
- };
-
- ValidateMetrics(instrument, measurement, tags, expectedTags);
- }
+ new("CustomerResourceId", "TestCustomerResourceId"),
+ new("LocationId", "ms-loc://az/public/West US 3"),
+ new("Operation", "TestOperation"),
+ new("activity.status.code", "Ok"),
+ new("http.response.status.code", 200),
+ };
- _callbackCalled.Should().BeTrue();
+ ValidateMetrics(expectedTags);
}
[Fact]
public async Task Override_CustomerResourceId()
{
- _meterListener.SetMeasurementEventCallback(OnMeasurementRecorded);
- _meterListener.Start();
-
using var host = await TestHostBuilder.CreateHostWithSli(_meter);
- var response = await host.GetTestClient().GetAsync("test/customer_resourceid/myId");
+ var response = await host.GetTestClient().GetAsync("test/customer_resourceid/myId", TestContext.Current.CancellationToken);
response.StatusCode.Should().Be(HttpStatusCode.OK);
- void OnMeasurementRecorded(Instrument instrument, long measurement, ReadOnlySpan> tags, object? state)
+ var expectedTags = new KeyValuePair[]
{
- var expectedTags = new KeyValuePair[]
- {
- new("CustomerResourceId", "myId"),
- new("LocationId", "ms-loc://az/public/West US 3"),
- new("Operation", "GET Test/customer_resourceid/{id}"),
- new("activity.status.code", "Ok"),
- new("http.response.status.code", 200),
- };
-
- ValidateMetrics(instrument, measurement, tags, expectedTags);
- }
+ new("CustomerResourceId", "myId"),
+ new("LocationId", "ms-loc://az/public/West US 3"),
+ new("Operation", "GET Test/customer_resourceid/{id}"),
+ new("activity.status.code", "Ok"),
+ new("http.response.status.code", 200),
+ };
- _callbackCalled.Should().BeTrue();
+ ValidateMetrics(expectedTags);
}
[Fact]
public async Task CustomAttribute_is_added_to_SLI_dimension()
{
- _meterListener.SetMeasurementEventCallback(OnMeasurementRecorded);
- _meterListener.Start();
-
using var host = await TestHostBuilder.CreateHostWithSli(_meter);
- var response = await host.GetTestClient().GetAsync("test/custom_attribute/Mickey");
+ var response = await host.GetTestClient().GetAsync("test/custom_attribute/Mickey", TestContext.Current.CancellationToken);
response.StatusCode.Should().Be(HttpStatusCode.OK);
- void OnMeasurementRecorded(Instrument instrument, long measurement, ReadOnlySpan> tags, object? state)
+ var expectedTags = new KeyValuePair[]
{
- var expectedTags = new KeyValuePair[]
- {
- new("CustomerResourceId", "TestCustomerResourceId"),
- new("LocationId", "ms-loc://az/public/West US 3"),
- new("Operation", "GET Test/custom_attribute/{value}"),
- new("activity.status.code", "Ok"),
- new("http.response.status.code", 200),
- new("CustomAttribute", "Mickey"),
- };
-
- ValidateMetrics(instrument, measurement, tags, expectedTags);
- }
+ new("CustomerResourceId", "TestCustomerResourceId"),
+ new("LocationId", "ms-loc://az/public/West US 3"),
+ new("Operation", "GET Test/custom_attribute/{value}"),
+ new("activity.status.code", "Ok"),
+ new("http.response.status.code", 200),
+ new("CustomAttribute", "Mickey"),
+ };
- _callbackCalled.Should().BeTrue();
+ ValidateMetrics(expectedTags);
}
[Fact]
public async Task When_automatically_emit_SLI_is_Off_do_not_send_SLI()
{
- _meterListener.SetMeasurementEventCallback(OnMeasurementRecorded);
- _meterListener.Start();
-
using var host = await TestHostBuilder.CreateHostWithoutAutomaticSli(_meter);
- var response = await host.GetTestClient().GetAsync("test");
+ var response = await host.GetTestClient().GetAsync("test", TestContext.Current.CancellationToken);
response.StatusCode.Should().Be(HttpStatusCode.OK);
- void OnMeasurementRecorded(Instrument instrument, long measurement, ReadOnlySpan> tags, object? state)
- {
- var expectedTags = new KeyValuePair[]
- {
- new KeyValuePair("CustomerResourceId", "TestCustomerResourceId"),
- };
-
- ValidateMetrics(instrument, measurement, tags, expectedTags);
- }
-
_callbackCalled.Should().BeFalse();
}
[Fact]
public async Task When_automatically_emit_SLI_is_Off_X2C_send_SLI_using_attribute()
{
- _meterListener.SetMeasurementEventCallback(OnMeasurementRecorded);
- _meterListener.Start();
-
using var host = await TestHostBuilder.CreateHostWithoutAutomaticSli(_meter);
- var response = await host.GetTestClient().GetAsync("test/send_sli");
+ var response = await host.GetTestClient().GetAsync("test/send_sli", TestContext.Current.CancellationToken);
response.StatusCode.Should().Be(HttpStatusCode.OK);
- void OnMeasurementRecorded(Instrument instrument, long measurement, ReadOnlySpan> tags, object? state)
+ var expectedTags = new KeyValuePair[]
{
- var expectedTags = new KeyValuePair[]
- {
- new("CustomerResourceId", "TestCustomerResourceId"),
- new("LocationId", "ms-loc://az/public/West US 3"),
- new("Operation", "GET Test/send_sli"),
- new("activity.status.code", "Ok"),
- new("http.response.status.code", 200),
- };
-
- ValidateMetrics(instrument, measurement, tags, expectedTags);
- }
+ new("CustomerResourceId", "TestCustomerResourceId"),
+ new("LocationId", "ms-loc://az/public/West US 3"),
+ new("Operation", "GET Test/send_sli"),
+ new("activity.status.code", "Ok"),
+ new("http.response.status.code", 200),
+ };
- _callbackCalled.Should().BeTrue();
+ ValidateMetrics(expectedTags);
}
[Fact]
@@ -289,10 +222,10 @@ public async Task GetMeasuredOperation_will_throw_if_route_does_not_emit_SLI()
{
using var host = await TestHostBuilder.CreateHostWithoutSli();
- var response = await host.GetTestClient().GetAsync("test");
+ var response = await host.GetTestClient().GetAsync("test", TestContext.Current.CancellationToken);
response.StatusCode.Should().Be(HttpStatusCode.OK);
- Func getMeasuredOperationLatency = () => host.GetTestClient().GetAsync("test/custom_attribute/Mickey");
+ Func getMeasuredOperationLatency = () => host.GetTestClient().GetAsync("test/custom_attribute/Mickey", TestContext.Current.CancellationToken);
await getMeasuredOperationLatency.Should().ThrowAsync();
}
@@ -302,9 +235,9 @@ public async Task TryGetMeasuredOperation_will_return_false_if_route_does_not_em
{
using var host = await TestHostBuilder.CreateHostWithoutSli();
- var response = await host.GetTestClient().GetAsync("test/try_get_measured_operation/Donald");
+ var response = await host.GetTestClient().GetAsync("test/try_get_measured_operation/Donald", TestContext.Current.CancellationToken);
response.StatusCode.Should().Be(HttpStatusCode.OK);
- var content = await response.Content.ReadAsStringAsync();
+ var content = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);
content.Should().Be("false");
}
@@ -312,74 +245,141 @@ public async Task TryGetMeasuredOperation_will_return_false_if_route_does_not_em
[Fact]
public async Task TryGetMeasuredOperation_will_return_true_if_route_emits_SLI()
{
- _meterListener.SetMeasurementEventCallback(OnMeasurementRecorded);
- _meterListener.Start();
-
using var host = await TestHostBuilder.CreateHostWithSli(_meter);
- var response = await host.GetTestClient().GetAsync("test/try_get_measured_operation/Goofy");
+ var response = await host.GetTestClient().GetAsync("test/try_get_measured_operation/Goofy", TestContext.Current.CancellationToken);
response.StatusCode.Should().Be(HttpStatusCode.OK);
- var content = await response.Content.ReadAsStringAsync();
+ var content = await response.Content.ReadAsStringAsync(TestContext.Current.CancellationToken);
content.Should().Be("true");
- void OnMeasurementRecorded(Instrument instrument, long measurement, ReadOnlySpan> tags, object? state)
+ var expectedTags = new KeyValuePair[]
{
- var expectedTags = new KeyValuePair[]
- {
- new("CustomerResourceId", "TestCustomerResourceId"),
- new("LocationId", "ms-loc://az/public/West US 3"),
- new("Operation", "GET Test/try_get_measured_operation/{value}"),
- new("activity.status.code", "Ok"),
- new("http.response.status.code", 200),
- new("CustomAttribute", "Goofy"),
- };
-
- ValidateMetrics(instrument, measurement, tags, expectedTags);
- }
+ new("CustomerResourceId", "TestCustomerResourceId"),
+ new("LocationId", "ms-loc://az/public/West US 3"),
+ new("Operation", "GET Test/try_get_measured_operation/{value}"),
+ new("activity.status.code", "Ok"),
+ new("http.response.status.code", 200),
+ new("CustomAttribute", "Goofy"),
+ };
- _callbackCalled.Should().BeTrue();
+ ValidateMetrics(expectedTags);
}
[Fact]
public async Task SLI_Measure_is_emitted()
{
- _meterListener.SetMeasurementEventCallback(OnMeasurementRecorded);
- _meterListener.Start();
-
using var host = await TestHostBuilder.CreateHostWithSli(_meter);
- var response = await host.GetTestClient().GetAsync("test/name/Xavier/Jon/25");
+ var response = await host.GetTestClient().GetAsync("test/name/Xavier/Jon/25", TestContext.Current.CancellationToken);
response.StatusCode.Should().Be(HttpStatusCode.OK);
- void OnMeasurementRecorded(Instrument instrument, long measurement, ReadOnlySpan> tags, object? state)
+ var expectedTags = new KeyValuePair[]
{
- var expectedTags = new KeyValuePair[]
- {
- new("CustomerResourceId", "Jon"),
- new("first", "Xavier"),
- new("age", "25"),
- new("LocationId", "ms-loc://az/public/West US 3"),
- new("Operation", "GET Test/name/{first}/{surname}/{age}"),
- new("activity.status.code", "Ok"),
- new("http.response.status.code", 200),
- };
-
- ValidateMetrics(instrument, measurement, tags, expectedTags);
- }
+ new("CustomerResourceId", "Jon"),
+ new("first", "Xavier"),
+ new("age", "25"),
+ new("LocationId", "ms-loc://az/public/West US 3"),
+ new("Operation", "GET Test/name/{first}/{surname}/{age}"),
+ new("activity.status.code", "Ok"),
+ new("http.response.status.code", 200),
+ };
- _callbackCalled.Should().BeTrue();
+ ValidateMetrics(expectedTags);
+ }
+
+ [Fact]
+ public async Task SLI_multiple_CustomerResourceId_MinimalApi_will_fail()
+ {
+ // Minimal API endpoint with multiple [CustomerResourceId] should throw when the endpoint is built
+ using var host = await new HostBuilder()
+ .ConfigureWebHost(webBuilder => webBuilder
+ .UseTestServer()
+ .ConfigureServices(services =>
+ {
+ services.AddRouting();
+ services.AddServiceLevelIndicator(options =>
+ {
+ options.Meter = _meter;
+ options.CustomerResourceId = "TestCustomerResourceId";
+ options.LocationId = ServiceLevelIndicator.CreateLocationId("public", "West US 3");
+ options.AutomaticallyEmitted = false;
+ });
+ })
+ .Configure(app =>
+ {
+ app.UseRouting();
+ app.UseServiceLevelIndicator();
+ app.UseEndpoints(endpoints =>
+ endpoints.MapGet("/bad/{a}/{b}",
+ ([CustomerResourceId] string a, [CustomerResourceId] string b) => a + b)
+ .AddServiceLevelIndicator());
+ }))
+ .StartAsync(TestContext.Current.CancellationToken);
+
+ Func act = () => host.GetTestClient().GetAsync("/bad/x/y", TestContext.Current.CancellationToken);
+ await act.Should().ThrowAsync()
+ .WithMessage("*Multiple*CustomerResourceId*");
}
[Fact]
- public async Task SLI_multiple_CustomerResourceId_will_fail()
+ public async Task SLI_multiple_CustomerResourceId_Mvc_will_fail()
+ {
+ // MVC controller action with multiple [CustomerResourceId] should throw at startup
+ Func act = () => new HostBuilder()
+ .ConfigureWebHost(webBuilder => webBuilder
+ .UseTestServer()
+ .ConfigureServices(services =>
+ {
+ var mvcBuilder = services.AddControllers();
+ mvcBuilder.PartManager.FeatureProviders.Add(
+ new SingleControllerFeatureProvider(typeof(MultipleCustomerResourceIdController)));
+ services.AddServiceLevelIndicator(options =>
+ {
+ options.Meter = _meter;
+ options.CustomerResourceId = "TestCustomerResourceId";
+ options.LocationId = ServiceLevelIndicator.CreateLocationId("public", "West US 3");
+ }).AddMvc();
+ })
+ .Configure(app => app.UseRouting()
+ .UseServiceLevelIndicator()
+ .UseEndpoints(endpoints => endpoints.MapControllers())))
+ .StartAsync(TestContext.Current.CancellationToken);
+
+ await act.Should().ThrowAsync()
+ .WithMessage("*Multiple*CustomerResourceId*");
+ }
+
+ [Fact]
+ public async Task Middleware_should_not_emit_metrics_for_nonexistent_route()
{
using var host = await TestHostBuilder.CreateHostWithSli(_meter);
- Func act = () => host.GetTestClient().GetAsync("test/multiple_customer_resource_id/Xavier/Jon");
- await act.Should().ThrowAsync();
+ var response = await host.GetTestClient().GetAsync("does-not-exist", TestContext.Current.CancellationToken);
+ response.StatusCode.Should().Be(HttpStatusCode.NotFound);
+
+ _callbackCalled.Should().BeFalse();
}
+ [Fact]
+ public async Task SLI_Metrics_is_emitted_for_server_error()
+ {
+ using var host = await TestHostBuilder.CreateHostWithSli(_meter);
+
+ var response = await host.GetTestClient().GetAsync("test/server_error", TestContext.Current.CancellationToken);
+ response.StatusCode.Should().Be(HttpStatusCode.InternalServerError);
+
+ var expectedTags = new KeyValuePair[]
+ {
+ new("CustomerResourceId", "TestCustomerResourceId"),
+ new("LocationId", "ms-loc://az/public/West US 3"),
+ new("Operation", "GET Test/server_error"),
+ new("activity.status.code", "Error"),
+ new("http.response.status.code", 500),
+ };
+
+ ValidateMetrics(expectedTags);
+ }
protected virtual void Dispose(bool disposing)
{
@@ -401,14 +401,22 @@ public void Dispose()
GC.SuppressFinalize(this);
}
- private void ValidateMetrics(Instrument instrument, long measurement, ReadOnlySpan> tags, KeyValuePair[] expectedTags)
+ private void OnMeasurementRecorded(Instrument instrument, long measurement, ReadOnlySpan> tags, object? state)
{
_callbackCalled = true;
- instrument.Name.Should().Be("operation.duration");
- instrument.Unit.Should().Be("ms");
- measurement.Should().BeInRange(TestHostBuilder.MillisecondsDelay - 10, TestHostBuilder.MillisecondsDelay + 400);
+ _instrument = instrument;
+ _measurement = measurement;
+ _actualTags = tags.ToArray();
_output.WriteLine($"Measurement {measurement}");
- tags.ToArray().Should().BeEquivalentTo(expectedTags);
}
-}
+ private void ValidateMetrics(KeyValuePair[] expectedTags)
+ {
+ _callbackCalled.Should().BeTrue();
+ _instrument!.Name.Should().Be("operation.duration");
+ _instrument.Unit.Should().Be("ms");
+ _measurement.Should().BeInRange(TestHostBuilder.MillisecondsDelay - 10, TestHostBuilder.MillisecondsDelay + 400);
+ _actualTags.Should().BeEquivalentTo(expectedTags);
+ }
+
+}
\ No newline at end of file
diff --git a/ServiceLevelIndicators.Asp/tests/ServiceLevelIndicatorMinimalApiTests.cs b/ServiceLevelIndicators.Asp/tests/ServiceLevelIndicatorMinimalApiTests.cs
new file mode 100644
index 0000000..80db3b5
--- /dev/null
+++ b/ServiceLevelIndicators.Asp/tests/ServiceLevelIndicatorMinimalApiTests.cs
@@ -0,0 +1,288 @@
+namespace ServiceLevelIndicators.Asp.Tests;
+
+using System;
+using System.Diagnostics.Metrics;
+using System.Net;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+public class ServiceLevelIndicatorMinimalApiTests : IDisposable
+{
+ private const int MillisecondsDelay = 200;
+ private readonly Meter _meter;
+ private readonly MeterListener _meterListener;
+ private readonly ITestOutputHelper _output;
+ private KeyValuePair[] _actualTags = [];
+ private Instrument? _instrument;
+ private long _measurement;
+ private bool _callbackCalled;
+ private bool _disposedValue;
+
+ public ServiceLevelIndicatorMinimalApiTests(ITestOutputHelper output)
+ {
+ _output = output;
+ const string MeterName = "SliMinApiTestMeter";
+ _meter = new(MeterName, "1.0.0");
+ _meterListener = new()
+ {
+ InstrumentPublished = (instrument, listener) =>
+ {
+ if (instrument.Meter.Name is MeterName)
+ listener.EnableMeasurementEvents(instrument);
+ }
+ };
+ _meterListener.SetMeasurementEventCallback(OnMeasurementRecorded);
+ _meterListener.Start();
+ }
+
+ [Fact]
+ public async Task SLI_Metrics_is_emitted_for_minimal_api_get()
+ {
+ // Arrange
+ using var host = await CreateMinimalApiHost();
+
+ // Act
+ var response = await host.GetTestClient().GetAsync("hello", TestContext.Current.CancellationToken);
+
+ // Assert
+ response.StatusCode.Should().Be(HttpStatusCode.OK);
+
+ var expectedTags = new KeyValuePair[]
+ {
+ new("CustomerResourceId", "TestCustomerResourceId"),
+ new("LocationId", "ms-loc://az/public/West US 3"),
+ new("Operation", "GET /hello"),
+ new("activity.status.code", "Ok"),
+ new("http.response.status.code", 200),
+ };
+
+ ValidateMetrics(expectedTags);
+ }
+
+ [Fact]
+ public async Task SLI_Metrics_is_emitted_with_custom_operation_name()
+ {
+ // Arrange
+ using var host = await CreateMinimalApiHost();
+
+ // Act
+ var response = await host.GetTestClient().GetAsync("custom-operation", TestContext.Current.CancellationToken);
+
+ // Assert
+ response.StatusCode.Should().Be(HttpStatusCode.OK);
+
+ var expectedTags = new KeyValuePair[]
+ {
+ new("CustomerResourceId", "TestCustomerResourceId"),
+ new("LocationId", "ms-loc://az/public/West US 3"),
+ new("Operation", "CustomOp"),
+ new("activity.status.code", "Ok"),
+ new("http.response.status.code", 200),
+ };
+
+ ValidateMetrics(expectedTags);
+ }
+
+ [Fact]
+ public async Task SLI_Metrics_is_emitted_with_customer_resource_id_from_route()
+ {
+ // Arrange
+ using var host = await CreateMinimalApiHost();
+
+ // Act
+ var response = await host.GetTestClient().GetAsync("resource/myResourceId", TestContext.Current.CancellationToken);
+
+ // Assert
+ response.StatusCode.Should().Be(HttpStatusCode.OK);
+
+ var expectedTags = new KeyValuePair[]
+ {
+ new("CustomerResourceId", "myResourceId"),
+ new("LocationId", "ms-loc://az/public/West US 3"),
+ new("Operation", "GET /resource/myResourceId"),
+ new("activity.status.code", "Ok"),
+ new("http.response.status.code", 200),
+ };
+
+ ValidateMetrics(expectedTags);
+ }
+
+ [Fact]
+ public async Task SLI_Metrics_is_emitted_with_measure_attribute()
+ {
+ // Arrange
+ using var host = await CreateMinimalApiHost();
+
+ // Act
+ var response = await host.GetTestClient().GetAsync("measured/items/Widget", TestContext.Current.CancellationToken);
+
+ // Assert
+ response.StatusCode.Should().Be(HttpStatusCode.OK);
+
+ var expectedTags = new KeyValuePair[]
+ {
+ new("name", "Widget"),
+ new("CustomerResourceId", "TestCustomerResourceId"),
+ new("LocationId", "ms-loc://az/public/West US 3"),
+ new("Operation", "GET /measured/items/Widget"),
+ new("activity.status.code", "Ok"),
+ new("http.response.status.code", 200),
+ };
+
+ ValidateMetrics(expectedTags);
+ }
+
+ [Fact]
+ public async Task SLI_Metrics_not_emitted_when_AddServiceLevelIndicator_not_called()
+ {
+ // Arrange
+ using var host = await CreateMinimalApiHost();
+
+ // Act
+ var response = await host.GetTestClient().GetAsync("no-sli", TestContext.Current.CancellationToken);
+
+ // Assert
+ response.StatusCode.Should().Be(HttpStatusCode.OK);
+ _callbackCalled.Should().BeFalse();
+ }
+
+ [Fact]
+ public async Task SLI_Metrics_is_emitted_with_enrichment_for_minimal_api()
+ {
+ // Arrange
+ using var host = await CreateMinimalApiHostWithEnrichment();
+
+ // Act
+ var response = await host.GetTestClient().GetAsync("hello", TestContext.Current.CancellationToken);
+
+ // Assert
+ response.StatusCode.Should().Be(HttpStatusCode.OK);
+
+ var expectedTags = new KeyValuePair[]
+ {
+ new("CustomerResourceId", "TestCustomerResourceId"),
+ new("LocationId", "ms-loc://az/public/West US 3"),
+ new("Operation", "GET /hello"),
+ new("activity.status.code", "Ok"),
+ new("http.response.status.code", 200),
+ new("http.request.method", "GET"),
+ };
+
+ ValidateMetrics(expectedTags);
+ }
+
+ private async Task CreateMinimalApiHost() =>
+ await new HostBuilder()
+ .ConfigureWebHost(webBuilder => webBuilder
+ .UseTestServer()
+ .ConfigureServices(services =>
+ {
+ services.AddRouting();
+ services.AddServiceLevelIndicator(options =>
+ {
+ options.Meter = _meter;
+ options.CustomerResourceId = "TestCustomerResourceId";
+ options.LocationId = ServiceLevelIndicator.CreateLocationId("public", "West US 3");
+ options.AutomaticallyEmitted = false;
+ });
+ })
+ .Configure(app =>
+ {
+ app.UseRouting();
+ app.UseServiceLevelIndicator();
+ app.Use(async (context, next) =>
+ {
+ await Task.Delay(MillisecondsDelay);
+ await next(context);
+ });
+ app.UseEndpoints(endpoints =>
+ {
+ endpoints.MapGet("/hello", () => "Hello World!")
+ .AddServiceLevelIndicator();
+
+ endpoints.MapGet("/custom-operation", () => "Custom!")
+ .AddServiceLevelIndicator("CustomOp");
+
+ endpoints.MapGet("/resource/{id}", ([CustomerResourceId] string id) => $"Resource {id}")
+ .AddServiceLevelIndicator();
+
+ endpoints.MapGet("/measured/items/{name}", ([Measure] string name) => $"Item {name}")
+ .AddServiceLevelIndicator();
+
+ endpoints.MapGet("/no-sli", () => "No SLI");
+ });
+ }))
+ .StartAsync();
+
+ private async Task CreateMinimalApiHostWithEnrichment() =>
+ await new HostBuilder()
+ .ConfigureWebHost(webBuilder => webBuilder
+ .UseTestServer()
+ .ConfigureServices(services =>
+ {
+ services.AddRouting();
+ services.AddServiceLevelIndicator(options =>
+ {
+ options.Meter = _meter;
+ options.CustomerResourceId = "TestCustomerResourceId";
+ options.LocationId = ServiceLevelIndicator.CreateLocationId("public", "West US 3");
+ options.AutomaticallyEmitted = false;
+ })
+ .AddHttpMethod();
+ })
+ .Configure(app =>
+ {
+ app.UseRouting();
+ app.UseServiceLevelIndicator();
+ app.Use(async (context, next) =>
+ {
+ await Task.Delay(MillisecondsDelay);
+ await next(context);
+ });
+ app.UseEndpoints(endpoints =>
+ endpoints.MapGet("/hello", () => "Hello World!")
+ .AddServiceLevelIndicator());
+ }))
+ .StartAsync();
+
+ private void OnMeasurementRecorded(Instrument instrument, long measurement, ReadOnlySpan> tags, object? state)
+ {
+ _callbackCalled = true;
+ _instrument = instrument;
+ _measurement = measurement;
+ _actualTags = tags.ToArray();
+ _output.WriteLine($"Measurement {measurement}");
+ }
+
+ private void ValidateMetrics(KeyValuePair[] expectedTags)
+ {
+ _callbackCalled.Should().BeTrue();
+ _instrument!.Name.Should().Be("operation.duration");
+ _instrument.Unit.Should().Be("ms");
+ _measurement.Should().BeInRange(MillisecondsDelay - 10, MillisecondsDelay + 400);
+ _actualTags.Should().BeEquivalentTo(expectedTags);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (!_disposedValue)
+ {
+ if (disposing)
+ {
+ _meter.Dispose();
+ _meterListener.Dispose();
+ }
+
+ _disposedValue = true;
+ }
+ }
+
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+}
diff --git a/ServiceLevelIndicators.Asp/tests/TestController.cs b/ServiceLevelIndicators.Asp/tests/TestController.cs
index 9bf9cc1..af1b2f1 100644
--- a/ServiceLevelIndicators.Asp/tests/TestController.cs
+++ b/ServiceLevelIndicators.Asp/tests/TestController.cs
@@ -1,4 +1,5 @@
namespace ServiceLevelIndicators.Asp.Tests;
+
using Microsoft.AspNetCore.Mvc;
[ApiController]
@@ -14,6 +15,9 @@ public class TestController : ControllerBase
[HttpGet("bad_request")]
public IActionResult Bad() => BadRequest("Sad World!");
+ [HttpGet("server_error")]
+ public IActionResult ServerError() => StatusCode(500, "Server Error!");
+
[HttpGet("operation")]
[ServiceLevelIndicator(Operation = "TestOperation")]
public IActionResult GetOperation() => Ok("Hello World!");
@@ -36,6 +40,7 @@ public IActionResult TryGetMeasuredOperation(string value)
measuredOperation.AddAttribute("CustomAttribute", value);
return Ok(true);
}
+
return Ok(false);
}
@@ -45,7 +50,4 @@ public IActionResult TryGetMeasuredOperation(string value)
[HttpGet("name/{first}/{surname}/{age}")]
public IActionResult GetCustomerResourceId([Measure] string first, [CustomerResourceId] string surname, [Measure] int age) => Ok(first + " " + surname + " " + age);
-
- [HttpGet("multiple_customer_resource_id/{first}/{surname}")]
- public IActionResult MultipleCustomerResourceId([CustomerResourceId] string first, [CustomerResourceId] string surname) => Ok(first + " " + surname);
-}
+}
\ No newline at end of file
diff --git a/ServiceLevelIndicators.Asp/tests/TestEnrichment.cs b/ServiceLevelIndicators.Asp/tests/TestEnrichment.cs
index a44424c..b10a4dc 100644
--- a/ServiceLevelIndicators.Asp/tests/TestEnrichment.cs
+++ b/ServiceLevelIndicators.Asp/tests/TestEnrichment.cs
@@ -13,4 +13,4 @@ public ValueTask EnrichAsync(WebEnrichmentContext context, CancellationToken can
context.AddAttribute(_key, _value);
return ValueTask.CompletedTask;
}
-}
+}
\ No newline at end of file
diff --git a/ServiceLevelIndicators.Asp/tests/TestHostBuilder.cs b/ServiceLevelIndicators.Asp/tests/TestHostBuilder.cs
index c5aab92..73f0f9c 100644
--- a/ServiceLevelIndicators.Asp/tests/TestHostBuilder.cs
+++ b/ServiceLevelIndicators.Asp/tests/TestHostBuilder.cs
@@ -1,4 +1,5 @@
namespace ServiceLevelIndicators.Asp.Tests;
+
using System.Diagnostics.Metrics;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
@@ -13,9 +14,7 @@ internal class TestHostBuilder
public static async Task CreateHostWithSli(Meter meter) =>
await new HostBuilder()
- .ConfigureWebHost(webBuilder =>
- {
- webBuilder
+ .ConfigureWebHost(webBuilder => webBuilder
.UseTestServer()
.ConfigureServices(services =>
{
@@ -27,25 +26,19 @@ public static async Task CreateHostWithSli(Meter meter) =>
options.LocationId = ServiceLevelIndicator.CreateLocationId("public", "West US 3");
}).AddMvc();
})
- .Configure(app =>
- {
- app.UseRouting()
+ .Configure(app => app.UseRouting()
.UseServiceLevelIndicator()
.Use(async (context, next) =>
{
await Task.Delay(MillisecondsDelay);
await next(context);
})
- .UseEndpoints(endpoints => endpoints.MapControllers());
- });
- })
+ .UseEndpoints(endpoints => endpoints.MapControllers())))
.StartAsync();
public static async Task CreateHostWithSliEnriched(Meter meter) =>
await new HostBuilder()
- .ConfigureWebHost(webBuilder =>
- {
- webBuilder
+ .ConfigureWebHost(webBuilder => webBuilder
.UseTestServer()
.ConfigureServices(services =>
{
@@ -71,26 +64,18 @@ public static async Task CreateHostWithSliEnriched(Meter meter) =>
return ValueTask.CompletedTask;
});
})
- .Configure(app =>
- {
- app.UseRouting()
+ .Configure(app => app.UseRouting()
.UseServiceLevelIndicator()
.Use(async (context, next) =>
{
await Task.Delay(MillisecondsDelay);
await next(context);
})
- .UseEndpoints(endpoints => endpoints.MapControllers());
- });
- })
+ .UseEndpoints(endpoints => endpoints.MapControllers())))
.StartAsync();
- public static async Task CreateHostWithoutAutomaticSli(Meter meter)
- {
- return await new HostBuilder()
- .ConfigureWebHost(webBuilder =>
- {
- webBuilder
+ public static async Task CreateHostWithoutAutomaticSli(Meter meter) => await new HostBuilder()
+ .ConfigureWebHost(webBuilder => webBuilder
.UseTestServer()
.ConfigureServices(services =>
{
@@ -103,44 +88,26 @@ public static async Task CreateHostWithoutAutomaticSli(Meter meter)
options.AutomaticallyEmitted = false;
}).AddMvc();
})
- .Configure(app =>
- {
- app.UseRouting()
+ .Configure(app => app.UseRouting()
.UseServiceLevelIndicator()
.Use(async (context, next) =>
{
await Task.Delay(MillisecondsDelay);
await next(context);
})
- .UseEndpoints(endpoints => endpoints.MapControllers());
- });
- })
+ .UseEndpoints(endpoints => endpoints.MapControllers())))
.StartAsync();
- }
- public static async Task CreateHostWithoutSli()
- {
- return await new HostBuilder()
- .ConfigureWebHost(webBuilder =>
- {
- webBuilder
+ public static async Task CreateHostWithoutSli() => await new HostBuilder()
+ .ConfigureWebHost(webBuilder => webBuilder
.UseTestServer()
- .ConfigureServices(services =>
- {
- services.AddControllers();
- })
- .Configure(app =>
- {
- app.UseRouting()
+ .ConfigureServices(services => services.AddControllers())
+ .Configure(app => app.UseRouting()
.Use(async (context, next) =>
{
await Task.Delay(MillisecondsDelay);
await next(context);
})
- .UseEndpoints(endpoints => endpoints.MapControllers());
- });
- })
+ .UseEndpoints(endpoints => endpoints.MapControllers())))
.StartAsync();
- }
-}
-
+}
\ No newline at end of file
diff --git a/ServiceLevelIndicators.sln b/ServiceLevelIndicators.sln
deleted file mode 100644
index 9455ef1..0000000
--- a/ServiceLevelIndicators.sln
+++ /dev/null
@@ -1,114 +0,0 @@
-
-Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.2.32602.215
-MinimumVisualStudioVersion = 10.0.40219.1
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Sample", "Sample", "{A7D8DEF7-8D37-4DE9-B935-7D9FF571132B}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleWebApplicationSLI", "sample\WebApi\SampleWebApplicationSLI.csproj", "{D7E3F524-2505-47DC-ACD8-52BC58DA84A2}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{1D89EC6C-8E06-49C5-A4EF-0218F9E6C29B}"
- ProjectSection(SolutionItems) = preProject
- Directory.Build.props = Directory.Build.props
- Directory.Packages.props = Directory.Packages.props
- global.json = global.json
- version.json = version.json
- EndProjectSection
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleVersionedWebApplicationSLI", "sample\WebApiVersioned\SampleVersionedWebApplicationSLI.csproj", "{A9AB0954-B049-43FD-A676-829F2D5A8F1B}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ServiceLevelIndicators", "ServiceLevelIndicators", "{133B3755-5A30-4EA0-B6F3-49FC7B8D4BB2}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServiceLevelIndicators", "ServiceLevelIndicators\src\ServiceLevelIndicators.csproj", "{CE23EAA9-6584-40B2-B457-CCAFD07475D7}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServiceLevelIndicators.Tests", "ServiceLevelIndicators\tests\ServiceLevelIndicators.Tests.csproj", "{DD4A7A95-B3C2-484B-94B5-F96200EA0087}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ServiceLevelIndicators.Asp", "ServiceLevelIndicators.Asp", "{74558EE8-938D-41C2-8EB8-DC993ECB8A1F}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServiceLevelIndicators.Asp", "ServiceLevelIndicators.Asp\src\ServiceLevelIndicators.Asp.csproj", "{8B3CF99C-7A4C-40D6-8679-93FB041FA011}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServiceLevelIndicators.Asp.Tests", "ServiceLevelIndicators.Asp\tests\ServiceLevelIndicators.Asp.Tests.csproj", "{808C8C7F-5FFF-4476-8A93-BF6CC95222CA}"
-EndProject
-Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ServiceLevelIncidators.Asp.ApiVersioning", "ServiceLevelIncidators.Asp.ApiVersioning", "{2C6E9F62-8968-455A-AD02-DC795296471A}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServiceLevelIndicators.Asp.ApiVersioning", "ServiceLevelIndicators.Asp.ApiVersioning\src\ServiceLevelIndicators.Asp.ApiVersioning.csproj", "{B3F4A43C-64C1-4A41-99DC-0864BB30479C}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServiceLevelIndicators.Asp.ApiVersioning.Tests", "ServiceLevelIndicators.Asp.ApiVersioning\tests\ServiceLevelIndicators.Asp.ApiVersioning.Tests.csproj", "{A776B107-2730-4EB6-B481-7F3EFAC2A2A8}"
-EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleMinimalApiSli", "sample\MinApi\SampleMinimalApiSli.csproj", "{D25F951D-CE1A-47D1-8421-F71BEBF3A356}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GenerateSli", "sample\GenerateSli\GenerateSli.csproj", "{94FA59E4-26A3-4457-8AD7-4091205F1D06}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleConsoleSLI", "sample\ConsoleApp\SampleConsoleSLI.csproj", "{08C09D03-67BE-25CF-68EA-8A94E1EBFE2F}"
-EndProject
-Global
- GlobalSection(SolutionConfigurationPlatforms) = preSolution
- Debug|Any CPU = Debug|Any CPU
- Release|Any CPU = Release|Any CPU
- EndGlobalSection
- GlobalSection(ProjectConfigurationPlatforms) = postSolution
- {D7E3F524-2505-47DC-ACD8-52BC58DA84A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {D7E3F524-2505-47DC-ACD8-52BC58DA84A2}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {D7E3F524-2505-47DC-ACD8-52BC58DA84A2}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {D7E3F524-2505-47DC-ACD8-52BC58DA84A2}.Release|Any CPU.Build.0 = Release|Any CPU
- {A9AB0954-B049-43FD-A676-829F2D5A8F1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {A9AB0954-B049-43FD-A676-829F2D5A8F1B}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {A9AB0954-B049-43FD-A676-829F2D5A8F1B}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {A9AB0954-B049-43FD-A676-829F2D5A8F1B}.Release|Any CPU.Build.0 = Release|Any CPU
- {CE23EAA9-6584-40B2-B457-CCAFD07475D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {CE23EAA9-6584-40B2-B457-CCAFD07475D7}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {CE23EAA9-6584-40B2-B457-CCAFD07475D7}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {CE23EAA9-6584-40B2-B457-CCAFD07475D7}.Release|Any CPU.Build.0 = Release|Any CPU
- {DD4A7A95-B3C2-484B-94B5-F96200EA0087}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {DD4A7A95-B3C2-484B-94B5-F96200EA0087}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {DD4A7A95-B3C2-484B-94B5-F96200EA0087}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {DD4A7A95-B3C2-484B-94B5-F96200EA0087}.Release|Any CPU.Build.0 = Release|Any CPU
- {8B3CF99C-7A4C-40D6-8679-93FB041FA011}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {8B3CF99C-7A4C-40D6-8679-93FB041FA011}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {8B3CF99C-7A4C-40D6-8679-93FB041FA011}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {8B3CF99C-7A4C-40D6-8679-93FB041FA011}.Release|Any CPU.Build.0 = Release|Any CPU
- {808C8C7F-5FFF-4476-8A93-BF6CC95222CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {808C8C7F-5FFF-4476-8A93-BF6CC95222CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {808C8C7F-5FFF-4476-8A93-BF6CC95222CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {808C8C7F-5FFF-4476-8A93-BF6CC95222CA}.Release|Any CPU.Build.0 = Release|Any CPU
- {B3F4A43C-64C1-4A41-99DC-0864BB30479C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {B3F4A43C-64C1-4A41-99DC-0864BB30479C}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {B3F4A43C-64C1-4A41-99DC-0864BB30479C}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {B3F4A43C-64C1-4A41-99DC-0864BB30479C}.Release|Any CPU.Build.0 = Release|Any CPU
- {A776B107-2730-4EB6-B481-7F3EFAC2A2A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {A776B107-2730-4EB6-B481-7F3EFAC2A2A8}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {A776B107-2730-4EB6-B481-7F3EFAC2A2A8}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {A776B107-2730-4EB6-B481-7F3EFAC2A2A8}.Release|Any CPU.Build.0 = Release|Any CPU
- {D25F951D-CE1A-47D1-8421-F71BEBF3A356}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {D25F951D-CE1A-47D1-8421-F71BEBF3A356}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {D25F951D-CE1A-47D1-8421-F71BEBF3A356}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {D25F951D-CE1A-47D1-8421-F71BEBF3A356}.Release|Any CPU.Build.0 = Release|Any CPU
- {94FA59E4-26A3-4457-8AD7-4091205F1D06}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {94FA59E4-26A3-4457-8AD7-4091205F1D06}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {94FA59E4-26A3-4457-8AD7-4091205F1D06}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {94FA59E4-26A3-4457-8AD7-4091205F1D06}.Release|Any CPU.Build.0 = Release|Any CPU
- {08C09D03-67BE-25CF-68EA-8A94E1EBFE2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {08C09D03-67BE-25CF-68EA-8A94E1EBFE2F}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {08C09D03-67BE-25CF-68EA-8A94E1EBFE2F}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {08C09D03-67BE-25CF-68EA-8A94E1EBFE2F}.Release|Any CPU.Build.0 = Release|Any CPU
- EndGlobalSection
- GlobalSection(SolutionProperties) = preSolution
- HideSolutionNode = FALSE
- EndGlobalSection
- GlobalSection(NestedProjects) = preSolution
- {D7E3F524-2505-47DC-ACD8-52BC58DA84A2} = {A7D8DEF7-8D37-4DE9-B935-7D9FF571132B}
- {A9AB0954-B049-43FD-A676-829F2D5A8F1B} = {A7D8DEF7-8D37-4DE9-B935-7D9FF571132B}
- {CE23EAA9-6584-40B2-B457-CCAFD07475D7} = {133B3755-5A30-4EA0-B6F3-49FC7B8D4BB2}
- {DD4A7A95-B3C2-484B-94B5-F96200EA0087} = {133B3755-5A30-4EA0-B6F3-49FC7B8D4BB2}
- {8B3CF99C-7A4C-40D6-8679-93FB041FA011} = {74558EE8-938D-41C2-8EB8-DC993ECB8A1F}
- {808C8C7F-5FFF-4476-8A93-BF6CC95222CA} = {74558EE8-938D-41C2-8EB8-DC993ECB8A1F}
- {B3F4A43C-64C1-4A41-99DC-0864BB30479C} = {2C6E9F62-8968-455A-AD02-DC795296471A}
- {A776B107-2730-4EB6-B481-7F3EFAC2A2A8} = {2C6E9F62-8968-455A-AD02-DC795296471A}
- {D25F951D-CE1A-47D1-8421-F71BEBF3A356} = {A7D8DEF7-8D37-4DE9-B935-7D9FF571132B}
- {94FA59E4-26A3-4457-8AD7-4091205F1D06} = {A7D8DEF7-8D37-4DE9-B935-7D9FF571132B}
- {08C09D03-67BE-25CF-68EA-8A94E1EBFE2F} = {A7D8DEF7-8D37-4DE9-B935-7D9FF571132B}
- EndGlobalSection
- GlobalSection(ExtensibilityGlobals) = postSolution
- SolutionGuid = {5C1087B8-93AA-4847-9D18-6A16E754593F}
- EndGlobalSection
-EndGlobal
diff --git a/ServiceLevelIndicators.slnx b/ServiceLevelIndicators.slnx
new file mode 100644
index 0000000..7322d45
--- /dev/null
+++ b/ServiceLevelIndicators.slnx
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ServiceLevelIndicators/src/IEnrichment.cs b/ServiceLevelIndicators/src/IEnrichment.cs
index ac4eab7..81db63b 100644
--- a/ServiceLevelIndicators/src/IEnrichment.cs
+++ b/ServiceLevelIndicators/src/IEnrichment.cs
@@ -1,8 +1,19 @@
namespace ServiceLevelIndicators;
+
using System.Threading.Tasks;
+///
+/// Defines an enrichment that adds additional attributes to a measurement context.
+///
+/// The enrichment context type.
public interface IEnrichment
where T : IEnrichmentContext
{
+ ///
+ /// Enriches the measurement context with additional attributes.
+ ///
+ /// The enrichment context.
+ /// A cancellation token.
+ /// A representing the asynchronous operation.
ValueTask EnrichAsync(T context, CancellationToken cancellationToken);
-}
+}
\ No newline at end of file
diff --git a/ServiceLevelIndicators/src/IEnrichmentContext.cs b/ServiceLevelIndicators/src/IEnrichmentContext.cs
index 35ca61d..a8781e3 100644
--- a/ServiceLevelIndicators/src/IEnrichmentContext.cs
+++ b/ServiceLevelIndicators/src/IEnrichmentContext.cs
@@ -1,8 +1,25 @@
namespace ServiceLevelIndicators;
+///
+/// Context provided to enrichment callbacks, allowing attributes to be added to a measurement.
+///
public interface IEnrichmentContext
{
+ ///
+ /// Gets the operation name.
+ ///
string Operation { get; }
+
+ ///
+ /// Overrides the customer resource identifier for this measurement.
+ ///
+ /// The customer resource identifier.
void SetCustomerResourceId(string id);
- void AddAttribute(string name, object value);
-}
+
+ ///
+ /// Adds a custom attribute to the measurement.
+ ///
+ /// The attribute name.
+ /// The attribute value.
+ void AddAttribute(string name, object? value);
+}
\ No newline at end of file
diff --git a/ServiceLevelIndicators/src/MeasuredOperation.cs b/ServiceLevelIndicators/src/MeasuredOperation.cs
index 1f3304b..c41bac8 100644
--- a/ServiceLevelIndicators/src/MeasuredOperation.cs
+++ b/ServiceLevelIndicators/src/MeasuredOperation.cs
@@ -4,6 +4,10 @@
using System.Collections.Generic;
using System.Diagnostics;
+///
+/// Represents an in-flight measured operation. Disposing this instance stops the stopwatch
+/// and records the elapsed time as a metric.
+///
public class MeasuredOperation : IDisposable
{
private bool _disposed;
@@ -25,14 +29,32 @@ public MeasuredOperation(ServiceLevelIndicator serviceLevelIndicator, string ope
_stopWatch = Stopwatch.StartNew();
}
+ ///
+ /// Gets or sets the operation name emitted as a tag.
+ ///
public string Operation { get; set; }
+
+ ///
+ /// Gets or sets the customer resource identifier emitted as a tag.
+ ///
public string CustomerResourceId { get; set; }
- // OTEL Attributes to emit
+ ///
+ /// Gets the additional OpenTelemetry attributes emitted with the measurement.
+ ///
public List> Attributes { get; }
+ ///
+ /// Sets the recorded with the measurement.
+ ///
+ /// The activity status code.
public void SetActivityStatusCode(ActivityStatusCode activityStatusCode) => _activityStatusCode = activityStatusCode;
+ ///
+ /// Adds a custom attribute to the measurement.
+ ///
+ /// The attribute name.
+ /// The attribute value.
public void AddAttribute(string attribute, object? value) => Attributes.Add(new KeyValuePair(attribute, value));
protected virtual void Dispose(bool disposing)
@@ -54,10 +76,10 @@ protected virtual void Dispose(bool disposing)
}
}
+ ///
public void Dispose()
{
- // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
-}
+}
\ No newline at end of file
diff --git a/ServiceLevelIndicators/src/ServiceLevelIndicator.cs b/ServiceLevelIndicators/src/ServiceLevelIndicator.cs
index 0a70fd0..8f2d485 100644
--- a/ServiceLevelIndicators/src/ServiceLevelIndicator.cs
+++ b/ServiceLevelIndicators/src/ServiceLevelIndicator.cs
@@ -6,9 +6,24 @@
using System.Reflection;
using Microsoft.Extensions.Options;
+///
+/// Core service that creates and records Service Level Indicator metrics
+/// using an OpenTelemetry .
+///
+///
+/// Registered as a singleton. If the underlying is disposed externally,
+/// recording becomes a silent no-op per OpenTelemetry convention.
+///
public class ServiceLevelIndicator
{
+ ///
+ /// Default meter name used when no is provided in options.
+ ///
public const string DefaultMeterName = nameof(ServiceLevelIndicator);
+
+ ///
+ /// Gets the options used to configure this instance.
+ ///
public ServiceLevelIndicatorOptions ServiceLevelIndicatorOptions { get; }
private readonly Histogram _responseLatencyHistogram;
@@ -16,6 +31,10 @@ public class ServiceLevelIndicator
public ServiceLevelIndicator(IOptions options)
{
ServiceLevelIndicatorOptions = options.Value;
+
+ ArgumentException.ThrowIfNullOrWhiteSpace(ServiceLevelIndicatorOptions.LocationId, nameof(ServiceLevelIndicatorOptions.LocationId));
+ ArgumentException.ThrowIfNullOrWhiteSpace(ServiceLevelIndicatorOptions.DurationInstrumentName, nameof(ServiceLevelIndicatorOptions.DurationInstrumentName));
+
if (ServiceLevelIndicatorOptions.Meter == null)
{
AssemblyName AssemblyName = typeof(ServiceLevelIndicator).Assembly.GetName();
@@ -26,14 +45,27 @@ public ServiceLevelIndicator(IOptions options)
_responseLatencyHistogram = ServiceLevelIndicatorOptions.Meter.CreateHistogram(ServiceLevelIndicatorOptions.DurationInstrumentName, "ms", "Duration of the operation.");
}
+ ///
+ /// Records an operation measurement using the default .
+ ///
+ /// The operation name.
+ /// Elapsed time in milliseconds.
+ /// Additional measurement attributes.
public void Record(string operation, long elapsedTime, params KeyValuePair[] attributes) =>
Record(operation, ServiceLevelIndicatorOptions.CustomerResourceId, elapsedTime, attributes);
- public void Record(string operation, string customerResourseId, long elapsedTime, params KeyValuePair[] attributes)
+ ///
+ /// Records an operation measurement with an explicit customer resource identifier.
+ ///
+ /// The operation name.
+ /// The customer resource identifier.
+ /// Elapsed time in milliseconds.
+ /// Additional measurement attributes.
+ public void Record(string operation, string customerResourceId, long elapsedTime, params KeyValuePair[] attributes)
{
var tagList = new TagList
{
- { "CustomerResourceId", customerResourseId },
+ { "CustomerResourceId", customerResourceId },
{ "LocationId", ServiceLevelIndicatorOptions.LocationId },
{ "Operation", operation }
};
@@ -44,18 +76,37 @@ public void Record(string operation, string customerResourseId, long elapsedTime
_responseLatencyHistogram.Record(elapsedTime, tagList);
}
+ ///
+ /// Starts measuring an operation. Dispose the returned to record the elapsed time.
+ ///
+ /// The operation name.
+ /// Additional measurement attributes.
+ /// A that records the metric on disposal.
public MeasuredOperation StartMeasuring(string operation, params KeyValuePair[] attributes) => new(this, operation, attributes);
+ ///
+ /// Creates a customer resource identifier from a Service Tree GUID.
+ ///
+ /// A non-empty Service Tree identifier.
+ /// A formatted ServiceTreeId:// URI string.
+ /// is .
public static string CreateCustomerResourceId(Guid serviceId)
{
if (serviceId == Guid.Empty) throw new ArgumentNullException(nameof(serviceId));
return "ServiceTreeId://" + serviceId.ToString();
}
+ ///
+ /// Creates a location identifier in the ms-loc://az format.
+ ///
+ /// Cloud name (e.g., "public").
+ /// Optional region (e.g., "West US 3").
+ /// Optional availability zone.
+ /// A formatted location identifier.
public static string CreateLocationId(string cloud, string? region = null, string? zone = null)
{
var arr = new string?[] { "ms-loc://az", cloud, region, zone };
var id = string.Join("/", arr.Where(s => !string.IsNullOrEmpty(s)));
return id;
}
-}
+}
\ No newline at end of file
diff --git a/ServiceLevelIndicators/src/ServiceLevelIndicatorMeterProviderBuilderExtensions.cs b/ServiceLevelIndicators/src/ServiceLevelIndicatorMeterProviderBuilderExtensions.cs
index 1ceaf42..3eda3aa 100644
--- a/ServiceLevelIndicators/src/ServiceLevelIndicatorMeterProviderBuilderExtensions.cs
+++ b/ServiceLevelIndicators/src/ServiceLevelIndicatorMeterProviderBuilderExtensions.cs
@@ -1,4 +1,5 @@
namespace ServiceLevelIndicators;
+
using OpenTelemetry.Metrics;
///
@@ -13,4 +14,4 @@ public static class ServiceLevelIndicatorMeterProviderBuilderExtensions
/// The instance of to chain the calls.
public static MeterProviderBuilder AddServiceLevelIndicatorInstrumentation(this MeterProviderBuilder builder)
=> builder.AddMeter(ServiceLevelIndicator.DefaultMeterName);
-}
+}
\ No newline at end of file
diff --git a/ServiceLevelIndicators/src/ServiceLevelIndicatorOptions.cs b/ServiceLevelIndicators/src/ServiceLevelIndicatorOptions.cs
index e44405b..8b13e4f 100644
--- a/ServiceLevelIndicators/src/ServiceLevelIndicatorOptions.cs
+++ b/ServiceLevelIndicators/src/ServiceLevelIndicatorOptions.cs
@@ -8,28 +8,14 @@
///
public class ServiceLevelIndicatorOptions
{
- private readonly object _meterLock = new();
- private Meter _meter = null!;
-
///
/// The meter that is used to create the histogram that reports the latency.
+ /// Configure this during startup; the value is read once when is constructed.
///
- public Meter Meter
- {
- get
- {
- lock (_meterLock)
- return _meter;
- }
- set
- {
- lock (_meterLock)
- _meter = value;
- }
- }
+ public Meter Meter { get; set; } = null!;
///
- /// CustomerResrouceId is the unique identifier for the customer like subscriptionId, tenantId, etc.
+ /// CustomerResourceId is the unique identifier for the customer like subscriptionId, tenantId, etc.
/// CustomerResourceId can be set for the entire service here or in each API method.
///
public string CustomerResourceId { get; set; } = "Unset";
@@ -55,4 +41,4 @@ public Meter Meter
/// If false, use the ServiceLevelIndicator Attribute to emit.
///
public bool AutomaticallyEmitted { get; set; } = true;
-}
+}
\ No newline at end of file
diff --git a/ServiceLevelIndicators/tests/CustomerResourceIdTests.cs b/ServiceLevelIndicators/tests/CustomerResourceIdTests.cs
index 8c769ae..9c8e10e 100644
--- a/ServiceLevelIndicators/tests/CustomerResourceIdTests.cs
+++ b/ServiceLevelIndicators/tests/CustomerResourceIdTests.cs
@@ -31,4 +31,4 @@ public void Cannot_create_CustomerResourceId_with_default_GUID()
action.Should().Throw()
.WithMessage("Value cannot be null. (Parameter 'serviceId')");
}
-}
+}
\ No newline at end of file
diff --git a/ServiceLevelIndicators/tests/LocationIdTests.cs b/ServiceLevelIndicators/tests/LocationIdTests.cs
index 0343a51..173018e 100644
--- a/ServiceLevelIndicators/tests/LocationIdTests.cs
+++ b/ServiceLevelIndicators/tests/LocationIdTests.cs
@@ -37,4 +37,4 @@ public void Will_create_LocationId_with_cloud_region_zone()
// Assert
actual.Should().Be("ms-loc://az/Public/eastus2/1");
}
-}
+}
\ No newline at end of file
diff --git a/ServiceLevelIndicators/tests/ServiceLevelIndicatorTests.cs b/ServiceLevelIndicators/tests/ServiceLevelIndicatorTests.cs
index 8d92f53..7c1ac26 100644
--- a/ServiceLevelIndicators/tests/ServiceLevelIndicatorTests.cs
+++ b/ServiceLevelIndicators/tests/ServiceLevelIndicatorTests.cs
@@ -1,9 +1,10 @@
namespace ServiceLevelIndicators.Tests;
+
using System;
using System.Collections.Generic;
using System.Diagnostics.Metrics;
+using System.Threading;
using Microsoft.Extensions.Options;
-using Xunit.Abstractions;
public class ServiceLevelIndicatorTests : IDisposable
{
@@ -37,7 +38,6 @@ public ServiceLevelIndicatorTests(ITestOutputHelper output)
_expectedTags = [];
}
-
[Fact]
public void Record()
{
@@ -77,7 +77,6 @@ public void Record()
ValidateMetrics(elapsedTime);
}
-
[Fact]
public async Task Will_measure_code_block()
{
@@ -94,7 +93,6 @@ public async Task Will_measure_code_block()
};
var serviceLevelIndicator = new ServiceLevelIndicator(Options.Create(options));
-
// Act
await MeasureCodeBlock(serviceLevelIndicator);
@@ -117,6 +115,121 @@ async Task MeasureCodeBlock(ServiceLevelIndicator serviceLevelIndicator)
}
}
+ [Fact]
+ public void Uses_default_meter_when_none_provided()
+ {
+ // Arrange
+ var options = new ServiceLevelIndicatorOptions
+ {
+ CustomerResourceId = "TestResourceId",
+ LocationId = "TestLocationId"
+ };
+
+ // Act
+ var serviceLevelIndicator = new ServiceLevelIndicator(Options.Create(options));
+
+ // Assert
+ serviceLevelIndicator.ServiceLevelIndicatorOptions.Meter.Should().NotBeNull();
+ serviceLevelIndicator.ServiceLevelIndicatorOptions.Meter.Name.Should().Be(ServiceLevelIndicator.DefaultMeterName);
+ }
+
+ [Fact]
+ public void Record_with_no_attributes()
+ {
+ // Arrange
+ var customerResourceId = "TestResourceId";
+ var locationId = "TestLocationId";
+
+ var options = new ServiceLevelIndicatorOptions
+ {
+ CustomerResourceId = customerResourceId,
+ LocationId = locationId,
+ Meter = _meter
+ };
+ var serviceLevelIndicator = new ServiceLevelIndicator(Options.Create(options));
+
+ var operation = "TestOperation";
+ var elapsedTime = 50;
+
+ // Act
+ serviceLevelIndicator.Record(operation, elapsedTime);
+
+ // Assert
+ _expectedTags =
+ [
+ new("CustomerResourceId", customerResourceId),
+ new("LocationId", locationId),
+ new("Operation", operation)
+ ];
+
+ ValidateMetrics(elapsedTime);
+ }
+
+ [Fact]
+ public void Record_with_customerResourceId_override()
+ {
+ // Arrange
+ var defaultCustomerResourceId = "DefaultResourceId";
+ var overrideCustomerResourceId = "OverrideResourceId";
+ var locationId = "TestLocationId";
+
+ var options = new ServiceLevelIndicatorOptions
+ {
+ CustomerResourceId = defaultCustomerResourceId,
+ LocationId = locationId,
+ Meter = _meter
+ };
+ var serviceLevelIndicator = new ServiceLevelIndicator(Options.Create(options));
+
+ var operation = "TestOperation";
+ var elapsedTime = 75;
+
+ // Act
+ serviceLevelIndicator.Record(operation, overrideCustomerResourceId, elapsedTime);
+
+ // Assert
+ _expectedTags =
+ [
+ new("CustomerResourceId", overrideCustomerResourceId),
+ new("LocationId", locationId),
+ new("Operation", operation)
+ ];
+
+ ValidateMetrics(elapsedTime);
+ }
+
+ [Fact]
+ public async Task MeasuredOperation_double_dispose_does_not_record_twice()
+ {
+ // Arrange
+ var customerResourceId = "TestResourceId";
+ var locationId = "TestLocationId";
+ int callCount = 0;
+
+ var options = new ServiceLevelIndicatorOptions
+ {
+ CustomerResourceId = customerResourceId,
+ LocationId = locationId,
+ Meter = _meter
+ };
+ var serviceLevelIndicator = new ServiceLevelIndicator(Options.Create(options));
+
+ _meterListener.SetMeasurementEventCallback((Instrument instrument, long measurement, ReadOnlySpan> tags, object? state) =>
+ {
+ Interlocked.Increment(ref callCount);
+ OnMeasurementRecorded(instrument, measurement, tags, state);
+ });
+
+ // Act
+ var measuredOperation = serviceLevelIndicator.StartMeasuring("DoubleDispose");
+ await Task.Delay(50, TestContext.Current.CancellationToken);
+ measuredOperation.SetActivityStatusCode(System.Diagnostics.ActivityStatusCode.Ok);
+ measuredOperation.Dispose();
+ measuredOperation.Dispose(); // Second dispose should be a no-op
+
+ // Assert
+ callCount.Should().Be(1);
+ }
[Fact]
public void Customize_instrument_name()
@@ -194,4 +307,4 @@ public void Dispose()
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
-}
+}
\ No newline at end of file
diff --git a/build/test.props b/build/test.props
index a8ea92e..163d350 100644
--- a/build/test.props
+++ b/build/test.props
@@ -1,12 +1,15 @@
+
+ $(NoWarn);CA1707
+
+
-
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/build/xunit.runner.json b/build/xunit.runner.json
deleted file mode 100644
index da63501..0000000
--- a/build/xunit.runner.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json",
- "methodDisplay": "method",
- "methodDisplayOptions": "all"
-}
\ No newline at end of file
diff --git a/global.json b/global.json
index 26b43b2..f72210c 100644
--- a/global.json
+++ b/global.json
@@ -1,9 +1,6 @@
{
"sdk": {
- "version": "8.0.100",
+ "version": "10.0.100",
"rollForward": "latestFeature"
- },
- "msbuild-sdks": {
- "Microsoft.Build.Traversal": "4.1.0"
}
}
\ No newline at end of file
diff --git a/sample/ConsoleApp/Program.cs b/sample/ConsoleApp/Program.cs
index 107e9c9..f36f059 100644
--- a/sample/ConsoleApp/Program.cs
+++ b/sample/ConsoleApp/Program.cs
@@ -20,7 +20,6 @@
.CreateEmpty()
.AddService(ProgramName, serviceVersion: version, serviceInstanceId: Environment.MachineName);
-
ServiceCollection services = new();
services.Configure(r =>
{
@@ -29,15 +28,12 @@
});
services
- .AddLogging(builder =>
- {
- builder.AddOpenTelemetry(options =>
+ .AddLogging(builder => builder.AddOpenTelemetry(options =>
{
options.SetResourceBuilder(resourceBuilder);
options.AddConsoleExporter();
- });
- })
- .AddSingleton(); ;
+ }))
+ .AddSingleton();
var serviceProvider = services.BuildServiceProvider();
diff --git a/sample/ConsoleApp/SampleConsoleSLI.csproj b/sample/ConsoleApp/SampleConsoleSLI.csproj
index cb5659a..4bc87ed 100644
--- a/sample/ConsoleApp/SampleConsoleSLI.csproj
+++ b/sample/ConsoleApp/SampleConsoleSLI.csproj
@@ -2,7 +2,7 @@
Exe
- net8.0
+ net10.0
enable
enable
diff --git a/sample/DockerOpenTelemetry/docker-compose.yaml b/sample/DockerOpenTelemetry/docker-compose.yaml
deleted file mode 100644
index 82a6610..0000000
--- a/sample/DockerOpenTelemetry/docker-compose.yaml
+++ /dev/null
@@ -1,28 +0,0 @@
-services:
- # back-ends
- zipkin-all-in-one:
- image: openzipkin/zipkin:latest
- ports:
- - "9411:9411"
-
- prometheus:
- container_name: prometheus
- image: prom/prometheus:latest
- volumes:
- - ./prometheus.yaml:/etc/prometheus/prometheus.yml
- ports:
- - "9090:9090"
-
- # OpenTelemetry Collector
- otel-collector:
- image: otel/opentelemetry-collector:latest
- command: ["--config=/etc/otel-collector-config.yaml"]
- volumes:
- - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
- - ./output:/etc/output:rw # Store the logs
- ports:
- - "8888:8888" # Prometheus metrics exposed by the collector
- - "8889:8889" # Prometheus exporter metrics
- - "4317:4317" # OTLP gRPC receiver
- depends_on:
- - zipkin-all-in-one
\ No newline at end of file
diff --git a/sample/DockerOpenTelemetry/otel-collector-config.yaml b/sample/DockerOpenTelemetry/otel-collector-config.yaml
deleted file mode 100644
index 6ddb9aa..0000000
--- a/sample/DockerOpenTelemetry/otel-collector-config.yaml
+++ /dev/null
@@ -1,50 +0,0 @@
-# Configure receivers
-# We only need otlp protocol on grpc, but you can use http, zipkin, jaeger, aws, etc.
-# https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/receiver
-receivers:
- otlp:
- protocols:
- grpc:
-
-# Configure exporters
-exporters:
- # Export prometheus endpoint
- prometheus:
- endpoint: "0.0.0.0:8889"
-
- # log to the console
- logging:
-
- # Export to zipkin
- zipkin:
- endpoint: "http://zipkin-all-in-one:9411/api/v2/spans"
- format: proto
-
- # Export to a file
- file:
- path: /etc/output/logs.json
-
-# Configure processors (batch, sampling, filtering, hashing sensitive data, etc.)
-# https://opentelemetry.io/docs/collector/configuration/#processors
-processors:
- batch:
-
-# Configure pipelines. Pipeline defines a path the data follows in the Collector
-# starting from reception, then further processing or modification and finally
-# exiting the Collector via exporters.
-# https://opentelemetry.io/docs/collector/configuration/#service
-# https://github.com/open-telemetry/opentelemetry-collector/blob/main/docs/design.md#pipelines
-service:
- pipelines:
- traces:
- receivers: [otlp]
- processors: [batch]
- exporters: [logging, zipkin]
- metrics:
- receivers: [otlp]
- processors: [batch]
- exporters: [logging, prometheus]
- logs:
- receivers: [otlp]
- processors: []
- exporters: [logging, file]
\ No newline at end of file
diff --git a/sample/DockerOpenTelemetry/prometheus.yaml b/sample/DockerOpenTelemetry/prometheus.yaml
deleted file mode 100644
index a5d2642..0000000
--- a/sample/DockerOpenTelemetry/prometheus.yaml
+++ /dev/null
@@ -1,6 +0,0 @@
-scrape_configs:
-- job_name: 'otel-collector'
- scrape_interval: 10s
- static_configs:
- - targets: ['otel-collector:8889']
- - targets: ['otel-collector:8888']
\ No newline at end of file
diff --git a/sample/DockerOpenTelemetry/run.cmd b/sample/DockerOpenTelemetry/run.cmd
index 7d0dc6c..17f1360 100644
--- a/sample/DockerOpenTelemetry/run.cmd
+++ b/sample/DockerOpenTelemetry/run.cmd
@@ -1 +1 @@
-docker-compose up
\ No newline at end of file
+docker run --rm -it -d -p 18888:18888 -p 4317:18889 --name aspire-dashboard mcr.microsoft.com/dotnet/aspire-dashboard:latest
\ No newline at end of file
diff --git a/sample/GenerateSli/GenerateSli.csproj b/sample/GenerateSli/GenerateSli.csproj
index 6c4d6bb..c8256ec 100644
--- a/sample/GenerateSli/GenerateSli.csproj
+++ b/sample/GenerateSli/GenerateSli.csproj
@@ -2,7 +2,7 @@
Exe
- net8.0
+ net10.0
enable
enable
false
diff --git a/sample/GenerateSli/Program.cs b/sample/GenerateSli/Program.cs
index 923dfe0..346491c 100644
--- a/sample/GenerateSli/Program.cs
+++ b/sample/GenerateSli/Program.cs
@@ -35,7 +35,4 @@ static async Task ClientRequests()
Console.WriteLine($"Request {i}: Exception occurred: {ex.Message}");
}
}
-}
-
-
-
+}
\ No newline at end of file
diff --git a/sample/MinApi/Program.cs b/sample/MinApi/Program.cs
index d5ba528..f58e5de 100644
--- a/sample/MinApi/Program.cs
+++ b/sample/MinApi/Program.cs
@@ -25,7 +25,7 @@
// Build a resource configuration action to set service information.
Action configureResource = r => r.AddService(
- serviceName: "SampleServiceName",
+ serviceName: "SampleMinimalApiSli",
serviceVersion: typeof(Program).Assembly.GetName().Version?.ToString() ?? "unknown");
builder.Services.AddOpenTelemetry()
@@ -36,7 +36,6 @@
builder.AddOtlpExporter();
});
-
builder.Services.AddServiceLevelIndicator(options =>
{
options.CustomerResourceId = "SampleCustomerResourceId";
diff --git a/sample/MinApi/UserExt.cs b/sample/MinApi/UserExt.cs
index 25da247..86c39cd 100644
--- a/sample/MinApi/UserExt.cs
+++ b/sample/MinApi/UserExt.cs
@@ -1,4 +1,5 @@
namespace SampleMinimalApiSli;
+
using ServiceLevelIndicators;
///
@@ -15,10 +16,9 @@ public static void UseUserRoute(this WebApplication app)
var userApi = app.MapGroup("/users")
.AddServiceLevelIndicator();
-
userApi.MapGet("/", () => "Hello Users");
userApi.MapGet("/{name}", (string name) => $"Hello {name}").WithName("GetUserById");
}
-}
+}
\ No newline at end of file
diff --git a/sample/WebApi/Api.http b/sample/WebApi/Api.http
new file mode 100644
index 0000000..4f7d785
--- /dev/null
+++ b/sample/WebApi/Api.http
@@ -0,0 +1,17 @@
+@HostAddress = https://localhost:63936
+
+### Get weather forecast
+GET {{HostAddress}}/WeatherForecast
+Accept: application/json
+
+### Get weather forecast (MyAction1)
+GET {{HostAddress}}/WeatherForecast/MyAction1
+Accept: application/json
+
+### Get weather forecast (custom operation "MyOperation")
+GET {{HostAddress}}/WeatherForecast/MyAction2
+Accept: application/json
+
+### Get weather forecast with customer resource id
+GET {{HostAddress}}/WeatherForecast/my-customer-resource-id
+Accept: application/json
diff --git a/sample/WebApi/ConfigureServiceLevelIndicatorOptions.cs b/sample/WebApi/ConfigureServiceLevelIndicatorOptions.cs
index 0e64e97..e833c54 100644
--- a/sample/WebApi/ConfigureServiceLevelIndicatorOptions.cs
+++ b/sample/WebApi/ConfigureServiceLevelIndicatorOptions.cs
@@ -10,4 +10,4 @@ internal sealed class ConfigureServiceLevelIndicatorOptions : IConfigureOptions<
public ConfigureServiceLevelIndicatorOptions(SampleApiMeters meters) => this.meters = meters;
public void Configure(ServiceLevelIndicatorOptions options) => options.Meter = meters.Meter;
-}
+}
\ No newline at end of file
diff --git a/sample/WebApi/Controllers/WeatherForecastController.cs b/sample/WebApi/Controllers/WeatherForecastController.cs
index daae991..a85762c 100644
--- a/sample/WebApi/Controllers/WeatherForecastController.cs
+++ b/sample/WebApi/Controllers/WeatherForecastController.cs
@@ -19,7 +19,7 @@ public class WeatherForecastController : ControllerBase
///
/// Should emit SLI metrics
/// Operation: "GET WeatherForecast"
- /// CustomerResourceId = "SampleCustomerResrouceId"
+ /// CustomerResourceId = "SampleCustomerResourceId"
///
[HttpGet]
public IEnumerable Get() => GetWeather();
@@ -27,7 +27,7 @@ public class WeatherForecastController : ControllerBase
///
/// Should emit SLI metrics
/// Operation: "GET WeatherForecast/MyAction1"
- /// CustomerResourceId = "SampleCustomerResrouceId"
+ /// CustomerResourceId = "SampleCustomerResourceId"
///
[HttpGet("MyAction1")]
@@ -36,7 +36,7 @@ public class WeatherForecastController : ControllerBase
///
/// Should emit SLI metrics
/// Operation: "MyOperation"
- /// CustomerResourceId = "SampleCustomerResrouceId"
+ /// CustomerResourceId = "SampleCustomerResourceId"
///
[HttpGet("MyAction2")]
[ServiceLevelIndicator(Operation = "MyOperation")]
@@ -50,14 +50,11 @@ public class WeatherForecastController : ControllerBase
[HttpGet("{customerResourceId}")]
public IEnumerable Get([CustomerResourceId] string customerResourceId) => GetWeather();
- private static WeatherForecast[] GetWeather()
+ private static WeatherForecast[] GetWeather() => Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
- return Enumerable.Range(1, 5).Select(index => new WeatherForecast
- {
- Date = DateTime.Now.AddDays(index),
- TemperatureC = Random.Shared.Next(-20, 55),
- Summary = Summaries[Random.Shared.Next(Summaries.Length)]
- })
+ Date = DateTime.Now.AddDays(index),
+ TemperatureC = Random.Shared.Next(-20, 55),
+ Summary = Summaries[Random.Shared.Next(Summaries.Length)]
+ })
.ToArray();
- }
-}
+}
\ No newline at end of file
diff --git a/sample/WebApi/Program.cs b/sample/WebApi/Program.cs
index dd056f3..7eb2fdc 100644
--- a/sample/WebApi/Program.cs
+++ b/sample/WebApi/Program.cs
@@ -24,7 +24,7 @@
// Build a resource configuration action to set service information.
Action configureResource = r => r.AddService(
- serviceName: "SampleServiceName",
+ serviceName: "SampleWebApplicationSLI",
serviceVersion: typeof(Program).Assembly.GetName().Version?.ToString() ?? "unknown");
builder.Services.AddOpenTelemetry()
diff --git a/sample/WebApi/SampleApiMeters.cs b/sample/WebApi/SampleApiMeters.cs
index 3663b54..343baf9 100644
--- a/sample/WebApi/SampleApiMeters.cs
+++ b/sample/WebApi/SampleApiMeters.cs
@@ -1,11 +1,10 @@
-namespace SampleWebApplicationSLI
-{
- using System.Diagnostics.Metrics;
+namespace SampleWebApplicationSLI;
+
+using System.Diagnostics.Metrics;
- internal class SampleApiMeters
- {
- public const string MeterName = "SampleMeter";
+internal class SampleApiMeters
+{
+ public const string MeterName = "SampleMeter";
- public Meter Meter { get; } = new Meter(MeterName);
- }
-}
+ public Meter Meter { get; } = new Meter(MeterName);
+}
\ No newline at end of file
diff --git a/sample/WebApi/WeatherForecast.cs b/sample/WebApi/WeatherForecast.cs
index 28270ba..0893baa 100644
--- a/sample/WebApi/WeatherForecast.cs
+++ b/sample/WebApi/WeatherForecast.cs
@@ -1,28 +1,27 @@
-namespace SampleWebApplicationSLI
+namespace SampleWebApplicationSLI;
+
+///
+/// Weather Forecast
+///
+public class WeatherForecast
{
///
- /// Weather Forecast
+ /// Date of recording
///
- public class WeatherForecast
- {
- ///
- /// Date of recording
- ///
- public DateTime Date { get; set; }
+ public DateTime Date { get; set; }
- ///
- /// Temperature in Centigrade.
- ///
- public int TemperatureC { get; set; }
+ ///
+ /// Temperature in Centigrade.
+ ///
+ public int TemperatureC { get; set; }
- ///
- /// Temperature in Fahrenheit.
- ///
- public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
+ ///
+ /// Temperature in Fahrenheit.
+ ///
+ public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
- ///
- /// Temperature feeling.
- ///
- public string? Summary { get; set; }
- }
-}
+ ///
+ /// Temperature feeling.
+ ///
+ public string? Summary { get; set; }
+}
\ No newline at end of file
diff --git a/sample/WebApiVersioned/AddApiVersionMetadata.cs b/sample/WebApiVersioned/AddApiVersionMetadata.cs
index b07e4d8..f8e7a7d 100644
--- a/sample/WebApiVersioned/AddApiVersionMetadata.cs
+++ b/sample/WebApiVersioned/AddApiVersionMetadata.cs
@@ -1,11 +1,11 @@
namespace SampleVersionedWebApplicationSLI;
+using System.Globalization;
+using System.Text.Json;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
-using System.Globalization;
-using System.Text.Json;
///
/// Represents the OpenAPI/Swashbuckle operation filter used to document information provided, but not used.
@@ -19,7 +19,7 @@ public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var apiDescription = context.ApiDescription;
- operation.Deprecated |= apiDescription.IsDeprecated();
+ operation.Deprecated |= apiDescription.IsDeprecated;
// REF: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/1752#issue-663991077
foreach (var responseType in context.ApiDescription.SupportedResponseTypes)
@@ -66,4 +66,4 @@ description.DefaultValue is not DBNull &&
parameter.Required |= description.IsRequired;
}
}
-}
+}
\ No newline at end of file
diff --git a/sample/WebApiVersioned/Api.http b/sample/WebApiVersioned/Api.http
new file mode 100644
index 0000000..b663576
--- /dev/null
+++ b/sample/WebApiVersioned/Api.http
@@ -0,0 +1,17 @@
+@HostAddress = https://localhost:63936
+
+### Hello World (default version)
+GET {{HostAddress}}/hello-world
+Accept: application/json
+
+### Hello World with specific API version
+GET {{HostAddress}}/hello-world?api-version=2023-08-06
+Accept: application/json
+
+### Hello World with name (customer resource id)
+GET {{HostAddress}}/hello-world/Xavier
+Accept: application/json
+
+### Hello World with name and specific API version
+GET {{HostAddress}}/hello-world/Xavier?api-version=2023-08-06
+Accept: application/json
diff --git a/sample/WebApiVersioned/ConfigureSwaggerDefaultOptions.cs b/sample/WebApiVersioned/ConfigureSwaggerDefaultOptions.cs
index bc8e66f..7a3279f 100644
--- a/sample/WebApiVersioned/ConfigureSwaggerDefaultOptions.cs
+++ b/sample/WebApiVersioned/ConfigureSwaggerDefaultOptions.cs
@@ -1,12 +1,12 @@
namespace SampleVersionedWebApplicationSLI;
+using System.Text;
using Asp.Versioning;
using Asp.Versioning.ApiExplorer;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
-using System.Text;
///
/// Configures the Swagger generation options.
@@ -86,4 +86,4 @@ private static OpenApiInfo CreateInfoForApiVersion(ApiVersionDescription descrip
return info;
}
-}
+}
\ No newline at end of file
diff --git a/sample/WebApiVersioned/Controllers/2023-06-06/HelloWorldController.cs b/sample/WebApiVersioned/Controllers/2023-06-06/HelloWorldController.cs
index 14728f9..0562d09 100644
--- a/sample/WebApiVersioned/Controllers/2023-06-06/HelloWorldController.cs
+++ b/sample/WebApiVersioned/Controllers/2023-06-06/HelloWorldController.cs
@@ -39,4 +39,4 @@ public ActionResult GetCustom([CustomerResourceId] string name)
if (next < 20) return StatusCode(StatusCodes.Status500InternalServerError, "Sim Server error");
return Ok("Hello World " + name);
}
-}
+}
\ No newline at end of file
diff --git a/sample/WebApiVersioned/Program.cs b/sample/WebApiVersioned/Program.cs
index 1ec6548..742b310 100644
--- a/sample/WebApiVersioned/Program.cs
+++ b/sample/WebApiVersioned/Program.cs
@@ -1,10 +1,10 @@
-using OpenTelemetry.Metrics;
+using Azure.Core;
+using Microsoft.Extensions.Options;
+using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
+using SampleVersionedWebApplicationSLI;
using ServiceLevelIndicators;
-using Microsoft.Extensions.Options;
using Swashbuckle.AspNetCore.SwaggerGen;
-using SampleVersionedWebApplicationSLI;
-using Azure.Core;
var builder = WebApplication.CreateBuilder(args);
@@ -33,7 +33,7 @@
builder.Services.AddProblemDetails();
Action configureResource = r => r.AddService(
- serviceName: "SampleServiceName",
+ serviceName: "SampleVersionedWebApplicationSLI",
serviceVersion: typeof(Program).Assembly.GetName().Version?.ToString() ?? "unknown");
builder.Services.AddOpenTelemetry()
.ConfigureResource(configureResource)
@@ -43,11 +43,7 @@
builder.AddOtlpExporter();
});
-
-builder.Services.AddServiceLevelIndicator(options =>
-{
- options.LocationId = ServiceLevelIndicator.CreateLocationId("public", AzureLocation.WestUS3.Name);
-})
+builder.Services.AddServiceLevelIndicator(options => options.LocationId = ServiceLevelIndicator.CreateLocationId("public", AzureLocation.WestUS3.Name))
.AddMvc()
.AddApiVersion();
diff --git a/sample/WebApiVersioned/SampleVersionedWebApplicationSLI.csproj b/sample/WebApiVersioned/SampleVersionedWebApplicationSLI.csproj
index 7684525..77707f7 100644
--- a/sample/WebApiVersioned/SampleVersionedWebApplicationSLI.csproj
+++ b/sample/WebApiVersioned/SampleVersionedWebApplicationSLI.csproj
@@ -2,6 +2,7 @@
True
false
+ $(NoWarn);CA1707
diff --git a/version.json b/version.json
index 381e084..ba31006 100644
--- a/version.json
+++ b/version.json
@@ -1,6 +1,6 @@
{
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/master/src/NerdBank.GitVersioning/version.schema.json",
- "version": "8.0",
+ "version": "10.0-preview.{height}",
"nuGetPackageVersion": {
"semVer": 2.0
},