├── .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 | [](https://github.com/prom-client-net/prom-client-metricserver/actions/workflows/ci.yml)
4 | [](https://www.nuget.org/packages/Prometheus.Client.MetricServer)
5 | [](https://www.nuget.org/packages/Prometheus.Client.MetricServer)
6 | [](https://app.codecov.io/gh/prom-client-net/prom-client-metricserver)
7 | [](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 |
--------------------------------------------------------------------------------