├── .editorconfig ├── .gitattributes ├── .github ├── CODEOWNERS ├── FUNDING.yml ├── renovate.json ├── settings.yml └── workflows │ ├── build-win.yml │ ├── build.yml │ ├── ci.yml │ └── labeled.yml ├── .gitignore ├── .ruleset ├── Directory.Build.props ├── LICENSE ├── Prometheus.Client.MetricServer.sln ├── Prometheus.Client.MetricServer.snk ├── README.md ├── icon.png ├── src ├── Defaults.cs ├── IMetricServer.cs ├── MetricServer.cs ├── MetricServerOptions.cs └── Prometheus.Client.MetricServer.csproj ├── stylecop.json └── tests ├── MetricServerTests.cs ├── PortFixture.cs └── Prometheus.Client.MetricServer.Tests.csproj /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | max_line_length = 160 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.cs] 13 | indent_size = 4 14 | 15 | ## Dotnet code style settings: 16 | 17 | # Sort using and Import directives with System.* appearing first 18 | dotnet_sort_system_directives_first = true 19 | # Avoid "this." and "Me." if not necessary 20 | dotnet_style_qualification_for_field = false:suggestion 21 | dotnet_style_qualification_for_property = false:suggestion 22 | dotnet_style_qualification_for_method = false:suggestion 23 | dotnet_style_qualification_for_event = false:suggestion 24 | 25 | # Use language keywords instead of framework type names for type references 26 | dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion 27 | dotnet_style_predefined_type_for_member_access = true:suggestion 28 | 29 | # Suggest more modern language features when available 30 | dotnet_style_object_initializer = true:suggestion 31 | dotnet_style_collection_initializer = true:suggestion 32 | dotnet_style_coalesce_expression = true:suggestion 33 | dotnet_style_null_propagation = true:suggestion 34 | dotnet_style_explicit_tuple_names = true:suggestion 35 | 36 | # CSharp code style settings: 37 | 38 | # Prefer "var" everywhere 39 | csharp_style_var_for_built_in_types = false:none 40 | csharp_style_var_when_type_is_apparent = true:suggestion 41 | csharp_style_var_elsewhere = true:suggestion 42 | 43 | # Prefer method-like constructs to have a block body 44 | csharp_style_expression_bodied_methods = false:none 45 | csharp_style_expression_bodied_constructors = false:none 46 | csharp_style_expression_bodied_operators = false:none 47 | 48 | # Prefer property-like constructs to have an expression-body 49 | csharp_style_expression_bodied_properties = true:none 50 | csharp_style_expression_bodied_indexers = true:none 51 | csharp_style_expression_bodied_accessors = true:none 52 | 53 | # Suggest more modern language features when available 54 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 55 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 56 | csharp_style_inlined_variable_declaration = true:suggestion 57 | csharp_style_throw_expression = true:suggestion 58 | csharp_style_conditional_delegate_call = true:suggestion 59 | 60 | # Newline settings 61 | csharp_new_line_before_else = true 62 | csharp_new_line_before_catch = true 63 | csharp_new_line_before_finally = true 64 | 65 | ## Naming 66 | 67 | ### private fields should be _camelCase 68 | 69 | dotnet_naming_style.underscore_prefix.capitalization = camel_case 70 | dotnet_naming_style.underscore_prefix.required_prefix = _ 71 | 72 | dotnet_naming_rule.private_fields_with_underscore.symbols = private_fields 73 | dotnet_naming_rule.private_fields_with_underscore.style = underscore_prefix 74 | dotnet_naming_rule.private_fields_with_underscore.severity = suggestion 75 | 76 | dotnet_naming_symbols.private_fields.applicable_kinds = field 77 | dotnet_naming_symbols.private_fields.applicable_accessibilities = private 78 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @phnx47 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: phnx47 2 | ko_fi: phnx47 3 | buy_me_a_coffee: phnx47 4 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [":dependencyDashboard", ":semanticPrefixFixDepsChoreOthers", "group:monorepos", "workarounds:all"], 4 | "labels": ["dependencies"], 5 | "assignees": ["phnx47"], 6 | "packageRules": [ 7 | { 8 | "automerge": true, 9 | "groupName": "coverlet packages", 10 | "matchSourceUrls": ["https://github.com/coverlet-coverage{/,}**"] 11 | }, 12 | { 13 | "automerge": true, 14 | "extends": ["monorepo:vstest", "monorepo:xunit-dotnet"] 15 | }, 16 | { 17 | "matchManagers": ["github-actions"], 18 | "enabled": false 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.github/settings.yml: -------------------------------------------------------------------------------- 1 | repository: 2 | topics: metrics, prometheus, prometheus-client 3 | has_issues: true 4 | has_wiki: false 5 | default_branch: main 6 | allow_auto_merge: true 7 | delete_branch_on_merge: true 8 | 9 | labels: 10 | - name: dependencies 11 | color: '0052CC' 12 | description: 13 | 14 | - name: bug 15 | color: 'D73A4A' 16 | description: 17 | 18 | - name: documentation 19 | color: '0075CA' 20 | description: 21 | 22 | - name: duplicate 23 | color: 'CFD3D7' 24 | description: 25 | 26 | - name: enhancement 27 | color: 'A2EEEF' 28 | description: 29 | 30 | - name: good first issue 31 | color: '7057FF' 32 | description: 33 | 34 | - name: help wanted 35 | color: '008672' 36 | description: 37 | 38 | - name: invalid 39 | color: 'E4E669' 40 | description: 41 | 42 | - name: question 43 | color: 'D876E3' 44 | description: 45 | 46 | - name: wontfix 47 | color: 'FFFFFF' 48 | description: 49 | 50 | - name: vulnerability 51 | color: 'D1260F' 52 | description: 53 | 54 | - name: sync 55 | color: '6E81A3' 56 | description: 57 | 58 | branches: 59 | - name: main 60 | protection: 61 | required_pull_request_reviews: null 62 | required_status_checks: 63 | strict: false 64 | contexts: ['Build & Test', 'Build & Test (Windows)'] 65 | enforce_admins: false 66 | required_linear_history: false 67 | restrictions: null 68 | -------------------------------------------------------------------------------- /.github/workflows/build-win.yml: -------------------------------------------------------------------------------- 1 | name: Build Win 2 | 3 | on: 4 | push: 5 | branches: 6 | - "main" 7 | pull_request: 8 | branches: 9 | - "main" 10 | workflow_dispatch: 11 | 12 | jobs: 13 | build-win: 14 | name: Build & Test (Windows) 15 | runs-on: windows-2025 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | 20 | - name: Setup .NET 21 | uses: actions/setup-dotnet@v4 22 | with: 23 | dotnet-version: | 24 | 6.0.x 25 | 8.0.x 26 | 27 | - name: Run tests 28 | run: dotnet test -c Release -p:CollectCoverage=false 29 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - "main" 7 | workflow_dispatch: 8 | workflow_call: 9 | 10 | jobs: 11 | build: 12 | name: Build & Test 13 | runs-on: ubuntu-24.04 14 | 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | 19 | - name: Setup .NET 20 | uses: actions/setup-dotnet@v4 21 | with: 22 | dotnet-version: | 23 | 6.0.x 24 | 8.0.x 25 | 26 | - name: Build 27 | run: dotnet build -c Release 28 | 29 | - name: Run tests with Coverage 30 | run: dotnet test --no-build -c Release -p:CollectCoverage=true -e:CoverletOutputFormat=opencover 31 | 32 | - name: Publish to Codecov 33 | uses: codecov/codecov-action@v5 34 | with: 35 | fail_ci_if_error: true 36 | token: ${{ secrets.CODECOV_TOKEN }} 37 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - "main" 7 | tags: 8 | - "v*" 9 | 10 | jobs: 11 | build: 12 | name: CI Build 13 | uses: ./.github/workflows/build.yml 14 | secrets: inherit 15 | 16 | pack: 17 | name: Create NuGet packages 18 | needs: [build] 19 | runs-on: ubuntu-24.04 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | with: 24 | fetch-depth: 0 25 | - name: Set Dev version 26 | if: github.ref == 'refs/heads/main' 27 | run: | 28 | version="$(git describe --long --tags | sed 's/^v//;0,/-/s//./')" 29 | if [ -z "${version}" ]; then 30 | version="0.0.0.$(git rev-list --count HEAD)-g$(git rev-parse --short HEAD)" 31 | fi 32 | echo "VERSION=${version}" >> $GITHUB_ENV 33 | 34 | - name: Set Release version 35 | if: startsWith(github.ref, 'refs/tags/v') 36 | run: echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV 37 | 38 | - name: Pack artifacts 39 | run: dotnet pack -p:PackageVersion="${{ env.VERSION }}" -o packages 40 | 41 | - name: Upload artifacts 42 | uses: actions/upload-artifact@v4 43 | with: 44 | name: packages 45 | path: packages/*nupkg 46 | 47 | github: 48 | name: Deploy to GitHub 49 | needs: [pack] 50 | runs-on: ubuntu-24.04 51 | steps: 52 | - name: Download artifacts 53 | uses: actions/download-artifact@v4 54 | with: 55 | name: packages 56 | - name: Push to pkg.github.com 57 | run: | 58 | dotnet nuget push "*.nupkg" \ 59 | --skip-duplicate \ 60 | -k ${{ secrets.GITHUB_TOKEN }} \ 61 | -s https://nuget.pkg.github.com/${{ github.repository_owner }}/index.json 62 | 63 | release: 64 | name: Create GitHub release 65 | needs: [pack] 66 | if: startsWith(github.ref, 'refs/tags/v') 67 | runs-on: ubuntu-24.04 68 | steps: 69 | - name: Checkout 70 | uses: actions/checkout@v4 71 | - name: Download artifacts 72 | uses: actions/download-artifact@v4 73 | with: 74 | name: packages 75 | path: packages 76 | - name: Create GitHub Release 77 | run: gh release create ${{ github.ref_name }} packages/*nupkg 78 | env: 79 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 80 | 81 | nuget: 82 | name: Deploy to NuGet 83 | needs: [release] 84 | if: startsWith(github.ref, 'refs/tags/v') 85 | runs-on: ubuntu-24.04 86 | steps: 87 | - name: Download artifacts 88 | uses: actions/download-artifact@v4 89 | with: 90 | name: packages 91 | - name: Push to nuget.org 92 | run: | 93 | dotnet nuget push "*.nupkg" \ 94 | -k ${{ secrets.NUGET_DEPLOY_KEY }} \ 95 | -s https://api.nuget.org/v3/index.json 96 | -------------------------------------------------------------------------------- /.github/workflows/labeled.yml: -------------------------------------------------------------------------------- 1 | name: PR Labeled 2 | 3 | on: 4 | pull_request: 5 | types: [labeled] 6 | branches: 7 | - "main" 8 | 9 | permissions: 10 | pull-requests: write 11 | contents: write 12 | 13 | jobs: 14 | automerge: 15 | name: Enable auto-merge 16 | runs-on: ubuntu-24.04 17 | if: github.actor == 'phnx47-bot' && contains(github.event.pull_request.labels.*.name, 'sync') 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | 22 | - name: Run command 23 | run: gh pr merge -s --auto ${{ github.event.pull_request.number }} 24 | env: 25 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.user 2 | *.opencover.xml 3 | *.orig 4 | *.nupkg 5 | *.snupkg 6 | .idea 7 | .vs 8 | bin 9 | obj 10 | BenchmarkDotNet.Artifacts 11 | -------------------------------------------------------------------------------- /.ruleset: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12.0 4 | prom-client-net contributors 5 | Copyright © prom-client-net 6 | $(MSBuildProjectName) 7 | prometheus;metrics 8 | icon.png 9 | README.md 10 | MIT 11 | git 12 | true 13 | true 14 | true 15 | true 16 | snupkg 17 | true 18 | nupkgs 19 | $(SolutionDir)$(SolutionName).snk 20 | $(SolutionDir).ruleset 21 | CS1591;NETSDK1138 22 | 23 | 24 | true 25 | 26 | 27 | 28 | 29 | 30 | stylecop.json 31 | 32 | 33 | 34 | 35 | all 36 | runtime; build; native; contentfiles; analyzers 37 | 38 | 39 | all 40 | runtime; build; native; contentfiles; analyzers 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) prom-client-net 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Prometheus.Client.MetricServer.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29215.179 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Prometheus.Client.MetricServer", "src\Prometheus.Client.MetricServer.csproj", "{62E44DB5-72EE-46E1-AF9D-4654A618A90A}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Prometheus.Client.MetricServer.Tests", "tests\Prometheus.Client.MetricServer.Tests.csproj", "{498531D2-4D87-48AB-BB96-A03D2196EF0D}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {62E44DB5-72EE-46E1-AF9D-4654A618A90A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {62E44DB5-72EE-46E1-AF9D-4654A618A90A}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {62E44DB5-72EE-46E1-AF9D-4654A618A90A}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {62E44DB5-72EE-46E1-AF9D-4654A618A90A}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {498531D2-4D87-48AB-BB96-A03D2196EF0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {498531D2-4D87-48AB-BB96-A03D2196EF0D}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {498531D2-4D87-48AB-BB96-A03D2196EF0D}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {498531D2-4D87-48AB-BB96-A03D2196EF0D}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(NestedProjects) = preSolution 29 | EndGlobalSection 30 | GlobalSection(ExtensibilityGlobals) = postSolution 31 | SolutionGuid = {6CA14866-8A1E-4C8C-AE98-A806F33741EF} 32 | EndGlobalSection 33 | EndGlobal 34 | -------------------------------------------------------------------------------- /Prometheus.Client.MetricServer.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prom-client-net/prom-client-metricserver/9f1de6b74ed474182a7edec68992dfe5c568f06e/Prometheus.Client.MetricServer.snk -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Prometheus.Client.MetricServer 2 | 3 | [![ci](https://img.shields.io/github/actions/workflow/status/prom-client-net/prom-client-metricserver/ci.yml?branch=main&label=ci&logo=github&style=flat-square)](https://github.com/prom-client-net/prom-client-metricserver/actions/workflows/ci.yml) 4 | [![nuget](https://img.shields.io/nuget/v/Prometheus.Client.MetricServer?logo=nuget&style=flat-square)](https://www.nuget.org/packages/Prometheus.Client.MetricServer) 5 | [![nuget](https://img.shields.io/nuget/dt/Prometheus.Client.MetricServer?logo=nuget&style=flat-square)](https://www.nuget.org/packages/Prometheus.Client.MetricServer) 6 | [![codecov](https://img.shields.io/codecov/c/github/prom-client-net/prom-client-metricserver?logo=codecov&style=flat-square)](https://app.codecov.io/gh/prom-client-net/prom-client-metricserver) 7 | [![license](https://img.shields.io/github/license/prom-client-net/prom-client-metricserver?style=flat-square)](https://github.com/prom-client-net/prom-client-metricserver/blob/main/LICENSE) 8 | 9 | Extension for [Prometheus.Client](https://github.com/prom-client-net/prom-client) 10 | 11 | ## Install 12 | 13 | ```sh 14 | dotnet add package Prometheus.Client.MetricServer 15 | ``` 16 | 17 | ## Use 18 | 19 | [Examples](https://github.com/prom-client-net/prom-examples) 20 | 21 | Simple Console App with static MetricFactory: 22 | 23 | ```c# 24 | public static void Main(string[] args) 25 | { 26 | var options = new MetricServerOptions 27 | { 28 | Port = 9091 29 | }; 30 | 31 | var metricServer = new MetricServer(options); 32 | metricServer.Start(); 33 | 34 | var counter = Metrics.DefaultFactory.CreateCounter("test_count", "helptext"); 35 | counter.Inc(); 36 | 37 | metricServer.Stop(); 38 | } 39 | 40 | ``` 41 | 42 | Worker with DI [Prometheus.Client.DependencyInjection](https://github.com/prom-client-net/prom-client-dependencyinjection): 43 | 44 | ```c# 45 | public static async Task Main(string[] args) 46 | { 47 | var host = Host.CreateDefaultBuilder(args) 48 | .ConfigureServices((_, services) => 49 | { 50 | services.AddMetricFactory(); 51 | services.AddSingleton(sp => new MetricServer( 52 | new MetricServerOptions 53 | { 54 | CollectorRegistry = sp.GetRequiredService(), 55 | UseDefaultCollectors = true 56 | })); 57 | services.AddHostedService(); 58 | }).Build(); 59 | 60 | var metricServer = host.Services.GetRequiredService(); 61 | 62 | try 63 | { 64 | metricServer.Start(); 65 | await host.RunAsync(); 66 | } 67 | catch (Exception ex) 68 | { 69 | Console.WriteLine("Host Terminated Unexpectedly"); 70 | } 71 | finally 72 | { 73 | metricServer.Stop(); 74 | } 75 | } 76 | 77 | ``` 78 | 79 | ## License 80 | 81 | All contents of this package are licensed under the [MIT license](https://opensource.org/licenses/MIT). 82 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/prom-client-net/prom-client-metricserver/9f1de6b74ed474182a7edec68992dfe5c568f06e/icon.png -------------------------------------------------------------------------------- /src/Defaults.cs: -------------------------------------------------------------------------------- 1 | namespace Prometheus.Client.MetricServer; 2 | 3 | internal static class Defaults 4 | { 5 | internal const string MapPath = "/metrics"; 6 | internal const string ContentType = "text/plain; version=0.0.4"; 7 | } 8 | -------------------------------------------------------------------------------- /src/IMetricServer.cs: -------------------------------------------------------------------------------- 1 | namespace Prometheus.Client.MetricServer; 2 | 3 | /// 4 | /// Interface for the metrics server. 5 | /// 6 | public interface IMetricServer 7 | { 8 | /// 9 | /// Get a value indicating whether the server is currently running. 10 | /// 11 | bool IsRunning { get; } 12 | 13 | /// 14 | /// Start the metrics server. 15 | /// 16 | void Start(); 17 | 18 | /// 19 | /// Stop the metrics server. 20 | /// 21 | void Stop(); 22 | } 23 | -------------------------------------------------------------------------------- /src/MetricServer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Reflection; 4 | using Microsoft.AspNetCore.Builder; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Prometheus.Client.Collectors; 9 | 10 | namespace Prometheus.Client.MetricServer; 11 | 12 | /// 13 | /// Kestrel-based implementation of the metrics server. 14 | /// 15 | public class MetricServer : IMetricServer 16 | { 17 | private readonly MetricServerOptions _options; 18 | private IWebHost _host; 19 | 20 | /// 21 | /// Initialize a new instance of the class with default options. 22 | /// 23 | public MetricServer() 24 | : this(new MetricServerOptions()) 25 | { 26 | } 27 | 28 | /// 29 | /// Initialize a new instance of the class with the specified options. 30 | /// 31 | /// The configuration. 32 | public MetricServer(MetricServerOptions options) 33 | { 34 | ArgumentNullException.ThrowIfNull(options); 35 | if (!options.MapPath.StartsWith('/')) 36 | options.MapPath = "/" + options.MapPath; 37 | _options = options; 38 | _options.CollectorRegistry ??= Metrics.DefaultCollectorRegistry; 39 | if (_options.UseDefaultCollectors) 40 | options.CollectorRegistry.UseDefaultCollectors(options.MetricPrefixName); 41 | } 42 | 43 | public bool IsRunning => _host != null; 44 | 45 | public void Start() 46 | { 47 | if (IsRunning) 48 | return; 49 | var configBuilder = new ConfigurationBuilder(); 50 | configBuilder.Properties["parent"] = this; 51 | var config = configBuilder.Build(); 52 | _host = new WebHostBuilder() 53 | .UseConfiguration(config) 54 | .UseKestrel(options => 55 | { 56 | if (_options.Certificate != null) 57 | options.Listen(IPAddress.Any, _options.Port, listenOptions => { listenOptions.UseHttps(_options.Certificate); }); 58 | }) 59 | .UseUrls($"http{(_options.Certificate != null ? "s" : "")}://{_options.Host}:{_options.Port}") 60 | .ConfigureServices(services => { services.AddSingleton(new Startup(_options)); }) 61 | .UseSetting(WebHostDefaults.ApplicationKey, typeof(Startup).GetTypeInfo().Assembly.FullName) 62 | .Build(); 63 | _host.Start(); 64 | } 65 | 66 | public void Stop() 67 | { 68 | if (!IsRunning) 69 | return; 70 | _host.Dispose(); 71 | _host = null; 72 | } 73 | 74 | internal class Startup(MetricServerOptions options) : IStartup 75 | { 76 | public IServiceProvider ConfigureServices(IServiceCollection services) 77 | { 78 | return services.BuildServiceProvider(); 79 | } 80 | 81 | public void Configure(IApplicationBuilder app) 82 | { 83 | var contentType = options.ResponseEncoding != null 84 | ? $"{Defaults.ContentType}; charset={options.ResponseEncoding.BodyName}" 85 | : Defaults.ContentType; 86 | app.Map(options.MapPath, coreapp => 87 | { 88 | coreapp.Run(async context => 89 | { 90 | var response = context.Response; 91 | response.ContentType = contentType; 92 | await using var outputStream = response.Body; 93 | await ScrapeHandler.ProcessAsync(options.CollectorRegistry, outputStream); 94 | }); 95 | }); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/MetricServerOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Cryptography.X509Certificates; 2 | using System.Text; 3 | using Prometheus.Client.Collectors; 4 | 5 | namespace Prometheus.Client.MetricServer; 6 | 7 | /// 8 | /// Configuration options for the metrics server. 9 | /// 10 | public class MetricServerOptions 11 | { 12 | /// 13 | /// The hostname to bind the server to. Default is "*". 14 | /// 15 | public string Host { get; set; } = "*"; 16 | 17 | /// 18 | /// The port number to listen on. Default is 5000. 19 | /// 20 | public int Port { get; set; } = 5000; 21 | 22 | /// 23 | /// The endpoint path for metrics. Default is "/metrics". 24 | /// 25 | public string MapPath { get; set; } = Defaults.MapPath; 26 | 27 | /// 28 | /// The HTTPS certificate. 29 | /// 30 | public X509Certificate2 Certificate { get; set; } 31 | 32 | /// 33 | /// The instance to use for metric collection. 34 | /// 35 | public ICollectorRegistry CollectorRegistry { get; set; } 36 | 37 | /// 38 | /// Whether to register default collectors. Default is true. 39 | /// 40 | public bool UseDefaultCollectors { get; set; } = true; 41 | 42 | /// 43 | /// The text encoding for response content. 44 | /// 45 | public Encoding ResponseEncoding { get; set; } 46 | 47 | /// 48 | /// Metric name prefix for default collectors. 49 | /// 50 | public string MetricPrefixName { get; set; } = string.Empty; 51 | } 52 | -------------------------------------------------------------------------------- /src/Prometheus.Client.MetricServer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Standalone Kestrel server for Prometheus.Client 4 | net8.0 5 | https://github.com/prom-client-net/prom-client-metricserver 6 | true 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /stylecop.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json", 3 | "settings": { 4 | "layoutRules": { 5 | "newlineAtEndOfFile": "require" 6 | }, 7 | "documentationRules": { 8 | "xmlHeader": false, 9 | "copyrightText": "", 10 | "documentInterfaces": false, 11 | "documentExposedElements": false, 12 | "documentInternalElements": false, 13 | "documentPrivateElements": false, 14 | "documentPrivateFields": false 15 | }, 16 | "orderingRules": { 17 | "usingDirectivesPlacement": "outsideNamespace", 18 | "systemUsingDirectivesFirst": true, 19 | "blankLinesBetweenUsingGroups": "omit", 20 | "elementOrder": [ 21 | "kind", 22 | "constant", 23 | "static", 24 | "readonly" 25 | ] 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/MetricServerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Net.Http; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Prometheus.Client.Collectors; 7 | using Xunit; 8 | 9 | namespace Prometheus.Client.MetricServer.Tests; 10 | 11 | public class MetricServerTests(PortFixture fixture, ITestOutputHelper testOutputHelper) : IClassFixture 12 | { 13 | private MetricServer _metricServer = new(new MetricServerOptions 14 | { 15 | Port = fixture.Port, 16 | CollectorRegistry = new CollectorRegistry() 17 | }); 18 | 19 | [Fact] 20 | public void Null_Options_Throws_ArgumentNullException() 21 | { 22 | Assert.Throws(() => new MetricServer(null)); 23 | } 24 | 25 | [Fact] 26 | public void Start_Stop_IsRunning() 27 | { 28 | _metricServer.Start(); 29 | Assert.True(_metricServer.IsRunning); 30 | _metricServer.Stop(); 31 | Assert.False(_metricServer.IsRunning); 32 | } 33 | 34 | [Fact] 35 | public void Start_DoubleStop_IsRunning() 36 | { 37 | _metricServer.Start(); 38 | Assert.True(_metricServer.IsRunning); 39 | _metricServer.Stop(); 40 | Assert.False(_metricServer.IsRunning); 41 | _metricServer.Stop(); 42 | Assert.False(_metricServer.IsRunning); 43 | } 44 | 45 | [Fact] 46 | public void DoubleStart_Stop_IsRunning() 47 | { 48 | _metricServer.Start(); 49 | Assert.True(_metricServer.IsRunning); 50 | _metricServer.Start(); 51 | Assert.True(_metricServer.IsRunning); 52 | _metricServer.Stop(); 53 | Assert.False(_metricServer.IsRunning); 54 | } 55 | 56 | [Fact] 57 | public void Start_Stop_WithDefaultPort_IsRunning() 58 | { 59 | _metricServer = new MetricServer(new MetricServerOptions { CollectorRegistry = new CollectorRegistry() }); 60 | _metricServer.Start(); 61 | Assert.True(_metricServer.IsRunning); 62 | _metricServer.Stop(); 63 | Assert.False(_metricServer.IsRunning); 64 | } 65 | 66 | [Fact] 67 | public void Start_Stop_WithDefaultRegisry_IsRunning() 68 | { 69 | _metricServer = new MetricServer(); 70 | _metricServer.Start(); 71 | Assert.True(_metricServer.IsRunning); 72 | _metricServer.Stop(); 73 | Assert.False(_metricServer.IsRunning); 74 | } 75 | 76 | [Fact] 77 | public async Task BaseMapPath_FindMetrics() 78 | { 79 | try 80 | { 81 | _metricServer.Start(); 82 | var counter = Metrics.DefaultFactory.CreateCounter("test_counter", "help"); 83 | counter.Inc(); 84 | using var httpClient = new HttpClient(); 85 | string response = await httpClient.GetStringAsync($"http://localhost:{fixture.Port}{Defaults.MapPath}", TestContext.Current.CancellationToken); 86 | Assert.False(string.IsNullOrEmpty(response)); 87 | Assert.Contains("process_private_memory_bytes", response); 88 | Assert.Contains("dotnet_total_memory_bytes", response); 89 | } 90 | catch (Exception ex) 91 | { 92 | testOutputHelper.WriteLine(ex.ToString()); 93 | throw; 94 | } 95 | finally 96 | { 97 | _metricServer.Stop(); 98 | } 99 | } 100 | 101 | [Fact] 102 | public async Task SetMapPath_FindMetricsWithEndSlash() 103 | { 104 | _metricServer = new MetricServer( 105 | new MetricServerOptions { Port = fixture.Port, CollectorRegistry = new CollectorRegistry(), MapPath = "/test" }); 106 | try 107 | { 108 | _metricServer.Start(); 109 | var counter = Metrics.DefaultFactory.CreateCounter("test_counter", "help"); 110 | counter.Inc(); 111 | using var httpClient = new HttpClient(); 112 | string response = await httpClient.GetStringAsync($"http://localhost:{fixture.Port}/test/", TestContext.Current.CancellationToken); 113 | Assert.False(string.IsNullOrEmpty(response)); 114 | Assert.Contains("process_private_memory_bytes", response); 115 | Assert.Contains("dotnet_total_memory_bytes", response); 116 | } 117 | catch (Exception ex) 118 | { 119 | testOutputHelper.WriteLine(ex.ToString()); 120 | throw; 121 | } 122 | finally 123 | { 124 | _metricServer.Stop(); 125 | } 126 | } 127 | 128 | [Theory] 129 | [InlineData(Defaults.MapPath)] 130 | [InlineData("metrics")] 131 | [InlineData("metrics12")] 132 | [InlineData("/metrics965")] 133 | public async Task SetMapPath_FindMetrics(string mapPath) 134 | { 135 | _metricServer = new MetricServer( 136 | new MetricServerOptions { Port = fixture.Port, CollectorRegistry = new CollectorRegistry(), MapPath = mapPath }); 137 | try 138 | { 139 | _metricServer.Start(); 140 | using var httpClient = new HttpClient(); 141 | if (!mapPath.StartsWith("/")) 142 | mapPath = "/" + mapPath; 143 | string response = await httpClient.GetStringAsync($"http://localhost:{fixture.Port}" + mapPath, TestContext.Current.CancellationToken); 144 | Assert.False(string.IsNullOrEmpty(response)); 145 | Assert.Contains("process_private_memory_bytes", response); 146 | Assert.Contains("dotnet_total_memory_bytes", response); 147 | } 148 | catch (Exception ex) 149 | { 150 | testOutputHelper.WriteLine(ex.ToString()); 151 | throw; 152 | } 153 | finally 154 | { 155 | _metricServer.Stop(); 156 | } 157 | } 158 | 159 | [Fact] 160 | public async Task CustomCounter_FindMetric() 161 | { 162 | var registry = new CollectorRegistry(); 163 | var factory = new MetricFactory(registry); 164 | _metricServer = new MetricServer(new MetricServerOptions { Port = fixture.Port, CollectorRegistry = registry }); 165 | 166 | try 167 | { 168 | _metricServer.Start(); 169 | 170 | const string metricName = "myCounter"; 171 | var counter = factory.CreateCounter(metricName, "helptext"); 172 | counter.Inc(); 173 | 174 | using var httpClient = new HttpClient(); 175 | string response = await httpClient.GetStringAsync($"http://localhost:{fixture.Port}{Defaults.MapPath}", TestContext.Current.CancellationToken); 176 | Assert.Contains(metricName, response); 177 | } 178 | catch (Exception ex) 179 | { 180 | testOutputHelper.WriteLine(ex.ToString()); 181 | throw; 182 | } 183 | finally 184 | { 185 | _metricServer.Stop(); 186 | } 187 | } 188 | 189 | [Fact] 190 | public async Task WrongUrl_NotFound() 191 | { 192 | try 193 | { 194 | _metricServer.Start(); 195 | var counter = Metrics.DefaultFactory.CreateCounter("test_counter", "help"); 196 | counter.Inc(); 197 | using var httpClient = new HttpClient(); 198 | 199 | var response = await httpClient.GetAsync($"http://localhost:{fixture.Port}/not-found", TestContext.Current.CancellationToken); 200 | Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); 201 | } 202 | catch (Exception ex) 203 | { 204 | testOutputHelper.WriteLine(ex.ToString()); 205 | throw; 206 | } 207 | finally 208 | { 209 | _metricServer.Stop(); 210 | } 211 | } 212 | 213 | [Fact] 214 | public async Task CustormEncoding_FindHelp() 215 | { 216 | var registry = new CollectorRegistry(); 217 | var factory = new MetricFactory(registry); 218 | _metricServer = new MetricServer( 219 | new MetricServerOptions { Port = fixture.Port, CollectorRegistry = registry, ResponseEncoding = Encoding.UTF8 }); 220 | 221 | try 222 | { 223 | _metricServer.Start(); 224 | 225 | const string help = "русский хелп"; 226 | var counter = factory.CreateCounter("test_counter_rus", help); 227 | counter.Inc(); 228 | 229 | using var httpClient = new HttpClient(); 230 | string response = await httpClient.GetStringAsync($"http://localhost:{fixture.Port}{Defaults.MapPath}", TestContext.Current.CancellationToken); 231 | Assert.Contains(help, response); 232 | } 233 | catch (Exception ex) 234 | { 235 | testOutputHelper.WriteLine(ex.ToString()); 236 | throw; 237 | } 238 | finally 239 | { 240 | _metricServer.Stop(); 241 | } 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /tests/PortFixture.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Net.Sockets; 3 | 4 | namespace Prometheus.Client.MetricServer.Tests; 5 | 6 | public class PortFixture 7 | { 8 | public int Port { get; } = FindAvailablePort(); 9 | 10 | private static int FindAvailablePort() 11 | { 12 | var listener = new TcpListener(IPAddress.Loopback, 0); 13 | 14 | try 15 | { 16 | listener.Start(); 17 | int port = ((IPEndPoint)listener.LocalEndpoint).Port; 18 | return port; 19 | } 20 | finally 21 | { 22 | listener.Stop(); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/Prometheus.Client.MetricServer.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net8.0 4 | $(NoWarn);CS0618 5 | false 6 | 7 | 8 | 9 | 10 | 11 | all 12 | runtime; build; native; contentfiles; analyzers 13 | 14 | 15 | all 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | 18 | 19 | all 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | 22 | 23 | 24 | 25 | 26 | 27 | --------------------------------------------------------------------------------