├── .editorconfig ├── .github └── workflows │ ├── Allegro.Prometheus.TrueRpsMetric.ci.yml │ ├── Allegro.Prometheus.TrueRpsMetric.publish.yml │ └── template.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── ap-catalog-info.yaml ├── docs └── index.md ├── mkdocs.yml ├── samples └── Allegro.Prometheus.Demo │ ├── Allegro.Prometheus.Demo.csproj │ ├── Allegro.Prometheus.Demo.sln │ ├── Controllers │ └── DemoController.cs │ ├── Program.cs │ └── Properties │ └── launchSettings.json └── src ├── Allegro.Prometheus.TrueRpsMetric ├── Allegro.Prometheus.TrueRpsMetric.Tests │ ├── Allegro.Prometheus.TrueRpsMetric.Tests.csproj │ ├── MaxPerSecondGaugeTests.cs │ └── TrueRpsMetricFactoryTests.cs ├── Allegro.Prometheus.TrueRpsMetric.sln ├── Allegro.Prometheus.TrueRpsMetric │ ├── Allegro.Prometheus.TrueRpsMetric.csproj │ ├── MaxPerSecondGauge.cs │ ├── TrueRpsMetricConfiguration.cs │ ├── TrueRpsMetricFactory.cs │ └── TrueRpsMiddleware.cs ├── CHANGELOG.md └── README.md ├── Directory.Build.props ├── Directory.Build.targets ├── Package.Build.props └── stylecop.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # Don't use tabs for indentation. 5 | [*] 6 | indent_style = space 7 | 8 | # (Please don't specify an indent_size here; that has too many unintended consequences.) 9 | 10 | # Code files 11 | [*.{cs,csx,fs,fsx}] 12 | indent_size = 4 13 | charset = utf-8 14 | trim_trailing_whitespace = true 15 | insert_final_newline = false 16 | 17 | # XML project files 18 | [*.{csproj,fsproj}] 19 | indent_size = 2 20 | 21 | # XML config files 22 | [*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] 23 | indent_size = 2 24 | 25 | # JSON files 26 | [*.json] 27 | indent_size = 2 28 | 29 | # Powershell files 30 | [*.ps1] 31 | indent_size = 2 32 | 33 | # Shell script files 34 | [*.sh] 35 | end_of_line = lf 36 | indent_size = 2 37 | 38 | # Dotnet code style settings: 39 | [*.{cs,fs}] 40 | 41 | # Sort using and Import directives with System.* appearing first 42 | dotnet_sort_system_directives_first = true:refactoring 43 | dotnet_separate_import_directive_groups = false:refactoring 44 | 45 | # Avoid "this." and "Me." if not necessary 46 | dotnet_style_qualification_for_field = false:error 47 | dotnet_style_qualification_for_property = false:error 48 | dotnet_style_qualification_for_method = false:error 49 | dotnet_style_qualification_for_event = false:error 50 | 51 | # Use language keywords instead of framework type names for type references 52 | dotnet_style_predefined_type_for_locals_parameters_members = true:error 53 | dotnet_style_predefined_type_for_member_access = true:error 54 | 55 | # Suggest more modern language features when available 56 | dotnet_style_object_initializer = true:suggestion 57 | dotnet_style_collection_initializer = true:suggestion 58 | dotnet_style_coalesce_expression = true:suggestion 59 | dotnet_style_null_propagation = true:suggestion 60 | dotnet_style_explicit_tuple_names = true:suggestion 61 | 62 | # Whitespace options 63 | dotnet_style_allow_multiple_blank_lines_experimental = false 64 | dotnet_style_allow_statement_immediately_after_block_experimental = false 65 | 66 | # Non-private static fields are PascalCase 67 | dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.severity = suggestion 68 | dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non_private_static_fields 69 | dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style 70 | 71 | dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field 72 | dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected 73 | dotnet_naming_symbols.non_private_static_fields.required_modifiers = static 74 | 75 | dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case 76 | 77 | # Non-private readonly fields are PascalCase 78 | dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.severity = suggestion 79 | dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.symbols = non_private_readonly_fields 80 | dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.style = non_private_readonly_field_style 81 | 82 | dotnet_naming_symbols.non_private_readonly_fields.applicable_kinds = field 83 | dotnet_naming_symbols.non_private_readonly_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected 84 | dotnet_naming_symbols.non_private_readonly_fields.required_modifiers = readonly 85 | 86 | dotnet_naming_style.non_private_readonly_field_style.capitalization = pascal_case 87 | 88 | # Constants are PascalCase 89 | dotnet_naming_rule.constants_should_be_pascal_case.severity = suggestion 90 | dotnet_naming_rule.constants_should_be_pascal_case.symbols = constants 91 | dotnet_naming_rule.constants_should_be_pascal_case.style = constant_style 92 | 93 | dotnet_naming_symbols.constants.applicable_kinds = field 94 | dotnet_naming_symbols.constants.required_modifiers = const 95 | 96 | dotnet_naming_style.constant_style.capitalization = pascal_case 97 | 98 | # Static fields are camelCase and start with s_ 99 | dotnet_naming_rule.static_fields_should_be_camel_case.severity = suggestion 100 | dotnet_naming_rule.static_fields_should_be_camel_case.symbols = static_fields 101 | dotnet_naming_rule.static_fields_should_be_camel_case.style = static_field_style 102 | 103 | dotnet_naming_symbols.static_fields.applicable_kinds = field 104 | dotnet_naming_symbols.static_fields.required_modifiers = static 105 | 106 | dotnet_naming_style.static_field_style.capitalization = pascal_case 107 | #dotnet_naming_style.static_field_style.required_prefix = s_ 108 | 109 | # Instance fields are camelCase and start with _ 110 | dotnet_naming_rule.instance_fields_should_be_camel_case.severity = suggestion 111 | dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields 112 | dotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_style 113 | 114 | dotnet_naming_symbols.instance_fields.applicable_kinds = field 115 | 116 | dotnet_naming_style.instance_field_style.capitalization = camel_case 117 | dotnet_naming_style.instance_field_style.required_prefix = _ 118 | 119 | # Locals and parameters are camelCase 120 | dotnet_naming_rule.locals_should_be_camel_case.severity = suggestion 121 | dotnet_naming_rule.locals_should_be_camel_case.symbols = locals_and_parameters 122 | dotnet_naming_rule.locals_should_be_camel_case.style = camel_case_style 123 | 124 | dotnet_naming_symbols.locals_and_parameters.applicable_kinds = parameter, local 125 | 126 | dotnet_naming_style.camel_case_style.capitalization = camel_case 127 | 128 | # Local functions are PascalCase 129 | dotnet_naming_rule.local_functions_should_be_pascal_case.severity = suggestion 130 | dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = local_functions 131 | dotnet_naming_rule.local_functions_should_be_pascal_case.style = local_function_style 132 | 133 | dotnet_naming_symbols.local_functions.applicable_kinds = local_function 134 | 135 | dotnet_naming_style.local_function_style.capitalization = pascal_case 136 | 137 | # By default, name items with PascalCase 138 | dotnet_naming_rule.members_should_be_pascal_case.severity = suggestion 139 | dotnet_naming_rule.members_should_be_pascal_case.symbols = all_members 140 | dotnet_naming_rule.members_should_be_pascal_case.style = pascal_case_style 141 | 142 | dotnet_naming_symbols.all_members.applicable_kinds = * 143 | 144 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case 145 | 146 | # IDE0036: Order modifiers 147 | dotnet_diagnostic.IDE0036.severity = warning 148 | 149 | # IDE0043: Format string contains invalid placeholder 150 | dotnet_diagnostic.IDE0043.severity = warning 151 | 152 | # IDE0044: Make field readonly 153 | dotnet_diagnostic.IDE0044.severity = warning 154 | 155 | # IDE0011: Add braces 156 | csharp_prefer_braces = when_multiline:warning 157 | # NOTE: We need the below severity entry for Add Braces due to https://github.com/dotnet/roslyn/issues/44201 158 | # TODO: decide if we want to suggest it or not 159 | dotnet_diagnostic.IDE0011.severity = suggestion 160 | 161 | # IDE0040: Add accessibility modifiers 162 | dotnet_diagnostic.IDE0040.severity = warning 163 | 164 | # CONSIDER: Are IDE0051 and IDE0052 too noisy to be warnings for IDE editing scenarios? Should they be made build-only warnings? 165 | # IDE0051: Remove unused private member 166 | dotnet_diagnostic.IDE0051.severity = warning 167 | 168 | # IDE0052: Remove unread private member 169 | dotnet_diagnostic.IDE0052.severity = suggestion 170 | 171 | # IDE0059: Unnecessary assignment to a value 172 | dotnet_diagnostic.IDE0059.severity = suggestion 173 | 174 | # IDE0060: Remove unused parameter 175 | dotnet_diagnostic.IDE0060.severity = suggestion 176 | 177 | # CA1012: Abstract types should not have public constructors 178 | dotnet_diagnostic.CA1012.severity = warning 179 | 180 | # CA1822: Make member static 181 | dotnet_diagnostic.CA1822.severity = warning 182 | 183 | # IDE0005: Using directive is unnecessary 184 | dotnet_diagnostic.IDE0005.severity = warning 185 | 186 | # dotnet_style_allow_multiple_blank_lines_experimental 187 | dotnet_diagnostic.IDE2000.severity = warning 188 | 189 | # csharp_style_allow_embedded_statements_on_same_line_experimental 190 | dotnet_diagnostic.IDE2001.severity = warning 191 | 192 | # csharp_style_allow_blank_lines_between_consecutive_braces_experimental 193 | dotnet_diagnostic.IDE2002.severity = warning 194 | 195 | # dotnet_style_allow_statement_immediately_after_block_experimental 196 | dotnet_diagnostic.IDE2003.severity = suggestion 197 | 198 | # csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental 199 | dotnet_diagnostic.IDE2004.severity = warning 200 | 201 | # CSharp code style settings: 202 | [*.cs] 203 | # Newline settings 204 | csharp_new_line_before_open_brace = all 205 | csharp_new_line_before_else = true 206 | csharp_new_line_before_catch = true 207 | csharp_new_line_before_finally = true 208 | csharp_new_line_before_members_in_object_initializers = true 209 | csharp_new_line_before_members_in_anonymous_types = true 210 | csharp_new_line_between_query_expression_clauses = true 211 | 212 | # Indentation preferences 213 | csharp_indent_block_contents = true 214 | csharp_indent_braces = false 215 | csharp_indent_case_contents = true 216 | csharp_indent_case_contents_when_block = true 217 | csharp_indent_switch_labels = true 218 | csharp_indent_labels = flush_left 219 | 220 | # Whitespace options 221 | csharp_style_allow_embedded_statements_on_same_line_experimental = false 222 | csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false 223 | csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = false 224 | 225 | # Prefer "var" everywhere 226 | dotnet_diagnostic.IDE0007.severity = warning 227 | csharp_style_var_for_built_in_types = true:error 228 | csharp_style_var_when_type_is_apparent = true:error 229 | csharp_style_var_elsewhere = true:error 230 | 231 | # Prefer method-like constructs to have a block body 232 | csharp_style_expression_bodied_methods = false:none 233 | csharp_style_expression_bodied_constructors = false:none 234 | csharp_style_expression_bodied_operators = false:none 235 | 236 | # Prefer property-like constructs to have an expression-body 237 | csharp_style_expression_bodied_properties = true:error 238 | csharp_style_expression_bodied_indexers = true:error 239 | csharp_style_expression_bodied_accessors = true:error 240 | 241 | # Suggest more modern language features when available 242 | csharp_style_pattern_matching_over_is_with_cast_check = true:error 243 | csharp_style_pattern_matching_over_as_with_null_check = true:error 244 | csharp_style_inlined_variable_declaration = true:suggestion 245 | csharp_style_throw_expression = true:error 246 | csharp_style_conditional_delegate_call = true:suggestion 247 | 248 | # Spacing 249 | csharp_space_after_cast = false 250 | csharp_space_after_colon_in_inheritance_clause = true 251 | csharp_space_after_comma = true 252 | csharp_space_after_dot = false 253 | csharp_space_after_keywords_in_control_flow_statements = true 254 | csharp_space_after_semicolon_in_for_statement = true 255 | csharp_space_around_binary_operators = before_and_after 256 | csharp_space_around_declaration_statements = do_not_ignore 257 | csharp_space_before_colon_in_inheritance_clause = true 258 | csharp_space_before_comma = false 259 | csharp_space_before_dot = false 260 | csharp_space_before_open_square_brackets = false 261 | csharp_space_before_semicolon_in_for_statement = false 262 | csharp_space_between_empty_square_brackets = false 263 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 264 | csharp_space_between_method_call_name_and_opening_parenthesis = false 265 | csharp_space_between_method_call_parameter_list_parentheses = false 266 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 267 | csharp_space_between_method_declaration_name_and_open_parenthesis = false 268 | csharp_space_between_method_declaration_parameter_list_parentheses = false 269 | csharp_space_between_parentheses = false 270 | csharp_space_between_square_brackets = false 271 | 272 | # Blocks are allowed 273 | csharp_prefer_braces = true:silent 274 | csharp_preserve_single_line_blocks = true 275 | csharp_preserve_single_line_statements = true 276 | 277 | # CS1591: Missing XML comment for publicly visible type or member 278 | dotnet_diagnostic.CS1591.severity = none 279 | 280 | # SA0001: All diagnostics of XML documentation comments has been disabled due to the current project configuration. 281 | dotnet_diagnostic.SA0001.severity = none 282 | 283 | # SA1009: A closing parenthesis within a C# statement is not spaced correctly. 284 | # TODO: it's broken (doesn't work with nullable types) 285 | dotnet_diagnostic.SA1009.severity = none 286 | 287 | # SA1011: A closing square bracket within a C# statement is not spaced correctly. 288 | # TODO: false positive with NRT 289 | dotnet_diagnostic.SA1011.severity = none 290 | 291 | # SA1101: Prefix local calls with this 292 | dotnet_diagnostic.SA1101.severity = none 293 | 294 | # SA1111: The closing parenthesis or bracket in a call to a C# method or indexer, 295 | # or the declaration of a method or indexer, is not placed on the same line as the last parameter. 296 | dotnet_diagnostic.SA1111.severity = suggestion 297 | 298 | # SA1118: A parameter to a C# method or indexer, other than the first parameter, spans across multiple lines. 299 | dotnet_diagnostic.SA1118.severity = suggestion 300 | 301 | # SA1123: The C# code contains a region within the body of a code element. 302 | dotnet_diagnostic.SA1123.severity = suggestion 303 | 304 | # SA1124: The C# code contains a region. 305 | dotnet_diagnostic.SA1124.severity = suggestion 306 | 307 | # SA1127: A generic constraint on a type or method declaration is on the same line as the declaration, within a C# code file. 308 | dotnet_diagnostic.SA1127.severity = suggestion 309 | 310 | # SA1128: A constructor initializer is on the same line as the constructor declaration, within a C# code file. 311 | dotnet_diagnostic.SA1128.severity = suggestion 312 | 313 | # SA1134: An attribute is placed on the same line of code as another attribute or element. 314 | dotnet_diagnostic.SA1134.severity = suggestion 315 | 316 | # SA1201: Elements should appear in the correct order 317 | dotnet_diagnostic.SA1201.severity = none 318 | 319 | # SA1202: An element within a C# code file is out of order within regard to access level 320 | dotnet_diagnostic.SA1202.severity = suggestion 321 | 322 | # SA1204: Static elements should appear before instance elements 323 | dotnet_diagnostic.SA1204.severity = suggestion 324 | 325 | # SA1309: Field names should not begin with underscore 326 | dotnet_diagnostic.SA1309.severity = none 327 | 328 | # SA1401: A field within a C# class has an access modifier other than private. 329 | dotnet_diagnostic.SA1401.severity = suggestion 330 | 331 | # SA1402: A C# code file contains more than one unique type. 332 | dotnet_diagnostic.SA1402.severity = none 333 | 334 | # SA1413: Use trailing commas in multi line initializers 335 | dotnet_diagnostic.SA1413.severity = none 336 | 337 | # SA1501: A C# statement containing opening and closing braces is written completely on a single line. 338 | dotnet_diagnostic.SA1501.severity = suggestion 339 | 340 | # SA1502: A C# element containing opening and closing braces is written completely on a single line. 341 | dotnet_diagnostic.SA1502.severity = suggestion 342 | 343 | # SA1503: The opening and closing braces for a C# statement have been omitted. 344 | dotnet_diagnostic.SA1503.severity = none 345 | 346 | # SA1512: A single-line comment within C# code is followed by a blank line. 347 | dotnet_diagnostic.SA1512.severity = suggestion 348 | 349 | # SA1515: A single-line comment within C# code is not preceded by a blank line. 350 | dotnet_diagnostic.SA1515.severity = suggestion 351 | 352 | # SA1516: Elements should be separated by blank line 353 | dotnet_diagnostic.SA1516.severity = suggestion 354 | 355 | # SA1519: The opening and closing braces for a multi-line C# statement have been omitted. 356 | dotnet_diagnostic.SA1519.severity = suggestion 357 | 358 | # SA1600: A C# code element is missing a documentation header. 359 | dotnet_diagnostic.SA1600.severity = none 360 | 361 | # SA1601: A C# partial element is missing a documentation header. 362 | dotnet_diagnostic.SA1601.severity = none 363 | 364 | # SA1602: An item within a C# enumeration is missing an Xml documentation header. 365 | dotnet_diagnostic.SA1602.severity = none 366 | 367 | # SA1611: A C# method, constructor, delegate or indexer element is missing documentation for one or more of its parameters. 368 | dotnet_diagnostic.SA1611.severity = none 369 | 370 | # SA1614: A tag within a C# element's documentation header is empty. 371 | dotnet_diagnostic.SA1614.severity = none 372 | 373 | # SA1615: A C# element is missing documentation for its return value. 374 | dotnet_diagnostic.SA1615.severity = none 375 | 376 | # SA1623: Property summary documentation should match accessors 377 | dotnet_diagnostic.SA1623.severity = none 378 | 379 | # SA1629: A section of the Xml header documentation for a C# element does not end with a period 380 | dotnet_diagnostic.SA1629.severity = none 381 | 382 | # SA1633: A C# code file is missing a standard file header. 383 | dotnet_diagnostic.SA1633.severity = none 384 | 385 | # SA1642: The XML documentation header for a C# constructor does not contain the appropriate summary text. 386 | dotnet_diagnostic.SA1642.severity = none 387 | 388 | # SA1649: The file name of a C# code file does not match the first type declared in the file. 389 | dotnet_diagnostic.SA1649.severity = none 390 | 391 | 392 | # Microsoft.VisualStudio.Threading.Analyzers 393 | 394 | # VSTHRD200: Use Async suffix for async methods 395 | dotnet_diagnostic.VSTHRD200.severity = none 396 | 397 | 398 | # xUnit 399 | 400 | # xUnit1013: This rule is trigger by having a public method in a test class that is not marked as a test. 401 | dotnet_diagnostic.xUnit1013.severity = suggestion 402 | 403 | 404 | # AsyncFixer 405 | 406 | # AsyncFixer01: Unnecessary async/await usage 407 | dotnet_diagnostic.AsyncFixer01.severity = suggestion 408 | 409 | 410 | # Meziantou.Analyzer 411 | 412 | # MA0002: IEqualityComparer or IComparer is missing 413 | dotnet_diagnostic.MA0002.severity = none 414 | 415 | # MA0004: Use Task.ConfigureAwait(false) 416 | dotnet_diagnostic.MA0004.severity = none 417 | 418 | # MA0006: Use String.Equals instead of equality operator 419 | dotnet_diagnostic.MA0006.severity = none 420 | 421 | # MA0011: IFormatProvider is missing 422 | dotnet_diagnostic.MA0011.severity = suggestion 423 | 424 | # MA0016: Prefer return collection abstraction instead of implementation 425 | dotnet_diagnostic.MA0016.severity = suggestion 426 | 427 | # MA0026: Fix TODO comment 428 | dotnet_diagnostic.MA0026.severity = suggestion 429 | 430 | # MA0048: File name must match type name 431 | dotnet_diagnostic.MA0048.severity = none 432 | 433 | # MA0051: Method is too long 434 | dotnet_diagnostic.MA0051.severity = suggestion 435 | MA0051.maximum_lines_per_method = 60 436 | MA0051.maximum_statements_per_method = 50 437 | 438 | # MA0056: Do not call overridable members in constructor 439 | dotnet_diagnostic.MA0056.severity = suggestion 440 | 441 | # MA0074: Avoid implicit culture-sensitive methods 442 | dotnet_diagnostic.MA0074.severity = suggestion 443 | -------------------------------------------------------------------------------- /.github/workflows/Allegro.Prometheus.TrueRpsMetric.ci.yml: -------------------------------------------------------------------------------- 1 | name: Build Allegro.Prometheus.TrueRpsMetric 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | paths: 7 | - 'src/Allegro.Prometheus.TrueRpsMetric/**' 8 | - 'src/*' 9 | pull_request: 10 | branches: [ main ] 11 | paths: 12 | - 'src/Allegro.Prometheus.TrueRpsMetric/**' 13 | - 'src/*' 14 | 15 | jobs: 16 | ci: 17 | uses: ./.github/workflows/template.yml 18 | with: 19 | projectName: Allegro.Prometheus.TrueRpsMetric 20 | -------------------------------------------------------------------------------- /.github/workflows/Allegro.Prometheus.TrueRpsMetric.publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Allegro.Prometheus.TrueRpsMetric 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'Allegro.Prometheus.TrueRpsMetric_*' 7 | 8 | jobs: 9 | publish: 10 | uses: ./.github/workflows/template.yml 11 | with: 12 | projectName: Allegro.Prometheus.TrueRpsMetric 13 | publish: true 14 | tagName: ${{ github.ref_name }} 15 | secrets: 16 | SM_CLIENT_CERT_PASSWORD: ${{ secrets.DIGICERT_SM_CLIENT_CERT_PASSWORD }} 17 | SM_CLIENT_CERT_FILE_B64: ${{ secrets.DIGICERT_SM_CLIENT_CERT_FILE_B64 }} 18 | SM_HOST: ${{ secrets.DIGICERT_SM_HOST }} 19 | SM_API_KEY: ${{ secrets.DIGICERT_SM_API_KEY }} 20 | SM_CODE_SIGNING_CERT_SHA1_HASH: ${{ secrets.DIGICERT_SM_CODE_SIGNING_CERT_SHA1_HASH }} 21 | nugetApiKey: ${{ secrets.NUGET_API_KEY }} 22 | -------------------------------------------------------------------------------- /.github/workflows/template.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_call: 3 | inputs: 4 | publish: 5 | required: false 6 | type: boolean 7 | default: false 8 | projectName: 9 | required: true 10 | type: string 11 | tagName: 12 | required: false 13 | type: string 14 | secrets: 15 | nugetCertificate: 16 | required: false 17 | nugetCertificatePassword: 18 | required: false 19 | nugetApiKey: 20 | required: false 21 | jobs: 22 | build: 23 | runs-on: ubuntu-latest 24 | defaults: 25 | run: 26 | working-directory: src/${{ inputs.projectName }} 27 | outputs: 28 | nupkgFilename: ${{ steps.nupkg.outputs.filename }} 29 | steps: 30 | - uses: actions/checkout@v2 31 | - name: Setup .NET 32 | uses: actions/setup-dotnet@v1 33 | with: 34 | dotnet-version: | 35 | 5.0.x 36 | 6.0.x 37 | - name: Restore dependencies 38 | run: dotnet restore 39 | - name: Build 40 | run: dotnet build -c Release --no-restore 41 | - name: Test 42 | run: dotnet test -c Release --no-build --verbosity normal 43 | - name: Pack 44 | run: dotnet pack -v normal -c Release --no-build --include-symbols --include-source -o package 45 | - name: Verify package name & version 46 | id: nupkg 47 | if: inputs.publish 48 | run: | 49 | TAG_NAME=${{ inputs.tagName }} 50 | VERSION=${TAG_NAME##*_} 51 | NUPKG="${{ inputs.projectName }}.${VERSION}.nupkg" 52 | echo "::set-output name=filename::$NUPKG" 53 | if [ ! -f package/$NUPKG ]; then 54 | echo "File '$NUPKG' does not exist"; 55 | exit 1; 56 | fi 57 | - uses: actions/upload-artifact@v3 58 | if: inputs.publish 59 | with: 60 | name: package 61 | path: src/${{ inputs.projectName }}/package 62 | publish: 63 | runs-on: windows-latest 64 | if: inputs.publish 65 | needs: [build] 66 | steps: 67 | - uses: actions/download-artifact@v3 68 | with: 69 | name: package 70 | path: unsigned 71 | - name: NuGet Install 72 | uses: NuGet/setup-nuget@v1.0.5 73 | with: 74 | nuget-version: latest 75 | - name: Setup Certificate 76 | run: | 77 | echo "${{ secrets.SM_CLIENT_CERT_FILE_B64 }}" | base64 --decode > /d/cognite_code_signing_github_actions.p12 78 | shell: bash 79 | - name: Set variables 80 | id: variables 81 | run: | 82 | echo "SM_HOST=${{ secrets.SM_HOST }}" >> "$GITHUB_ENV" 83 | echo "SM_API_KEY=${{ secrets.SM_API_KEY }}" >> "$GITHUB_ENV" 84 | echo "SM_CLIENT_CERT_PASSWORD=${{ secrets.SM_CLIENT_CERT_PASSWORD }}" >> "$GITHUB_ENV" 85 | echo "SM_CODE_SIGNING_CERT_SHA1_HASH=${{ secrets.SM_CODE_SIGNING_CERT_SHA1_HASH }}" >> "$GITHUB_ENV" 86 | echo "SM_CLIENT_CERT_FILE=D:\\cognite_code_signing_github_actions.p12" >> "$GITHUB_ENV" 87 | shell: bash 88 | - name: Configure Digicert Secure Software Manager 89 | uses: digicert/ssm-code-signing@v0.0.2 90 | env: 91 | SM_API_KEY: ${{ env.SM_API_KEY }} 92 | SM_CLIENT_CERT_PASSWORD: ${{ env.SM_CLIENT_CERT_PASSWORD }} 93 | SM_CLIENT_CERT_FILE: ${{ env.SM_CLIENT_CERT_FILE }} 94 | - name: Setup SSM KSP on windows latest 95 | run: | 96 | smksp_registrar.exe list 97 | smctl.exe keypair ls 98 | C:\Windows\System32\certutil.exe -csp "DigiCert Signing Manager KSP" -key -user 99 | smksp_cert_sync.exe 100 | smctl.exe healthcheck 101 | shell: cmd 102 | - name: Signing using Nuget 103 | run: | 104 | dir "%cd%\unsigned" 105 | mkdir "%cd%\signed" 106 | nuget sign "%cd%\unsigned\*.nupkg" -Timestamper http://timestamp.digicert.com -outputdirectory "%cd%\signed" -CertificateFingerprint ${{ secrets.SM_CODE_SIGNING_CERT_SHA1_HASH }} -HashAlgorithm SHA256 -Verbosity detailed -Overwrite 107 | nuget verify -All "%cd%\signed\*.nupkg" 108 | shell: cmd 109 | - name: Push package 110 | run: dotnet nuget push signed/${{needs.build.outputs.nupkgFilename}} --api-key ${{ secrets.nugetApiKey }} --source https://api.nuget.org/v3/index.json 111 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | 352 | # IDEA 353 | .idea/ 354 | 355 | # SonarQube 356 | .sonarqube/ -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Pull requests 4 | 5 | * for small changes, no need to add separate issue, defining problem in pull request is enough 6 | * if issue exists, reference it from PR title or description using GitHub magic words like *resolves #issue-number* 7 | * before making significant changes, please contact us via issues to discuss your plans and decrease number of changes after Pull Request is created 8 | * create pull requests to **main** branch 9 | * when updating a package, make sure to: 10 | * update its README.md 11 | * update its CHANGELOG.md 12 | * update its version 13 | * consider presenting usage in Demo app (`/samples`) 14 | * add tests 15 | * when creating a new package, make sure to: 16 | * maintain the repo structure (see existing packages) 17 | * add code analyzers 18 | * create the package's README.md 19 | * initialize the package's CHANGELOG.md 20 | * add tests 21 | * consider presenting usage in Demo app (`/samples`) 22 | * reference the package from README.md in repo's root 23 | 24 | ## Coding style 25 | 26 | The coding style is guarded by the analyzers (such as stylecop) and .editorconfig. 27 | Make sure to follow the defined standards. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Allegro Prometheus metrics 2 | 3 | ## About 4 | 5 | This repo contains a collection of useful metrics to be used with the [prometheus-net](https://github.com/prometheus-net/prometheus-net) library. Developed as part of [Allegro Pay](https://allegropay.pl/) product. 6 | 7 | ## Metrics 8 | 9 | - [Allegro.Prometheus.TrueRpsMetric](src/Allegro.Prometheus.TrueRpsMetric/README.md) - the "true RPS" aka "max RPS in last scrapping period" metric 10 | [![NuGet](https://img.shields.io/nuget/v/Allegro.Prometheus.TrueRpsMetric.svg)](https://nuget.org/packages/Allegro.Prometheus.TrueRpsMetric) [![Build Status](https://github.com/allegro/prometheus-net-metrics/actions/workflows/Allegro.Prometheus.TrueRpsMetric.ci.yml/badge.svg?branch=main)](https://github.com/allegro/prometheus-net-metrics/actions/workflows/Allegro.Prometheus.TrueRpsMetric.ci.yml?query=branch%3Amain) 11 | 12 | ## License 13 | 14 | Copyright 2022 Allegro Group 15 | 16 | Licensed under the Apache License, Version 2.0 (the "License"); 17 | you may not use this file except in compliance with the License. 18 | You may obtain a copy of the License at 19 | 20 | http://www.apache.org/licenses/LICENSE-2.0 21 | 22 | Unless required by applicable law or agreed to in writing, software 23 | distributed under the License is distributed on an "AS IS" BASIS, 24 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 25 | See the License for the specific language governing permissions and 26 | limitations under the License. 27 | -------------------------------------------------------------------------------- /ap-catalog-info.yaml: -------------------------------------------------------------------------------- 1 | # https://backstage.io/docs/features/software-catalog/descriptor-format#kind-component 2 | apiVersion: backstage.io/v1alpha1 3 | kind: Component 4 | metadata: 5 | name: prometheus-net-metrics 6 | description: "This repo contains a collection of useful metrics to be used with the prometheus-net library. Developed as part of Allegro Pay product." 7 | title: Allegro Prometheus metrics 8 | annotations: 9 | backstage.io/techdocs-ref: dir:. 10 | allegropay/entity-updated-at: '2023-11-03T08:15' 11 | github.com/project-slug: allegro/prometheus-net-metrics 12 | links: [] 13 | spec: 14 | type: library 15 | owner: aard 16 | lifecycle: production 17 | system: application-platform 18 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | {% 2 | include-markdown "../README.md" 3 | start="" 4 | end="" 5 | rewrite-relative-urls=true 6 | %} -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: prometheus-net-metrics docs 2 | 3 | plugins: 4 | - techdocs-core 5 | - include-markdown 6 | -------------------------------------------------------------------------------- /samples/Allegro.Prometheus.Demo/Allegro.Prometheus.Demo.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /samples/Allegro.Prometheus.Demo/Allegro.Prometheus.Demo.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Allegro.Prometheus.Demo", "Allegro.Prometheus.Demo.csproj", "{17B1DE96-77F1-4198-B1C4-6778D06D3667}" 4 | EndProject 5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Allegro.Prometheus.TrueRpsMetric", "..\..\src\Allegro.Prometheus.TrueRpsMetric\Allegro.Prometheus.TrueRpsMetric\Allegro.Prometheus.TrueRpsMetric.csproj", "{5A850BDA-EA0A-400F-A03F-6D4E65FC6BEB}" 6 | EndProject 7 | Global 8 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 9 | Debug|Any CPU = Debug|Any CPU 10 | Release|Any CPU = Release|Any CPU 11 | EndGlobalSection 12 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 13 | {17B1DE96-77F1-4198-B1C4-6778D06D3667}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 14 | {17B1DE96-77F1-4198-B1C4-6778D06D3667}.Debug|Any CPU.Build.0 = Debug|Any CPU 15 | {17B1DE96-77F1-4198-B1C4-6778D06D3667}.Release|Any CPU.ActiveCfg = Release|Any CPU 16 | {17B1DE96-77F1-4198-B1C4-6778D06D3667}.Release|Any CPU.Build.0 = Release|Any CPU 17 | {5A850BDA-EA0A-400F-A03F-6D4E65FC6BEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 18 | {5A850BDA-EA0A-400F-A03F-6D4E65FC6BEB}.Debug|Any CPU.Build.0 = Debug|Any CPU 19 | {5A850BDA-EA0A-400F-A03F-6D4E65FC6BEB}.Release|Any CPU.ActiveCfg = Release|Any CPU 20 | {5A850BDA-EA0A-400F-A03F-6D4E65FC6BEB}.Release|Any CPU.Build.0 = Release|Any CPU 21 | EndGlobalSection 22 | EndGlobal 23 | -------------------------------------------------------------------------------- /samples/Allegro.Prometheus.Demo/Controllers/DemoController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace Allegro.Prometheus.TrueRpsMetric.Demo.Controllers; 4 | 5 | [Route("demo")] 6 | public class DemoController : ControllerBase 7 | { 8 | [HttpGet] 9 | public ActionResult Demo() 10 | { 11 | return Ok(); 12 | } 13 | } -------------------------------------------------------------------------------- /samples/Allegro.Prometheus.Demo/Program.cs: -------------------------------------------------------------------------------- 1 | using Allegro.Prometheus.TrueRpsMetric; 2 | using Prometheus; 3 | 4 | var builder = WebApplication.CreateBuilder(args); 5 | builder.Services.AddControllers(); 6 | 7 | var app = builder.Build(); 8 | app.UseRouting(); 9 | app.UseMiddleware(); // here the "true RPS metric" is being added 10 | app.UseHttpMetrics(); 11 | app.UseEndpoints(endpoints => 12 | { 13 | endpoints.MapControllers(); 14 | endpoints.MapMetrics(); 15 | }); 16 | app.Run(); -------------------------------------------------------------------------------- /samples/Allegro.Prometheus.Demo/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:53029", 7 | "sslPort": 44389 8 | } 9 | }, 10 | "profiles": { 11 | "Allegro.Prometheus.TrueRpsMetric.Demo": { 12 | "commandName": "Project", 13 | "dotnetRunMessages": true, 14 | "launchBrowser": true, 15 | "applicationUrl": "https://localhost:7274;http://localhost:5246", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "IIS Express": { 21 | "commandName": "IISExpress", 22 | "launchBrowser": true, 23 | "environmentVariables": { 24 | "ASPNETCORE_ENVIRONMENT": "Development" 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Allegro.Prometheus.TrueRpsMetric/Allegro.Prometheus.TrueRpsMetric.Tests/Allegro.Prometheus.TrueRpsMetric.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | all 17 | 18 | 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | all 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/Allegro.Prometheus.TrueRpsMetric/Allegro.Prometheus.TrueRpsMetric.Tests/MaxPerSecondGaugeTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using Xunit; 3 | 4 | namespace Allegro.Prometheus.TrueRpsMetric.Tests; 5 | 6 | /// 7 | /// These are low-level tests of MaxPerSecondGauge's internal state that uses cyclic buffer to track metric value 8 | /// over last second in 10x100ms buckets. 9 | /// 10 | public class MaxPerSecondGaugeTests 11 | { 12 | [Fact] 13 | public void ShouldTrackMaxPerSecondValueInBuckets() 14 | { 15 | // arrange 16 | var sut = new MaxPerSecondGauge.MaxPerSecondGaugeGaugeCollector.MaxPerSecondState( 17 | new DateTime(2022, 05, 08, 19, 01, 30, 000)); 18 | 19 | // act 20 | sut.Observe(new DateTime(2022, 05, 08, 19, 01, 30, 000), 3); 21 | sut.Observe(new DateTime(2022, 05, 08, 19, 01, 30, 100), 2); 22 | sut.Observe(new DateTime(2022, 05, 08, 19, 01, 30, 200), 1); 23 | sut.Observe(new DateTime(2022, 05, 08, 19, 01, 30, 500), 5); 24 | sut.Observe(new DateTime(2022, 05, 08, 19, 01, 30, 900), 2.5); 25 | 26 | // assert 27 | sut.MaxPerSecondSoFar.Should().Be(13.5); 28 | sut.GetBucketsOrdered().Should().BeEquivalentTo(new[] { 3, 2, 1, 0, 0, 5, 0, 0, 0, 2.5 }); 29 | } 30 | 31 | [Theory] 32 | [InlineData(1)] 33 | [InlineData(1.5)] 34 | [InlineData(10)] 35 | [InlineData(100)] 36 | [InlineData(1000)] 37 | public void ShouldResetCyclicBufferAfterPeriodLongerThanOneSecond(double seconds) 38 | { 39 | // arrange 40 | var sut = new MaxPerSecondGauge.MaxPerSecondGaugeGaugeCollector.MaxPerSecondState( 41 | new DateTime(2022, 05, 08, 19, 01, 30, 000)); 42 | sut.Observe(new DateTime(2022, 05, 08, 19, 01, 30, 000), 3); 43 | sut.Observe(new DateTime(2022, 05, 08, 19, 01, 30, 100), 2); 44 | sut.Observe(new DateTime(2022, 05, 08, 19, 01, 30, 200), 1); 45 | sut.Observe(new DateTime(2022, 05, 08, 19, 01, 30, 500), 5); 46 | sut.Observe(new DateTime(2022, 05, 08, 19, 01, 30, 900), 2.5); 47 | sut.Observe(new DateTime(2022, 05, 08, 19, 01, 31, 150), 5); 48 | 49 | // act 50 | sut.Observe(new DateTime(2022, 05, 08, 19, 01, 31, 150).AddSeconds(seconds), 10); 51 | 52 | // assert 53 | sut.GetBucketsOrdered().Should().BeEquivalentTo(new[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 10 }); 54 | } 55 | } -------------------------------------------------------------------------------- /src/Allegro.Prometheus.TrueRpsMetric/Allegro.Prometheus.TrueRpsMetric.Tests/TrueRpsMetricFactoryTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using Microsoft.Extensions.Options; 3 | using Xunit; 4 | 5 | namespace Allegro.Prometheus.TrueRpsMetric.Tests; 6 | 7 | public class TrueRpsMetricFactoryTests 8 | { 9 | [Fact] 10 | public void ShouldCreateExpectedMetric() 11 | { 12 | // Act 13 | var sut = TrueRpsMetricFactory.Create(Options.Create(new TrueRpsMetricConfiguration())); 14 | 15 | // Assert 16 | sut.Should().NotBeNull(); 17 | sut!.Name 18 | .Should().Be("http_request_max_rps"); 19 | sut!.LabelNames 20 | .Should().BeEquivalentTo( 21 | "controller", 22 | "action", 23 | "method"); 24 | } 25 | 26 | [Fact] 27 | public void ShouldReturnNullWhenDisabled() 28 | { 29 | // Act 30 | var sut = TrueRpsMetricFactory.Create(Options.Create(new TrueRpsMetricConfiguration 31 | { 32 | Enabled = false 33 | })); 34 | 35 | // Assert 36 | sut.Should().BeNull(); 37 | } 38 | } -------------------------------------------------------------------------------- /src/Allegro.Prometheus.TrueRpsMetric/Allegro.Prometheus.TrueRpsMetric.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Allegro.Prometheus.TrueRpsMetric", "Allegro.Prometheus.TrueRpsMetric\Allegro.Prometheus.TrueRpsMetric.csproj", "{287692CA-9317-416E-93F3-73C37739928B}" 4 | EndProject 5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Allegro.Prometheus.TrueRpsMetric.Tests", "Allegro.Prometheus.TrueRpsMetric.Tests\Allegro.Prometheus.TrueRpsMetric.Tests.csproj", "{A3C5DEC7-8943-4C66-8FD6-5FE74F39383D}" 6 | EndProject 7 | Global 8 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 9 | Debug|Any CPU = Debug|Any CPU 10 | Release|Any CPU = Release|Any CPU 11 | EndGlobalSection 12 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 13 | {287692CA-9317-416E-93F3-73C37739928B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 14 | {287692CA-9317-416E-93F3-73C37739928B}.Debug|Any CPU.Build.0 = Debug|Any CPU 15 | {287692CA-9317-416E-93F3-73C37739928B}.Release|Any CPU.ActiveCfg = Release|Any CPU 16 | {287692CA-9317-416E-93F3-73C37739928B}.Release|Any CPU.Build.0 = Release|Any CPU 17 | {A3C5DEC7-8943-4C66-8FD6-5FE74F39383D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 18 | {A3C5DEC7-8943-4C66-8FD6-5FE74F39383D}.Debug|Any CPU.Build.0 = Debug|Any CPU 19 | {A3C5DEC7-8943-4C66-8FD6-5FE74F39383D}.Release|Any CPU.ActiveCfg = Release|Any CPU 20 | {A3C5DEC7-8943-4C66-8FD6-5FE74F39383D}.Release|Any CPU.Build.0 = Release|Any CPU 21 | EndGlobalSection 22 | EndGlobal 23 | -------------------------------------------------------------------------------- /src/Allegro.Prometheus.TrueRpsMetric/Allegro.Prometheus.TrueRpsMetric/Allegro.Prometheus.TrueRpsMetric.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Allegro.Prometheus.TrueRpsMetric 5 | The "true RPS" aka "max RPS in last scrapping period" metric, based on prometheus-net library. 6 | 1.0.0 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Allegro.Prometheus.TrueRpsMetric/Allegro.Prometheus.TrueRpsMetric/MaxPerSecondGauge.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using Prometheus; 3 | 4 | namespace Allegro.Prometheus.TrueRpsMetric; 5 | 6 | /// 7 | /// Wrapper on that collects max value observed over 1-second periods. 8 | /// The value of the metric is reset on every metric collection. 9 | /// aggregates a collection of 10 | /// per each labels set. 11 | /// 12 | public class MaxPerSecondGauge : ICollector 13 | { 14 | private readonly ICollectorRegistry _registry; 15 | private readonly TimeSpan _minPublishInterval; 16 | private readonly ConcurrentDictionary _collectors = new(); 17 | private readonly Gauge _gauge; 18 | 19 | /// 20 | /// Creates new . 21 | /// 22 | /// 23 | /// The metric that will be used to collect max per second value. 24 | /// 25 | /// 26 | /// The that will be used for the metric 27 | /// 28 | /// 29 | /// Minimal interval for publishing metric 30 | /// 31 | public MaxPerSecondGauge( 32 | Gauge gauge, 33 | ICollectorRegistry registry, 34 | TimeSpan minPublishInterval) 35 | { 36 | _gauge = gauge; 37 | _registry = registry; 38 | _minPublishInterval = minPublishInterval; 39 | } 40 | 41 | public string Name => _gauge.Name; 42 | 43 | public string Help => _gauge.Help; 44 | 45 | public string[] LabelNames => _gauge.LabelNames; 46 | 47 | /// 48 | /// Gets or creates the metric collector for given labels set. 49 | /// 50 | /// Labels set 51 | /// The collector 52 | public MaxPerSecondGaugeGaugeCollector WithLabels(params string[] labelValues) 53 | { 54 | var key = string.Join(",", labelValues); 55 | return _collectors.GetOrAdd( 56 | key, 57 | _ => new MaxPerSecondGaugeGaugeCollector( 58 | _gauge, 59 | _registry, 60 | _minPublishInterval, 61 | labelValues)); 62 | } 63 | 64 | /// 65 | /// Wrapper on that collects max value observed over 1-second periods for specific labels set. 66 | /// The value of the metric is reset on every metric collection. 67 | /// 68 | public class MaxPerSecondGaugeGaugeCollector 69 | { 70 | private readonly Gauge _gauge; 71 | private readonly string[] _labelValues; 72 | private readonly TimeSpan _minPublishInterval; 73 | private readonly object _monitor = new(); 74 | 75 | private MaxPerSecondState _state = new(DateTime.UtcNow); 76 | 77 | internal MaxPerSecondGaugeGaugeCollector( 78 | Gauge gauge, 79 | ICollectorRegistry collectorRegistry, 80 | TimeSpan minPublishInterval, 81 | string[] labelValues) 82 | { 83 | _gauge = gauge; 84 | _labelValues = labelValues; 85 | _minPublishInterval = minPublishInterval; 86 | 87 | collectorRegistry.AddBeforeCollectCallback(BeforeCollectCallback); 88 | } 89 | 90 | /// 91 | /// Observes the metric value. 92 | /// 93 | /// Observed value 94 | public void Observe(double val = 1D) 95 | { 96 | lock (_monitor) 97 | { 98 | _state.Observe(DateTime.UtcNow, val); 99 | } 100 | } 101 | 102 | private Task BeforeCollectCallback(CancellationToken cancellationToken) 103 | { 104 | double toPublish; 105 | 106 | lock (_monitor) 107 | { 108 | if (_state.TrackingFor < _minPublishInterval) 109 | { 110 | return Task.CompletedTask; 111 | } 112 | 113 | toPublish = _state.MaxPerSecondSoFar; 114 | _state = new MaxPerSecondState(DateTime.UtcNow); 115 | } 116 | 117 | _gauge.WithLabels(_labelValues).Set(toPublish); 118 | 119 | return Task.CompletedTask; 120 | } 121 | 122 | /// 123 | /// Holds the current state of the max-per-second metric. Tracks the metric value in 100ms periods, using 124 | /// cyclic buffer. The buffer is shifted (loosing oldest values) as the new data-points arrive. 125 | /// 126 | internal class MaxPerSecondState 127 | { 128 | private const int TrackingBucketsCount = 10; 129 | 130 | private double[] _trackingBuckets = new double[TrackingBucketsCount]; 131 | private DateTime _trackingEndDate; 132 | private ModuloInteger _trackingIndex; 133 | 134 | public MaxPerSecondState(DateTime trackingSince) 135 | { 136 | TrackingSince = trackingSince; 137 | _trackingEndDate = TrackingSince; 138 | } 139 | 140 | /// 141 | /// When did the max-per-second tracking begin. 142 | /// 143 | public DateTime TrackingSince { get; } 144 | 145 | /// 146 | /// How long is the max-per-second being tracked. 147 | /// 148 | public TimeSpan TrackingFor => DateTime.UtcNow - TrackingSince; 149 | 150 | /// 151 | /// Max per-second value observed so far since tracking begin. 152 | /// 153 | public double MaxPerSecondSoFar { get; private set; } 154 | 155 | /// 156 | /// Observes a value in given date. Shifts the cyclic buffer and increments the last bucket. 157 | /// 158 | /// The date the value was observed 159 | /// The observed value 160 | public void Observe(DateTime date, double val) 161 | { 162 | var shift = (date - _trackingEndDate).TotalSeconds < 1 163 | ? (int)((date - _trackingEndDate).TotalSeconds / (1.0 / TrackingBucketsCount)) 164 | : TrackingBucketsCount; 165 | 166 | if (shift >= TrackingBucketsCount) 167 | { 168 | _trackingBuckets = new double[TrackingBucketsCount]; 169 | _trackingEndDate = date; 170 | } 171 | else 172 | { 173 | for (var i = 0; i < shift; i++) 174 | { 175 | _trackingBuckets[_trackingIndex] = 0; 176 | _trackingIndex++; 177 | } 178 | 179 | _trackingEndDate = _trackingEndDate.AddSeconds((double)shift / TrackingBucketsCount); 180 | } 181 | 182 | _trackingBuckets[_trackingIndex + TrackingBucketsCount - 1] += val; 183 | 184 | var currentMaxPerSecond = _trackingBuckets.Sum(); 185 | if (currentMaxPerSecond > MaxPerSecondSoFar) 186 | { 187 | MaxPerSecondSoFar = currentMaxPerSecond; 188 | } 189 | } 190 | 191 | /// 192 | /// Gets the current 100ms buckets ordered from oldest to current. 193 | /// 194 | internal IEnumerable GetBucketsOrdered() 195 | { 196 | for (var i = 0; i < TrackingBucketsCount; i++) 197 | { 198 | yield return _trackingBuckets[_trackingIndex + i]; 199 | } 200 | } 201 | 202 | /// 203 | /// Simple int wrapper that limits its values to 0 (inclusive) - (exclusive). 204 | /// In mathematics it would be called a ring 'Zn', where n equals . 205 | /// 206 | internal struct ModuloInteger 207 | { 208 | private int _value; 209 | 210 | private int Value 211 | { 212 | get => _value; 213 | set 214 | { 215 | _value = value % TrackingBucketsCount; 216 | while (_value < 0) 217 | { 218 | _value = TrackingBucketsCount + _value; 219 | } 220 | } 221 | } 222 | 223 | public override string ToString() 224 | { 225 | #pragma warning disable MA0011 226 | return _value.ToString(); 227 | #pragma warning restore MA0011 228 | } 229 | 230 | public static implicit operator int(ModuloInteger val) => val._value; 231 | 232 | public static ModuloInteger operator +(ModuloInteger val, int add) => 233 | val with { Value = val.Value + add }; 234 | 235 | public static ModuloInteger operator -(ModuloInteger val, int sub) => val + -sub; 236 | 237 | public static ModuloInteger operator ++(ModuloInteger val) => val + 1; 238 | } 239 | } 240 | } 241 | } -------------------------------------------------------------------------------- /src/Allegro.Prometheus.TrueRpsMetric/Allegro.Prometheus.TrueRpsMetric/TrueRpsMetricConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Prometheus; 2 | 3 | namespace Allegro.Prometheus.TrueRpsMetric; 4 | 5 | /// 6 | /// Configuration used by . 7 | /// 8 | public class TrueRpsMetricConfiguration 9 | { 10 | /// 11 | /// Should collect the true RPS metric? 12 | /// 13 | public bool Enabled { get; set; } = true; 14 | 15 | /// 16 | /// Collector registry to be used for true RPS metric 17 | /// 18 | public CollectorRegistry Registry { get; set; } = Metrics.DefaultRegistry; 19 | 20 | /// 21 | /// Minimal interval for publishing metric 22 | /// 23 | public TimeSpan MinPublishInterval { get; set; } = TimeSpan.FromSeconds(59); 24 | } -------------------------------------------------------------------------------- /src/Allegro.Prometheus.TrueRpsMetric/Allegro.Prometheus.TrueRpsMetric/TrueRpsMetricFactory.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Options; 2 | using Prometheus; 3 | 4 | namespace Allegro.Prometheus.TrueRpsMetric; 5 | 6 | /// 7 | /// Factory for 8 | /// 9 | public static class TrueRpsMetricFactory 10 | { 11 | /// 12 | /// Creates the using given configuration. 13 | /// 14 | /// Configuration of the metric 15 | /// Created gauge or null, if is not set to true 16 | public static MaxPerSecondGauge? Create(IOptions configuration) 17 | { 18 | if (!configuration.Value.Enabled) 19 | { 20 | return null; 21 | } 22 | 23 | return new MaxPerSecondGauge( 24 | Metrics.WithCustomRegistry(configuration.Value.Registry) 25 | .CreateGauge( 26 | "http_request_max_rps", 27 | "The max RPS over last scrapping period.", 28 | "controller", 29 | "action", 30 | "method"), 31 | configuration.Value.Registry, 32 | configuration.Value.MinPublishInterval); 33 | } 34 | } -------------------------------------------------------------------------------- /src/Allegro.Prometheus.TrueRpsMetric/Allegro.Prometheus.TrueRpsMetric/TrueRpsMiddleware.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Microsoft.AspNetCore.Routing; 3 | using Microsoft.Extensions.Options; 4 | 5 | namespace Allegro.Prometheus.TrueRpsMetric; 6 | 7 | /// 8 | /// Middleware that collects the max RPS per second ("true RPS") per endpoint 9 | /// over last minute (prometheus collection interval). 10 | /// 11 | public class TrueRpsMiddleware 12 | { 13 | private readonly RequestDelegate _next; 14 | private readonly MaxPerSecondGauge? _trueRpsMetric; 15 | 16 | public TrueRpsMiddleware( 17 | RequestDelegate next, 18 | IOptions configuration) 19 | { 20 | _next = next; 21 | _trueRpsMetric = TrueRpsMetricFactory.Create(configuration); 22 | } 23 | 24 | /// 25 | /// Middleware invocation 26 | /// 27 | public async Task Invoke(HttpContext context) 28 | { 29 | if (_trueRpsMetric == null) 30 | { 31 | await _next(context); 32 | return; 33 | } 34 | 35 | var actionName = context.GetRouteValue("action")?.ToString(); 36 | var controllerName = context.GetRouteValue("controller")?.ToString(); 37 | 38 | if (!string.IsNullOrEmpty(actionName) && !string.IsNullOrEmpty(controllerName)) 39 | { 40 | _trueRpsMetric 41 | .WithLabels(controllerName, actionName, context.Request.Method) 42 | .Observe(); 43 | } 44 | 45 | await _next(context); 46 | } 47 | } -------------------------------------------------------------------------------- /src/Allegro.Prometheus.TrueRpsMetric/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres 6 | to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [1.0.0] - 2022-10-14 9 | 10 | ### Added 11 | 12 | * Initiated AllegroPay.Prometheus.TrueRpsMetric project 13 | -------------------------------------------------------------------------------- /src/Allegro.Prometheus.TrueRpsMetric/README.md: -------------------------------------------------------------------------------- 1 | # Allegro.Prometheus.TrueRpsMetric 2 | 3 | This library contains the "true RPS" aka "max RPS in last scrapping period" metric, based on [prometheus-net](https://github.com/prometheus-net/prometheus-net) library. 4 | 5 | ## What is the "true RPS" metric 6 | 7 | The [prometheus-net](https://github.com/prometheus-net/prometheus-net) library provides some useful metrics for monitoring the HTTP traffic 8 | of your app, such as `http_request_duration_seconds` histogram or the `http_requests_in_progress` gauge. However, when monitoring a high 9 | throughput system it is sometimes hard to notice very short peaks in HTTP calls - let's say originating from some marketing campaign. 10 | The typical scrapping interval for Prometheus is 1 minute, but even shorter intervals might be too long to catch a peak that lasted for 11 | a few seconds. 12 | 13 | This is where the idea of the "true RPS" metric came from. At [Allegro Pay](https://allegropay.pl/), we wanted to be aware that our services 14 | received short, but high peaks of incoming calls. The "true RPS" metric can be also described as "max RPS over scrapping period". Every time 15 | prometheus scrapes the metrics endpoint, the `http_request_max_rps` gives the max RPS that was observed since the previous scrape. 16 | 17 | To avoid the collected metrics to be disturbed by developers or administrators manually calling the metrics endpoint, there is 18 | a `MinPublishInterval` configuration property available. If the interval between two scrapes is shorter than the `MinPublishInterval`, 19 | the counter will not be reset. This should be set to an interval just a bit shorter than prometheus scrapping interval. 20 | 21 | ## How to... 22 | 23 | ### Use the metric 24 | 25 | Just add the `TrueRpsMiddleware` somewhere between the `UseRouting()` and `UseEndpoints()` invocations. 26 | 27 | ```csharp 28 | app.UseRouting(); 29 | (...) 30 | app.UseMiddleware(); 31 | (...) 32 | app.UseHttpMetrics(); 33 | app.UseEndpoints(endpoints => 34 | { 35 | endpoints.MapControllers(); 36 | endpoints.MapMetrics(); 37 | }); 38 | ``` 39 | 40 | ### Use custom collector registry 41 | 42 | ```csharp 43 | services.Configure(opt => opt.Registry = myRegistry); 44 | ``` 45 | 46 | ### Run the Demo 47 | 48 | You can test the metric using the provided Demo app. It is located under `/samples/Allegro.Prometheus.Demo`. Just run the project and call following endpoints: 49 | 50 | ``` 51 | GET https://localhost:7274/demo 52 | GET https://localhost:7274/metrics 53 | ``` 54 | 55 | You should be able to see the `http_request_max_rps` metric. -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | latest 5 | latest 6 | enable 7 | enable 8 | true 9 | true 10 | 11 | 12 | $(MSBuildThisFileDirectory)obj/$(MSBuildProjectName)/ 13 | $(MSBuildThisFileDirectory)bin/$(MSBuildProjectName)/ 14 | 15 | 16 | 17 | true 18 | true 19 | 20 | true 21 | 22 | 23 | $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/Package.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | Allegro 6 | Allegro 7 | Copyright 2022 Allegro Group 8 | Apache-2.0 9 | https://github.com/allegro/prometheus-net-metrics 10 | README.md 11 | true 12 | snupkg 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | all 23 | runtime; build; native; contentfiles; analyzers; buildtransitive 24 | 25 | 26 | all 27 | runtime; build; native; contentfiles; analyzers; buildtransitive 28 | 29 | 30 | all 31 | runtime; build; native; contentfiles; analyzers; buildtransitive 32 | 33 | 34 | all 35 | runtime; build; native; contentfiles; analyzers; buildtransitive 36 | 37 | 38 | all 39 | runtime; build; native; contentfiles; analyzers; buildtransitive 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/stylecop.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", 3 | 4 | "settings": { 5 | "orderingRules": { 6 | "elementOrder": [ 7 | "kind", 8 | "constant", 9 | "accessibility", 10 | "static", 11 | "readonly" 12 | ], 13 | "usingDirectivesPlacement": "outsideNamespace" 14 | }, 15 | "layoutRules": { 16 | "newlineAtEndOfFile": "omit" 17 | }, 18 | "documentationRules": { 19 | "xmlHeader": false, 20 | "documentInterfaces": false, 21 | "documentExposedElements": false, 22 | "documentInternalElements": false 23 | } 24 | } 25 | } --------------------------------------------------------------------------------