From 2e9ca5e0fd1c90118b14b43b96cd9e8d34bde041 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 27 Oct 2022 12:08:33 +0300 Subject: [PATCH 001/230] Added back .NET Standard 2.0 support as some customers had a hard dependency on .NET Standard 2.0 (despite not being a documented feature even earlier). --- History | 2 + Prometheus/DiagnosticSourceAdapter.cs | 4 +- Prometheus/DiagnosticSourceAdapterOptions.cs | 4 +- Prometheus/MeterAdapter.cs | 6 +- Prometheus/MeterAdapterOptions.cs | 4 +- Prometheus/Metrics.cs | 11 +++- Prometheus/Prometheus.csproj | 6 +- Prometheus/SuppressDefaultMetricOptions.cs | 23 +++++-- Resources/Nuspec/prometheus-net.nuspec | 9 ++- Resources/SolutionAssemblyInfo.cs | 2 +- Sample.Web.NetFramework/Web.config | 68 +++++++++++--------- Tester.NetFramework.AspNet/Web.config | 4 ++ 12 files changed, 94 insertions(+), 49 deletions(-) diff --git a/History b/History index 7d06e885..0fc39c19 100644 --- a/History +++ b/History @@ -1,3 +1,5 @@ +* 7.1.0 +- Added back .NET Standard 2.0 support as some customers had a hard dependency on .NET Standard 2.0 (despite not being a documented feature even earlier). * 7.0.0 - .NET Core specific functionality now targeting .NET 6.0 or greater (all previous versions will be end of life by December 2022). - Relaxed the API restriction that forbade you to create multiple metrics with the same name but different label names. While this is a Prometheus anti-pattern, it is a standard pattern in other metrics technologies and we will allow it in the name of interoperability. diff --git a/Prometheus/DiagnosticSourceAdapter.cs b/Prometheus/DiagnosticSourceAdapter.cs index fcbcfb9f..8a407e20 100644 --- a/Prometheus/DiagnosticSourceAdapter.cs +++ b/Prometheus/DiagnosticSourceAdapter.cs @@ -1,4 +1,5 @@ -using System.Diagnostics; +#if NET +using System.Diagnostics; namespace Prometheus { @@ -129,3 +130,4 @@ public void Dispose() } } } +#endif \ No newline at end of file diff --git a/Prometheus/DiagnosticSourceAdapterOptions.cs b/Prometheus/DiagnosticSourceAdapterOptions.cs index 089c1cc0..2bf9cf47 100644 --- a/Prometheus/DiagnosticSourceAdapterOptions.cs +++ b/Prometheus/DiagnosticSourceAdapterOptions.cs @@ -1,4 +1,5 @@ -using System.Diagnostics; +#if NET +using System.Diagnostics; namespace Prometheus { @@ -14,3 +15,4 @@ public sealed class DiagnosticSourceAdapterOptions public CollectorRegistry Registry = Metrics.DefaultRegistry; } } +#endif \ No newline at end of file diff --git a/Prometheus/MeterAdapter.cs b/Prometheus/MeterAdapter.cs index 62dfe5fe..8862caa3 100644 --- a/Prometheus/MeterAdapter.cs +++ b/Prometheus/MeterAdapter.cs @@ -1,4 +1,5 @@ -using System.Collections.Concurrent; +#if NET6_0_OR_GREATER +using System.Collections.Concurrent; using System.Diagnostics; using System.Diagnostics.Metrics; using System.Diagnostics.Tracing; @@ -272,4 +273,5 @@ private static string TranslateInstrumentDescriptionToPrometheusHelp(Instrument return sb.ToString(); } -} \ No newline at end of file +} +#endif \ No newline at end of file diff --git a/Prometheus/MeterAdapterOptions.cs b/Prometheus/MeterAdapterOptions.cs index 4b82dd1e..e27da58f 100644 --- a/Prometheus/MeterAdapterOptions.cs +++ b/Prometheus/MeterAdapterOptions.cs @@ -1,4 +1,5 @@ -using System.Diagnostics.Metrics; +#if NET6_0_OR_GREATER +using System.Diagnostics.Metrics; namespace Prometheus; @@ -31,3 +32,4 @@ public sealed class MeterAdapterOptions /// public Func ResolveHistogramBuckets { get; set; } = _ => DefaultHistogramBuckets; } +#endif \ No newline at end of file diff --git a/Prometheus/Metrics.cs b/Prometheus/Metrics.cs index d994aa4a..09571688 100644 --- a/Prometheus/Metrics.cs +++ b/Prometheus/Metrics.cs @@ -144,8 +144,10 @@ public static void SuppressDefaultMetrics(SuppressDefaultMetricOptions options) { var configureCallbacks = new SuppressDefaultMetricOptions.ConfigurationCallbacks() { -#if NET6_0_OR_GREATER +#if NET ConfigureEventCounterAdapter = _configureEventCounterAdapterCallback, +#endif +#if NET6_0_OR_GREATER ConfigureMeterAdapter = _configureMeterAdapterOptions #endif }; @@ -154,14 +156,17 @@ public static void SuppressDefaultMetrics(SuppressDefaultMetricOptions options) }); } -#if NET6_0_OR_GREATER +#if NET private static Action _configureEventCounterAdapterCallback = delegate { }; - private static Action _configureMeterAdapterOptions = delegate { }; /// /// Configures the event counter adapter that is enabled by default on startup. /// public static void ConfigureEventCounterAdapter(Action callback) => _configureEventCounterAdapterCallback = callback; +#endif + +#if NET6_0_OR_GREATER + private static Action _configureMeterAdapterOptions = delegate { }; /// /// Configures the meter adapter that is enabled by default on startup. diff --git a/Prometheus/Prometheus.csproj b/Prometheus/Prometheus.csproj index 1edb9783..21f6c246 100644 --- a/Prometheus/Prometheus.csproj +++ b/Prometheus/Prometheus.csproj @@ -1,10 +1,10 @@  - net6.0 + net6.0;netstandard2.0 - net462;net6.0 + net462;net6.0;netstandard2.0 @@ -44,7 +44,7 @@ - + diff --git a/Prometheus/SuppressDefaultMetricOptions.cs b/Prometheus/SuppressDefaultMetricOptions.cs index e03c5922..6b2b19cb 100644 --- a/Prometheus/SuppressDefaultMetricOptions.cs +++ b/Prometheus/SuppressDefaultMetricOptions.cs @@ -6,8 +6,11 @@ public sealed class SuppressDefaultMetricOptions { SuppressProcessMetrics = true, SuppressDebugMetrics = true, -#if NET6_0_OR_GREATER +#if NET SuppressEventCounters = true, +#endif + +#if NET6_0_OR_GREATER SuppressMeters = true #endif }; @@ -16,8 +19,11 @@ public sealed class SuppressDefaultMetricOptions { SuppressProcessMetrics = false, SuppressDebugMetrics = false, -#if NET6_0_OR_GREATER +#if NET SuppressEventCounters = false, +#endif + +#if NET6_0_OR_GREATER SuppressMeters = false #endif }; @@ -32,12 +38,14 @@ public sealed class SuppressDefaultMetricOptions /// public bool SuppressDebugMetrics { get; set; } -#if NET6_0_OR_GREATER +#if NET /// /// Suppress the default .NET Event Counter integration. /// public bool SuppressEventCounters { get; set; } +#endif +#if NET6_0_OR_GREATER /// /// Suppress the .NET Meter API integration. /// @@ -46,8 +54,11 @@ public sealed class SuppressDefaultMetricOptions internal sealed class ConfigurationCallbacks { -#if NET6_0_OR_GREATER +#if NET public Action ConfigureEventCounterAdapter = delegate { }; +#endif + +#if NET6_0_OR_GREATER public Action ConfigureMeterAdapter = delegate { }; #endif } @@ -65,14 +76,16 @@ internal void Configure(CollectorRegistry registry, ConfigurationCallbacks confi if (!SuppressDebugMetrics) registry.StartCollectingRegistryMetrics(); -#if NET6_0_OR_GREATER +#if NET if (!SuppressEventCounters) { var options = new EventCounterAdapterOptions(); configurationCallbacks.ConfigureEventCounterAdapter(options); EventCounterAdapter.StartListening(options); } +#endif +#if NET6_0_OR_GREATER if (!SuppressMeters) { var options = new MeterAdapterOptions(); diff --git a/Resources/Nuspec/prometheus-net.nuspec b/Resources/Nuspec/prometheus-net.nuspec index 257eb7ef..d70a506f 100644 --- a/Resources/Nuspec/prometheus-net.nuspec +++ b/Resources/Nuspec/prometheus-net.nuspec @@ -17,16 +17,21 @@ - + - + + + + + + diff --git a/Resources/SolutionAssemblyInfo.cs b/Resources/SolutionAssemblyInfo.cs index 5e93fced..e5a3f103 100644 --- a/Resources/SolutionAssemblyInfo.cs +++ b/Resources/SolutionAssemblyInfo.cs @@ -2,7 +2,7 @@ using System.Runtime.CompilerServices; // This is the real version number, used in NuGet packages and for display purposes. -[assembly: AssemblyFileVersion("7.0.0")] +[assembly: AssemblyFileVersion("7.1.0")] // Only use major version here, with others kept at zero, for correct assembly binding logic. [assembly: AssemblyVersion("7.0.0")] diff --git a/Sample.Web.NetFramework/Web.config b/Sample.Web.NetFramework/Web.config index 5ad0b860..1a4f9c74 100644 --- a/Sample.Web.NetFramework/Web.config +++ b/Sample.Web.NetFramework/Web.config @@ -1,67 +1,75 @@ - + - - - - + + + + - - + + - - - - + + + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - \ No newline at end of file + diff --git a/Tester.NetFramework.AspNet/Web.config b/Tester.NetFramework.AspNet/Web.config index 41fd9284..bee5b6c5 100644 --- a/Tester.NetFramework.AspNet/Web.config +++ b/Tester.NetFramework.AspNet/Web.config @@ -32,6 +32,10 @@ + + + + From f980713cb32f03b7743d9fb8a54eaca0114b6307 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 27 Oct 2022 12:11:45 +0300 Subject: [PATCH 002/230] Docs --- Prometheus/Prometheus.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Prometheus/Prometheus.csproj b/Prometheus/Prometheus.csproj index 21f6c246..a6b5d3ad 100644 --- a/Prometheus/Prometheus.csproj +++ b/Prometheus/Prometheus.csproj @@ -9,7 +9,7 @@ From d8a4c95598683edc39138ce3451dc00f7d5d36b2 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 27 Oct 2022 12:31:01 +0300 Subject: [PATCH 003/230] Add .NET Standard sample project --- README.md | 1 + Sample.NetStandard/ImportantProcess.cs | 21 ++++++++++++++++++++ Sample.NetStandard/Sample.NetStandard.csproj | 20 +++++++++++++++++++ prometheus-net.sln | 6 ++++++ 4 files changed, 48 insertions(+) create mode 100644 Sample.NetStandard/ImportantProcess.cs create mode 100644 Sample.NetStandard/Sample.NetStandard.csproj diff --git a/README.md b/README.md index 03a328ac..40882f60 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,7 @@ Refer to the sample projects for quick start instructions: | [Sample.Console.NoAspNetCore](Sample.Console.NoAspNetCore/Program.cs) | .NET console application that exports custom metrics without requiring the ASP.NET Core runtime to be installed | | [Sample.Grpc](Sample.Grpc/Program.cs) | ASP.NET Core application that publishes a gRPC service | | [Sample.Grpc.Client](Sample.Grpc.Client/Program.cs) | Client app for the above | +| [Sample.NetStandard](Sample.NetStandard/ImportantProcess.cs) | Demonstrates how to reference prometheus-net in a .NET Standard class library | | [Sample.Web.DifferentPort](Sample.Web.DifferentPort/Program.cs) | Demonstrates how to set up the metric exporter on a different port from the main web API (e.g. for security purposes) | | [Sample.Web.MetricExpiration](Sample.Web.MetricExpiration/Program.cs) | Demonstrates how to use [automatic metric unpublishing](#unpublishing-metrics) | | [Sample.Web.NetFramework](Sample.Web.NetFramework/Global.asax.cs) | .NET Framework web app that publishes custom metrics | diff --git a/Sample.NetStandard/ImportantProcess.cs b/Sample.NetStandard/ImportantProcess.cs new file mode 100644 index 00000000..9f96d77c --- /dev/null +++ b/Sample.NetStandard/ImportantProcess.cs @@ -0,0 +1,21 @@ +using Prometheus; + +namespace Sample.NetStandard; + +public static class ImportantProcess +{ + public static void Start() + { + _ = Task.Run(async delegate + { + while (true) + { + ImportantCounter.Inc(); + + await Task.Delay(TimeSpan.FromSeconds(0.1)); + } + }); + } + + private static readonly Counter ImportantCounter = Metrics.CreateCounter("sample_important_counter", "Counts up and up and up!"); +} diff --git a/Sample.NetStandard/Sample.NetStandard.csproj b/Sample.NetStandard/Sample.NetStandard.csproj new file mode 100644 index 00000000..fb1cf430 --- /dev/null +++ b/Sample.NetStandard/Sample.NetStandard.csproj @@ -0,0 +1,20 @@ + + + + netstandard2.0 + + enable + enable + True + True + 1591 + + latest + 9999 + + + + + + + diff --git a/prometheus-net.sln b/prometheus-net.sln index 26c01936..4bd8455c 100644 --- a/prometheus-net.sln +++ b/prometheus-net.sln @@ -55,6 +55,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample.Web.NetFramework", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.Console.NoAspNetCore", "Sample.Console.NoAspNetCore\Sample.Console.NoAspNetCore.csproj", "{673B5DEC-8530-4226-B923-9D97A6AC29DB}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample.NetStandard", "Sample.NetStandard\Sample.NetStandard.csproj", "{4F91DAA7-9DD3-418F-A276-84ABFED388A5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -151,6 +153,10 @@ Global {673B5DEC-8530-4226-B923-9D97A6AC29DB}.Debug|Any CPU.Build.0 = Debug|Any CPU {673B5DEC-8530-4226-B923-9D97A6AC29DB}.Release|Any CPU.ActiveCfg = Release|Any CPU {673B5DEC-8530-4226-B923-9D97A6AC29DB}.Release|Any CPU.Build.0 = Release|Any CPU + {4F91DAA7-9DD3-418F-A276-84ABFED388A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4F91DAA7-9DD3-418F-A276-84ABFED388A5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4F91DAA7-9DD3-418F-A276-84ABFED388A5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4F91DAA7-9DD3-418F-A276-84ABFED388A5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 9c9e1e948008760ae2425f157a73c9b2729dc2e2 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Mon, 7 Nov 2022 08:58:45 +0200 Subject: [PATCH 004/230] Reduce Microsoft.Extensions.Http dependency version for better backcompat --- Prometheus/Prometheus.csproj | 2 +- Resources/Nuspec/prometheus-net.nuspec | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Prometheus/Prometheus.csproj b/Prometheus/Prometheus.csproj index a6b5d3ad..ed53d944 100644 --- a/Prometheus/Prometheus.csproj +++ b/Prometheus/Prometheus.csproj @@ -44,7 +44,7 @@ - + diff --git a/Resources/Nuspec/prometheus-net.nuspec b/Resources/Nuspec/prometheus-net.nuspec index d70a506f..2d2fd5d5 100644 --- a/Resources/Nuspec/prometheus-net.nuspec +++ b/Resources/Nuspec/prometheus-net.nuspec @@ -17,15 +17,15 @@ - + - + - + From 9163723a83bd0491d6bde62d08d24272522ecac2 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Tue, 15 Nov 2022 16:53:46 +0200 Subject: [PATCH 005/230] Added (Observable)UpDownCounter support to MeterAdapter (.NET 7 specific feature). --- History | 1 + Prometheus/MeterAdapter.cs | 32 +++++++++---------- Prometheus/Prometheus.csproj | 7 ++-- Resources/Nuspec/prometheus-net.nuspec | 5 +++ .../CustomDotNetMeters.cs | 7 ++-- .../Sample.Console.DotNetMeters.csproj | 2 +- 6 files changed, 30 insertions(+), 24 deletions(-) diff --git a/History b/History index 0fc39c19..654678bf 100644 --- a/History +++ b/History @@ -1,5 +1,6 @@ * 7.1.0 - Added back .NET Standard 2.0 support as some customers had a hard dependency on .NET Standard 2.0 (despite not being a documented feature even earlier). +- Added (Observable)UpDownCounter support to MeterAdapter (.NET 7 specific feature). * 7.0.0 - .NET Core specific functionality now targeting .NET 6.0 or greater (all previous versions will be end of life by December 2022). - Relaxed the API restriction that forbade you to create multiple metrics with the same name but different label names. While this is a Prometheus anti-pattern, it is a standard pattern in other metrics technologies and we will allow it in the name of interoperability. diff --git a/Prometheus/MeterAdapter.cs b/Prometheus/MeterAdapter.cs index 8862caa3..73e60077 100644 --- a/Prometheus/MeterAdapter.cs +++ b/Prometheus/MeterAdapter.cs @@ -129,34 +129,34 @@ private void OnMeasurementRecorded(Instrument instrument, T measurement, Read var handle = _factory.CreateGauge(_instrumentPrometheusNames[instrument], _instrumentPrometheusHelp[instrument], labelNames); // A measurement is the increment. - handle.WithLease(c => c.Inc(value), labelValues); + handle.WithLease(x => x.Inc(value), labelValues); } else if (instrument is ObservableCounter) { var handle = _factory.CreateGauge(_instrumentPrometheusNames[instrument], _instrumentPrometheusHelp[instrument], labelNames); // A measurement is the current value. - handle.WithLease(c => c.IncTo(value), labelValues); + handle.WithLease(x => x.IncTo(value), labelValues); } - /* .NET 7: else if (instrument is UpDownCounter) +#if NET7_0_OR_GREATER + else if (instrument is UpDownCounter) { - var handle = _factory.CreateGauge(_instrumentPrometheusNames[instrument], _instrumentPrometheusHelp[instrument], new GaugeConfiguration - { - LabelNames = labelNames - }); + var handle = _factory.CreateGauge(_instrumentPrometheusNames[instrument], _instrumentPrometheusHelp[instrument], labelNames); - using (handle.AcquireLease(out var gauge, labelValues)) - { - // A measurement is the increment. - gauge.Inc(value); - } - }*/ - else if (instrument is ObservableGauge /* .NET 7: or ObservableUpDownCounter*/) + // A measurement is the increment. + handle.WithLease(x => x.Inc(value)); + } +#endif + else if (instrument is ObservableGauge +#if NET7_0_OR_GREATER + or ObservableUpDownCounter +#endif + ) { var handle = _factory.CreateGauge(_instrumentPrometheusNames[instrument], _instrumentPrometheusHelp[instrument], labelNames); // A measurement is the current value. - handle.WithLease(g => g.Set(value), labelValues); + handle.WithLease(x => x.Set(value), labelValues); } else if (instrument is Histogram) { @@ -167,7 +167,7 @@ private void OnMeasurementRecorded(Instrument instrument, T measurement, Read }); // A measurement is the observed value. - handle.WithLease(h => h.Observe(value), labelValues); + handle.WithLease(x => x.Observe(value), labelValues); } else { diff --git a/Prometheus/Prometheus.csproj b/Prometheus/Prometheus.csproj index ed53d944..6f0817c5 100644 --- a/Prometheus/Prometheus.csproj +++ b/Prometheus/Prometheus.csproj @@ -1,10 +1,10 @@  - net6.0;netstandard2.0 + net6.0;net7.0;netstandard2.0 - net462;net6.0;netstandard2.0 + net462;net6.0;net7.0;netstandard2.0 @@ -27,8 +27,7 @@ True 1591 - - preview + latest 9999 True diff --git a/Resources/Nuspec/prometheus-net.nuspec b/Resources/Nuspec/prometheus-net.nuspec index 2d2fd5d5..74bf9f5a 100644 --- a/Resources/Nuspec/prometheus-net.nuspec +++ b/Resources/Nuspec/prometheus-net.nuspec @@ -24,6 +24,10 @@ + + + + @@ -33,6 +37,7 @@ + diff --git a/Sample.Console.DotNetMeters/CustomDotNetMeters.cs b/Sample.Console.DotNetMeters/CustomDotNetMeters.cs index 86b5e00e..ed620a18 100644 --- a/Sample.Console.DotNetMeters/CustomDotNetMeters.cs +++ b/Sample.Console.DotNetMeters/CustomDotNetMeters.cs @@ -32,7 +32,7 @@ IEnumerable> ObserveGrossNestsAll() var histogram1 = meter1.CreateHistogram("bytes-considered", "bytes", "Informs about all the bytes considered."); // .NET 7: Example metric: an up/down counter. - /*var upDown1 = meter1.CreateUpDownCounter("water-level", "brick-heights", "Current water level in the tank (measured in visible bricks from the midpoint)."); + var upDown1 = meter1.CreateUpDownCounter("water-level", "brick-heights", "Current water level in the tank (measured in visible bricks from the midpoint)."); // Example metric: an observable up/down counter. int sandLevel = 0; @@ -43,7 +43,7 @@ int MeasureSandLevel() return sandLevel; } - var upDown2 = meter1.CreateObservableUpDownCounter("sand-level", MeasureSandLevel, "chainlinks", "Current sand level in the tank (measured in visible chain links from the midpoint).");*/ + var upDown2 = meter1.CreateObservableUpDownCounter("sand-level", MeasureSandLevel, "chainlinks", "Current sand level in the tank (measured in visible chain links from the midpoint)."); // Example high cardinality metric: bytes sent per connection. var highCardinalityCounter1 = meter1.CreateCounter("bytes-sent", "bytes", "Bytes sent per connection."); @@ -67,7 +67,8 @@ int MeasureSandLevel() histogram1.Record((byte)(Random.Shared.Next(256)), new KeyValuePair("is-faulted", true)); - // .NET 7: upDown1.Add(Random.Shared.Next(-1, 2)); + // .NET 7 + upDown1.Add(Random.Shared.Next(-1, 2)); // Add some bytes for every active connection. foreach (var connection in activeConnections) diff --git a/Sample.Console.DotNetMeters/Sample.Console.DotNetMeters.csproj b/Sample.Console.DotNetMeters/Sample.Console.DotNetMeters.csproj index 4fb63234..0ae9263f 100644 --- a/Sample.Console.DotNetMeters/Sample.Console.DotNetMeters.csproj +++ b/Sample.Console.DotNetMeters/Sample.Console.DotNetMeters.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net7.0 enable enable From 7239218c93f5f91ee9a751a69ce8375ed329415d Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Tue, 15 Nov 2022 17:06:09 +0200 Subject: [PATCH 006/230] Even with SDK 7 build pipeline requires langversion preview --- Prometheus/Prometheus.csproj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Prometheus/Prometheus.csproj b/Prometheus/Prometheus.csproj index 6f0817c5..30f0cfe5 100644 --- a/Prometheus/Prometheus.csproj +++ b/Prometheus/Prometheus.csproj @@ -27,7 +27,8 @@ True 1591 - latest + + preview 9999 True From c80b4649d9961129a10ace0823ecd01d22aea438 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Tue, 15 Nov 2022 17:34:23 +0200 Subject: [PATCH 007/230] Temporarily disable .NET Framework web project builds because build pipeline agents do not have the right version of Visual Studio installed. Re-enable once pipeline agents have 17.4. --- prometheus-net.sln | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/prometheus-net.sln b/prometheus-net.sln index 4bd8455c..8679b40a 100644 --- a/prometheus-net.sln +++ b/prometheus-net.sln @@ -55,7 +55,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample.Web.NetFramework", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.Console.NoAspNetCore", "Sample.Console.NoAspNetCore\Sample.Console.NoAspNetCore.csproj", "{673B5DEC-8530-4226-B923-9D97A6AC29DB}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample.NetStandard", "Sample.NetStandard\Sample.NetStandard.csproj", "{4F91DAA7-9DD3-418F-A276-84ABFED388A5}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.NetStandard", "Sample.NetStandard\Sample.NetStandard.csproj", "{4F91DAA7-9DD3-418F-A276-84ABFED388A5}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -96,7 +96,6 @@ Global {1C13A89A-563E-4C4C-83F9-0DCCCFFDFA86}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1C13A89A-563E-4C4C-83F9-0DCCCFFDFA86}.Debug|Any CPU.Build.0 = Debug|Any CPU {1C13A89A-563E-4C4C-83F9-0DCCCFFDFA86}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1C13A89A-563E-4C4C-83F9-0DCCCFFDFA86}.Release|Any CPU.Build.0 = Release|Any CPU {4B630AF4-71AD-4CF1-AD4E-D0608B426109}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4B630AF4-71AD-4CF1-AD4E-D0608B426109}.Debug|Any CPU.Build.0 = Debug|Any CPU {4B630AF4-71AD-4CF1-AD4E-D0608B426109}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -148,7 +147,6 @@ Global {35C90741-497A-465C-A018-633FAD414D0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {35C90741-497A-465C-A018-633FAD414D0D}.Debug|Any CPU.Build.0 = Debug|Any CPU {35C90741-497A-465C-A018-633FAD414D0D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {35C90741-497A-465C-A018-633FAD414D0D}.Release|Any CPU.Build.0 = Release|Any CPU {673B5DEC-8530-4226-B923-9D97A6AC29DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {673B5DEC-8530-4226-B923-9D97A6AC29DB}.Debug|Any CPU.Build.0 = Debug|Any CPU {673B5DEC-8530-4226-B923-9D97A6AC29DB}.Release|Any CPU.ActiveCfg = Release|Any CPU From 0479f538336aa3f99e8048cc0ca905661e2050ad Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Tue, 15 Nov 2022 17:41:48 +0200 Subject: [PATCH 008/230] Move tests to .NET 7 --- Tests.NetCore/Tests.NetCore.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests.NetCore/Tests.NetCore.csproj b/Tests.NetCore/Tests.NetCore.csproj index 71d24f2d..9b6cf69c 100644 --- a/Tests.NetCore/Tests.NetCore.csproj +++ b/Tests.NetCore/Tests.NetCore.csproj @@ -1,7 +1,7 @@  - net6.0 + net7.0 false From 8ea859b95f1f650e760e5ab7e7419e2d59b4a8e3 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Wed, 16 Nov 2022 07:53:24 +0200 Subject: [PATCH 009/230] Add AspNetCoreExporterBenchmarks --- Benchmark.NetCore/Benchmark.NetCore.csproj | 1 + .../KestrelExporterBenchmarks.cs | 68 +++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 Benchmark.NetCore/KestrelExporterBenchmarks.cs diff --git a/Benchmark.NetCore/Benchmark.NetCore.csproj b/Benchmark.NetCore/Benchmark.NetCore.csproj index 1279301f..57baef3e 100644 --- a/Benchmark.NetCore/Benchmark.NetCore.csproj +++ b/Benchmark.NetCore/Benchmark.NetCore.csproj @@ -25,6 +25,7 @@ + diff --git a/Benchmark.NetCore/KestrelExporterBenchmarks.cs b/Benchmark.NetCore/KestrelExporterBenchmarks.cs new file mode 100644 index 00000000..541322bf --- /dev/null +++ b/Benchmark.NetCore/KestrelExporterBenchmarks.cs @@ -0,0 +1,68 @@ +using BenchmarkDotNet.Attributes; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Prometheus; + +namespace Benchmark.NetCore; + +/// +/// We start up a real(ish) ASP.NET Core web server stack to measure exporter performance end to end. +/// +[MemoryDiagnoser] +public class AspNetCoreExporterBenchmarks +{ + static AspNetCoreExporterBenchmarks() + { + // We use the global singleton metrics registry here, so just populate it with some data. + for (var i = 0; i < 1000; i++) + Metrics.CreateGauge("dummy_metric_" + i, "For benchmark purposes", "label1", "label2", "label3").WithLabels("1", "2", "3").Inc(); + } + + [GlobalSetup] + public void Setup() + { + var builder = new WebHostBuilder().UseStartup(); + _server = new TestServer(builder); + _client = _server.CreateClient(); + + // Warmup the ASP.NET Core stack to avoid measuring the ASP.NET Core web server itself in benchmarks. + _client.GetAsync("/metrics").GetAwaiter().GetResult(); + } + + [GlobalCleanup] + public void Cleanup() + { + _client?.Dispose(); + _server?.Dispose(); + } + + private TestServer _server; + private HttpClient _client; + + private sealed class EntryPoint + { + public void ConfigureServices(IServiceCollection services) + { + services.AddRouting(); + } + + public void Configure(IApplicationBuilder app) + { + app.UseRouting(); + app.UseHttpMetrics(); + + app.UseEndpoints(endpoints => + { + endpoints.MapMetrics(); + }); + } + } + + [Benchmark] + public async Task GetMetrics() + { + await _client.GetAsync("/metrics"); + } +} From 43b8625cc11a65430c72113242bf21507999771c Mon Sep 17 00:00:00 2001 From: Hassan Syed <91477794+hsyed-dojo@users.noreply.github.com> Date: Wed, 16 Nov 2022 13:29:56 +0000 Subject: [PATCH 010/230] Openmetrics refactor and tests (#385) Signed-off-by: Hassan Syed --- Prometheus/CanonicalLabel.cs | 21 ++ Prometheus/ChildBase.cs | 36 +--- Prometheus/Collector.cs | 9 +- Prometheus/Counter.cs | 12 +- Prometheus/Gauge.cs | 8 +- Prometheus/Histogram.cs | 60 +++--- Prometheus/IMetricsSerializer.cs | 8 +- Prometheus/Summary.cs | 53 ++--- Prometheus/TextSerializer.cs | 92 ++++++++- Tests.NetCore/HistogramTests.cs | 15 +- Tests.NetCore/MetricInitializationTests.cs | 24 +-- Tests.NetCore/MetricsTests.cs | 20 +- Tests.NetCore/TextSerializerTests.cs | 195 +++++++++++++++++++ Tests.NetFramework/tests.netframework.csproj | 3 + 14 files changed, 427 insertions(+), 129 deletions(-) create mode 100644 Prometheus/CanonicalLabel.cs create mode 100644 Tests.NetCore/TextSerializerTests.cs diff --git a/Prometheus/CanonicalLabel.cs b/Prometheus/CanonicalLabel.cs new file mode 100644 index 00000000..3d88b57f --- /dev/null +++ b/Prometheus/CanonicalLabel.cs @@ -0,0 +1,21 @@ +namespace Prometheus; + +internal readonly struct CanonicalLabel +{ + public static readonly CanonicalLabel Empty = new( + Array.Empty(), Array.Empty(), Array.Empty()); + + public CanonicalLabel(byte[] name, byte[] prometheus, byte[] openMetrics) + { + Prometheus = prometheus; + OpenMetrics = openMetrics; + Name = name; + } + + public byte[] Name { get; } + + public byte[] Prometheus { get; } + public byte[] OpenMetrics { get; } + + public bool IsNotEmpty => Name.Length > 0; +} \ No newline at end of file diff --git a/Prometheus/ChildBase.cs b/Prometheus/ChildBase.cs index 33e96a6a..f54726cd 100644 --- a/Prometheus/ChildBase.cs +++ b/Prometheus/ChildBase.cs @@ -7,9 +7,10 @@ public abstract class ChildBase : ICollectorChild, IDisposable { internal ChildBase(Collector parent, LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish) { - _parent = parent; + Parent = parent; InstanceLabels = instanceLabels; FlattenedLabels = flattenedLabels; + FlattenedLabelsBytes = PrometheusConstants.ExportEncoding.GetBytes(flattenedLabels.Serialize()); _publish = publish; } @@ -43,7 +44,7 @@ public void Unpublish() /// public void Remove() { - _parent.RemoveLabelled(InstanceLabels); + Parent.RemoveLabelled(InstanceLabels); } public void Dispose() => Remove(); @@ -60,7 +61,9 @@ public void Remove() /// internal LabelSequence FlattenedLabels { get; } - private readonly Collector _parent; + internal byte[] FlattenedLabelsBytes { get; } + + internal readonly Collector Parent; private bool _publish; @@ -80,32 +83,5 @@ internal Task CollectAndSerializeAsync(IMetricsSerializer serializer, Cancellati // Same as above, just only called if we really need to serialize this metric (if publish is true). private protected abstract Task CollectAndSerializeImplAsync(IMetricsSerializer serializer, CancellationToken cancel); - - /// - /// Creates a metric identifier, with an optional name postfix and an optional extra label to append to the end. - /// familyname_postfix{labelkey1="labelvalue1",labelkey2="labelvalue2"} - /// - protected byte[] CreateIdentifier(string? postfix = null, string? extraLabelName = null, string? extraLabelValue = null) - { - var fullName = postfix != null ? $"{_parent.Name}_{postfix}" : _parent.Name; - - var labels = FlattenedLabels; - - if (extraLabelName != null && extraLabelValue != null) - { - var extraLabelNames = StringSequence.From(extraLabelName); - var extraLabelValues = StringSequence.From(extraLabelValue); - - var extraLabels = LabelSequence.From(extraLabelNames, extraLabelValues); - - // Extra labels go to the end (i.e. they are deepest to inherit from). - labels = labels.Concat(extraLabels); - } - - if (labels.Length != 0) - return PrometheusConstants.ExportEncoding.GetBytes($"{fullName}{{{labels.Serialize()}}}"); - else - return PrometheusConstants.ExportEncoding.GetBytes(fullName); - } } } \ No newline at end of file diff --git a/Prometheus/Collector.cs b/Prometheus/Collector.cs index 533237ff..5378f7c7 100644 --- a/Prometheus/Collector.cs +++ b/Prometheus/Collector.cs @@ -13,6 +13,8 @@ public abstract class Collector /// The metric name, e.g. http_requests_total. /// public string Name { get; } + + internal byte[] NameBytes { get; } /// /// The help text describing the metric for a human audience. @@ -57,8 +59,9 @@ internal Collector(string name, string help, StringSequence instanceLabelNames, { if (!MetricNameRegex.IsMatch(name)) throw new ArgumentException($"Metric name '{name}' does not match regex '{ValidMetricNameExpression}'."); - + Name = name; + NameBytes = PrometheusConstants.ExportEncoding.GetBytes(Name); Help = help; InstanceLabelNames = instanceLabelNames; StaticLabels = staticLabels; @@ -224,7 +227,9 @@ internal Collector(string name, string help, StringSequence instanceLabelNames, _familyHeaderLines = new byte[][] { - PrometheusConstants.ExportEncoding.GetBytes($"# HELP {name} {help}"), + string.IsNullOrWhiteSpace(help) + ? PrometheusConstants.ExportEncoding.GetBytes($"# HELP {name}") + : PrometheusConstants.ExportEncoding.GetBytes($"# HELP {name} {help}"), PrometheusConstants.ExportEncoding.GetBytes($"# TYPE {name} {Type.ToString().ToLowerInvariant()}") }; } diff --git a/Prometheus/Counter.cs b/Prometheus/Counter.cs index e35c477a..a02ad097 100644 --- a/Prometheus/Counter.cs +++ b/Prometheus/Counter.cs @@ -7,16 +7,18 @@ public sealed class Child : ChildBase, ICounter internal Child(Collector parent, LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish) : base(parent, instanceLabels, flattenedLabels, publish) { - _identifier = CreateIdentifier(); } - private readonly byte[] _identifier; - private ThreadSafeDouble _value; - private protected override Task CollectAndSerializeImplAsync(IMetricsSerializer serializer, CancellationToken cancel) + private protected override async Task CollectAndSerializeImplAsync(IMetricsSerializer serializer, CancellationToken cancel) { - return serializer.WriteMetricAsync(_identifier, Value, cancel); + await serializer.WriteMetricPointAsync( + Parent.NameBytes, + FlattenedLabelsBytes, + CanonicalLabel.Empty, + cancel, + Value); } public void Inc(double increment = 1.0) diff --git a/Prometheus/Gauge.cs b/Prometheus/Gauge.cs index 0bb293ed..04267c4b 100644 --- a/Prometheus/Gauge.cs +++ b/Prometheus/Gauge.cs @@ -7,16 +7,14 @@ public sealed class Child : ChildBase, IGauge internal Child(Collector parent, LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish) : base(parent, instanceLabels, flattenedLabels, publish) { - _identifier = CreateIdentifier(); } - private readonly byte[] _identifier; - private ThreadSafeDouble _value; - private protected override Task CollectAndSerializeImplAsync(IMetricsSerializer serializer, CancellationToken cancel) + private protected override async Task CollectAndSerializeImplAsync(IMetricsSerializer serializer, CancellationToken cancel) { - return serializer.WriteMetricAsync(_identifier, Value, cancel); + await serializer.WriteMetricPointAsync( + Parent.NameBytes, FlattenedLabelsBytes, CanonicalLabel.Empty, cancel, Value); } public void Inc(double increment = 1) diff --git a/Prometheus/Histogram.cs b/Prometheus/Histogram.cs index 3eef3d2e..645c8677 100644 --- a/Prometheus/Histogram.cs +++ b/Prometheus/Histogram.cs @@ -49,49 +49,61 @@ public sealed class Child : ChildBase, IHistogram internal Child(Histogram parent, LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish) : base(parent, instanceLabels, flattenedLabels, publish) { - _parent = parent; + Parent = parent; - _upperBounds = _parent._buckets; + _upperBounds = Parent._buckets; _bucketCounts = new ThreadSafeLong[_upperBounds.Length]; - - _sumIdentifier = CreateIdentifier("sum"); - _countIdentifier = CreateIdentifier("count"); - - _bucketIdentifiers = new byte[_upperBounds.Length][]; - for (var i = 0; i < _upperBounds.Length; i++) + _leLabels = new CanonicalLabel[_upperBounds.Length]; + for (var i = 0; i < Parent._buckets.Length; i++) { - var value = double.IsPositiveInfinity(_upperBounds[i]) ? "+Inf" : _upperBounds[i].ToString(CultureInfo.InvariantCulture); - - _bucketIdentifiers[i] = CreateIdentifier("bucket", "le", value); + _leLabels[i] = TextSerializer.EncodeValueAsCanonicalLabel(LeLabelName, Parent._buckets[i]); } } - private readonly Histogram _parent; + internal new readonly Histogram Parent; private ThreadSafeDouble _sum = new ThreadSafeDouble(0.0D); private readonly ThreadSafeLong[] _bucketCounts; private readonly double[] _upperBounds; - - internal readonly byte[] _sumIdentifier; - internal readonly byte[] _countIdentifier; - internal readonly byte[][] _bucketIdentifiers; - - private protected override async Task CollectAndSerializeImplAsync(IMetricsSerializer serializer, CancellationToken cancel) + private readonly CanonicalLabel[] _leLabels; + private static readonly byte[] SumSuffix = PrometheusConstants.ExportEncoding.GetBytes("sum"); + private static readonly byte[] CountSuffix = PrometheusConstants.ExportEncoding.GetBytes("count"); + private static readonly byte[] BucketSuffix = PrometheusConstants.ExportEncoding.GetBytes("bucket"); + private static readonly byte[] LeLabelName = PrometheusConstants.ExportEncoding.GetBytes("le"); + + private protected override async Task CollectAndSerializeImplAsync(IMetricsSerializer serializer, + CancellationToken cancel) { // We output sum. // We output count. // We output each bucket in order of increasing upper bound. - - await serializer.WriteMetricAsync(_sumIdentifier, _sum.Value, cancel); - await serializer.WriteMetricAsync(_countIdentifier, _bucketCounts.Sum(b => b.Value), cancel); + await serializer.WriteMetricPointAsync( + Parent.NameBytes, + FlattenedLabelsBytes, + CanonicalLabel.Empty, + cancel, + _sum.Value, + suffix: SumSuffix); + await serializer.WriteMetricPointAsync( + Parent.NameBytes, + FlattenedLabelsBytes, + CanonicalLabel.Empty, + cancel, + _bucketCounts.Sum(b => b.Value), + suffix: CountSuffix); var cumulativeCount = 0L; for (var i = 0; i < _bucketCounts.Length; i++) { cumulativeCount += _bucketCounts[i].Value; - - await serializer.WriteMetricAsync(_bucketIdentifiers[i], cumulativeCount, cancel); + await serializer.WriteMetricPointAsync( + Parent.NameBytes, + FlattenedLabelsBytes, + _leLabels[i], + cancel, + cumulativeCount, + suffix: BucketSuffix); } } @@ -251,4 +263,4 @@ public static double[] PowersOfTenDividedBuckets(int startPower, int endPower, i // sum + count + buckets internal override int TimeseriesCount => ChildCount * (2 + _buckets.Length); } -} +} \ No newline at end of file diff --git a/Prometheus/IMetricsSerializer.cs b/Prometheus/IMetricsSerializer.cs index 789a99ef..0dc39a04 100644 --- a/Prometheus/IMetricsSerializer.cs +++ b/Prometheus/IMetricsSerializer.cs @@ -13,13 +13,15 @@ internal interface IMetricsSerializer Task WriteFamilyDeclarationAsync(byte[][] headerLines, CancellationToken cancel); /// - /// Writes a single metric in a metric family. + /// Writes out a single metric point /// - Task WriteMetricAsync(byte[] identifier, double value, CancellationToken cancel); + /// + Task WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, CanonicalLabel canonicalLabel, + CancellationToken cancel, double value, byte[]? suffix = null); /// /// Flushes any pending buffers. Always call this after all your write calls. /// Task FlushAsync(CancellationToken cancel); } -} +} \ No newline at end of file diff --git a/Prometheus/Summary.cs b/Prometheus/Summary.cs index 23ead339..4a85024d 100644 --- a/Prometheus/Summary.cs +++ b/Prometheus/Summary.cs @@ -1,5 +1,4 @@ using Prometheus.SummaryImpl; -using System.Globalization; namespace Prometheus { @@ -14,9 +13,6 @@ public sealed class Summary : Collector, ISummary /// internal static readonly QuantileEpsilonPair[] DefObjectivesArray = new QuantileEpsilonPair[0]; - // Default Summary quantile values. - public static readonly IList DefObjectives = new List(DefObjectivesArray); - // Default duration for which observations stay relevant public static readonly TimeSpan DefMaxAge = TimeSpan.FromMinutes(10); @@ -96,30 +92,24 @@ internal Child(Summary parent, LabelSequence instanceLabels, LabelSequence flatt _headStream = _streams[0]; + _quantileLabels = new CanonicalLabel[_objectives.Count]; for (var i = 0; i < _objectives.Count; i++) { _sortedObjectives[i] = _objectives[i].Quantile; + _quantileLabels[i] = TextSerializer.EncodeValueAsCanonicalLabel( + QuantileLabelName, _objectives[i].Quantile); } Array.Sort(_sortedObjectives); - - _sumIdentifier = CreateIdentifier("sum"); - _countIdentifier = CreateIdentifier("count"); - - _quantileIdentifiers = new byte[_objectives.Count][]; - for (var i = 0; i < _objectives.Count; i++) - { - var value = double.IsPositiveInfinity(_objectives[i].Quantile) ? "+Inf" : _objectives[i].Quantile.ToString(CultureInfo.InvariantCulture); - - _quantileIdentifiers[i] = CreateIdentifier(null, "quantile", value); - } } - private readonly byte[] _sumIdentifier; - private readonly byte[] _countIdentifier; - private readonly byte[][] _quantileIdentifiers; + private readonly CanonicalLabel[] _quantileLabels; + private static readonly byte[] SumSuffix = PrometheusConstants.ExportEncoding.GetBytes("sum"); + private static readonly byte[] CountSuffix = PrometheusConstants.ExportEncoding.GetBytes("count"); + private static readonly byte[] QuantileLabelName = PrometheusConstants.ExportEncoding.GetBytes("quantile"); - private protected override async Task CollectAndSerializeImplAsync(IMetricsSerializer serializer, CancellationToken cancel) + private protected override async Task CollectAndSerializeImplAsync(IMetricsSerializer serializer, + CancellationToken cancel) { // We output sum. // We output count. @@ -152,11 +142,30 @@ private protected override async Task CollectAndSerializeImplAsync(IMetricsSeria } } - await serializer.WriteMetricAsync(_sumIdentifier, sum, cancel); - await serializer.WriteMetricAsync(_countIdentifier, count, cancel); + await serializer.WriteMetricPointAsync( + Parent.NameBytes, + FlattenedLabelsBytes, + CanonicalLabel.Empty, + cancel, + sum, + suffix: SumSuffix); + await serializer.WriteMetricPointAsync( + Parent.NameBytes, + FlattenedLabelsBytes, + CanonicalLabel.Empty, + cancel, + count, + suffix: CountSuffix); for (var i = 0; i < values.Count; i++) - await serializer.WriteMetricAsync(_quantileIdentifiers[i], values[i].value, cancel); + { + await serializer.WriteMetricPointAsync( + Parent.NameBytes, + FlattenedLabelsBytes, + _quantileLabels[i], + cancel, + values[i].value); + } } // Objectives defines the quantile rank estimates with their respective diff --git a/Prometheus/TextSerializer.cs b/Prometheus/TextSerializer.cs index 584ba762..f05ad9ea 100644 --- a/Prometheus/TextSerializer.cs +++ b/Prometheus/TextSerializer.cs @@ -7,8 +7,15 @@ namespace Prometheus /// internal sealed class TextSerializer : IMetricsSerializer { - private static readonly byte[] NewLine = new[] { (byte)'\n' }; - private static readonly byte[] Space = new[] { (byte)' ' }; + private static readonly byte[] NewLine = { (byte)'\n' }; + private static readonly byte[] Quote = { (byte)'"' }; + private static readonly byte[] Equal = { (byte)'=' }; + private static readonly byte[] Comma = { (byte)',' }; + private static readonly byte[] Underscore = { (byte)'_' }; + private static readonly byte[] LeftBrace = { (byte)'{' }; + private static readonly byte[] RightBraceSpace = { (byte)'}', (byte)' ' }; + private static readonly byte[] Space = { (byte)' ' }; + private static readonly byte[] PositiveInfinity = PrometheusConstants.ExportEncoding.GetBytes("+Inf"); public TextSerializer(Stream stream) { @@ -43,25 +50,92 @@ public async Task WriteFamilyDeclarationAsync(byte[][] headerLines, Cancellation } } + public async Task WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, CanonicalLabel canonicalLabel, + CancellationToken cancel, + double value, byte[]? suffix = null) + { + await WriteIdentifierPartAsync(name, flattenedLabels, cancel, canonicalLabel, suffix); + await WriteValuePartAsync(value, cancel); + } + // Reuse a buffer to do the UTF-8 encoding. // Maybe one day also ValueStringBuilder but that would be .NET Core only. // https://github.com/dotnet/corefx/issues/28379 // Size limit guided by https://stackoverflow.com/questions/21146544/what-is-the-maximum-length-of-double-tostringd private readonly byte[] _stringBytesBuffer = new byte[32]; - // name{labelkey1="labelvalue1",labelkey2="labelvalue2"} 123.456 - public async Task WriteMetricAsync(byte[] identifier, double value, CancellationToken cancel) + // 123.456 + // Note: Terminates with a NEWLINE + private async Task WriteValuePartAsync(double value, CancellationToken cancel) { - await _stream.Value.WriteAsync(identifier, 0, identifier.Length, cancel); - await _stream.Value.WriteAsync(Space, 0, Space.Length, cancel); - var valueAsString = value.ToString(CultureInfo.InvariantCulture); - var numBytes = PrometheusConstants.ExportEncoding .GetBytes(valueAsString, 0, valueAsString.Length, _stringBytesBuffer, 0); await _stream.Value.WriteAsync(_stringBytesBuffer, 0, numBytes, cancel); await _stream.Value.WriteAsync(NewLine, 0, NewLine.Length, cancel); } + + /// + /// Creates a metric identifier, with an optional name postfix and an optional extra label to append to the end. + /// familyname_postfix{labelkey1="labelvalue1",labelkey2="labelvalue2"} + /// Note: Terminates with a SPACE + /// + private async Task WriteIdentifierPartAsync(byte[] name, byte[] flattenedLabels, CancellationToken cancel, + CanonicalLabel canonicalLabel, byte[]? suffix = null) + { + await _stream.Value.WriteAsync(name, 0, name.Length, cancel); + if (suffix != null && suffix.Length > 0) + { + await _stream.Value.WriteAsync(Underscore, 0, Underscore.Length, cancel); + await _stream.Value.WriteAsync(suffix, 0, suffix.Length, cancel); + } + + if (flattenedLabels.Length > 0 || canonicalLabel.IsNotEmpty) + { + await _stream.Value.WriteAsync(LeftBrace, 0, LeftBrace.Length, cancel); + if (flattenedLabels.Length > 0) + { + await _stream.Value.WriteAsync(flattenedLabels, 0, flattenedLabels.Length, cancel); + } + + // Extra labels go to the end (i.e. they are deepest to inherit from). + if (canonicalLabel.IsNotEmpty) + { + if (flattenedLabels.Length > 0) + { + await _stream.Value.WriteAsync(Comma, 0, Comma.Length, cancel); + } + + await _stream.Value.WriteAsync(canonicalLabel.Name, 0, canonicalLabel.Name.Length, cancel); + await _stream.Value.WriteAsync(Equal, 0, Equal.Length, cancel); + await _stream.Value.WriteAsync(Quote, 0, Quote.Length, cancel); + await _stream.Value.WriteAsync(canonicalLabel.Prometheus, 0, canonicalLabel.Prometheus.Length, + cancel); + await _stream.Value.WriteAsync(Quote, 0, Quote.Length, cancel); + } + + await _stream.Value.WriteAsync(RightBraceSpace, 0, RightBraceSpace.Length, cancel); + } + else + { + await _stream.Value.WriteAsync(Space, 0, Space.Length, cancel); + } + } + + /// + /// Encode the special variable in regular Prometheus form and also return a OpenMetrics variant, these can be + /// the same. + /// see: https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#considerations-canonical-numbers + /// + internal static CanonicalLabel EncodeValueAsCanonicalLabel(byte[] name, double value) + { + if (double.IsPositiveInfinity(value)) + return new CanonicalLabel(name, PositiveInfinity, PositiveInfinity); + + var valueAsString = value.ToString(CultureInfo.InvariantCulture); + var bytes = PrometheusConstants.ExportEncoding.GetBytes(valueAsString); + return new CanonicalLabel(name, bytes, bytes); + } } -} +} \ No newline at end of file diff --git a/Tests.NetCore/HistogramTests.cs b/Tests.NetCore/HistogramTests.cs index 6edce2c1..57aeadab 100644 --- a/Tests.NetCore/HistogramTests.cs +++ b/Tests.NetCore/HistogramTests.cs @@ -1,4 +1,5 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Threading; +using Microsoft.VisualStudio.TestTools.UnitTesting; using NSubstitute; using System.Threading.Tasks; @@ -30,12 +31,12 @@ public async Task Observe_IncrementsCorrectBucketsAndCountAndSum() // 2.0 // 3.0 // +inf - await serializer.Received().WriteMetricAsync(histogram.Unlabelled._sumIdentifier, 5.0, default); - await serializer.Received().WriteMetricAsync(histogram.Unlabelled._countIdentifier, 2.0, default); - await serializer.Received().WriteMetricAsync(histogram.Unlabelled._bucketIdentifiers[0], 0, default); - await serializer.Received().WriteMetricAsync(histogram.Unlabelled._bucketIdentifiers[1], 1, default); - await serializer.Received().WriteMetricAsync(histogram.Unlabelled._bucketIdentifiers[2], 2, default); - await serializer.Received().WriteMetricAsync(histogram.Unlabelled._bucketIdentifiers[3], 2, default); + await serializer.Received().WriteMetricPointAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(),5.0, Arg.Any()); + await serializer.Received().WriteMetricPointAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(),2.0, Arg.Any()); + await serializer.Received().WriteMetricPointAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(),0, Arg.Any()); + await serializer.Received().WriteMetricPointAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(),1, Arg.Any()); + await serializer.Received().WriteMetricPointAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(),2, Arg.Any()); + await serializer.Received().WriteMetricPointAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(),2, Arg.Any()); } [TestMethod] diff --git a/Tests.NetCore/MetricInitializationTests.cs b/Tests.NetCore/MetricInitializationTests.cs index aa8ae491..bba5b19e 100644 --- a/Tests.NetCore/MetricInitializationTests.cs +++ b/Tests.NetCore/MetricInitializationTests.cs @@ -48,7 +48,7 @@ public async Task CreatingUnlabelledMetric_WithoutObservingAnyData_ExportsImmedi // Without touching any metrics, there should be output for all because default config publishes immediately. await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default); - await serializer.ReceivedWithAnyArgs(9).WriteMetricAsync(default, default, default); + await serializer.ReceivedWithAnyArgs(9).WriteMetricPointAsync(default, default, default, default, default); } [TestMethod] @@ -80,7 +80,7 @@ public async Task CreatingUnlabelledMetric_WithInitialValueSuppression_ExportsNo // There is a family for each of the above, in each family we expect to see 0 metrics. await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default); - await serializer.DidNotReceiveWithAnyArgs().WriteMetricAsync(default, default, default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default,default); } [TestMethod] @@ -117,7 +117,7 @@ public async Task CreatingUnlabelledMetric_WithInitialValueSuppression_ExportsAf // Even though suppressed, they all now have values so should all be published. await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default); - await serializer.ReceivedWithAnyArgs(9).WriteMetricAsync(default, default, default); + await serializer.ReceivedWithAnyArgs(9).WriteMetricPointAsync(default, default, default, default, default); } [TestMethod] @@ -154,7 +154,7 @@ public async Task CreatingUnlabelledMetric_WithInitialValueSuppression_ExportsAf // Even though suppressed, they were all explicitly published. await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default); - await serializer.ReceivedWithAnyArgs(9).WriteMetricAsync(default, default, default); + await serializer.ReceivedWithAnyArgs(9).WriteMetricPointAsync(default, default, default, default, default); } #endregion @@ -180,7 +180,7 @@ public async Task CreatingLabelledMetric_WithoutObservingAnyData_ExportsImmediat // Metrics are published as soon as label values are defined. await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default); - await serializer.ReceivedWithAnyArgs(9).WriteMetricAsync(default, default, default); + await serializer.ReceivedWithAnyArgs(9).WriteMetricPointAsync(default, default, default, default, default);; } [TestMethod] @@ -212,7 +212,7 @@ public async Task CreatingLabelledMetric_WithInitialValueSuppression_ExportsNoth // Publishing was suppressed. await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default); - await serializer.DidNotReceiveWithAnyArgs().WriteMetricAsync(default, default, default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default,default); } [TestMethod] @@ -249,7 +249,7 @@ public async Task CreatingLabelledMetric_WithInitialValueSuppression_ExportsAfte // Metrics are published because value was set. await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default); - await serializer.ReceivedWithAnyArgs(9).WriteMetricAsync(default, default, default); + await serializer.ReceivedWithAnyArgs(9).WriteMetricPointAsync(default, default, default, default, default); } [TestMethod] @@ -286,7 +286,7 @@ public async Task CreatingLabelledMetric_WithInitialValueSuppression_ExportsAfte // Metrics are published because of explicit publish. await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default); - await serializer.ReceivedWithAnyArgs(9).WriteMetricAsync(default, default, default); + await serializer.ReceivedWithAnyArgs(9).WriteMetricPointAsync(default, default, default, default, default); } [TestMethod] @@ -304,7 +304,7 @@ public async Task CreatingLabelledMetric_AndUnpublishingAfterObservingData_DoesN await registry.CollectAndSerializeAsync(serializer, default); await serializer.ReceivedWithAnyArgs(1).WriteFamilyDeclarationAsync(default, default); - await serializer.ReceivedWithAnyArgs(0).WriteMetricAsync(default, default, default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default,default); } #endregion @@ -329,7 +329,7 @@ public async Task CreatingLabelledMetric_WithoutObservingAnyData_DoesNotExportUn // Family for each of the above, in each is 0 metrics. await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default); - await serializer.DidNotReceiveWithAnyArgs().WriteMetricAsync(default, default, default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default,default); } [TestMethod] @@ -358,7 +358,7 @@ public async Task CreatingLabelledMetric_AfterObservingLabelledData_DoesNotExpor // Family for each of the above, in each is 4 metrics (labelled only). await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default); - await serializer.ReceivedWithAnyArgs(9).WriteMetricAsync(default, default, default); + await serializer.ReceivedWithAnyArgs(9).WriteMetricPointAsync(default, default, default, default, default); // Only after touching unlabelled do they get published. gauge.Inc(); @@ -371,7 +371,7 @@ public async Task CreatingLabelledMetric_AfterObservingLabelledData_DoesNotExpor // Family for each of the above, in each is 8 metrics (unlabelled+labelled). await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default); - await serializer.ReceivedWithAnyArgs(9 * 2).WriteMetricAsync(default, default, default); + await serializer.ReceivedWithAnyArgs(18).WriteMetricPointAsync(default, default, default, default, default); } #endregion diff --git a/Tests.NetCore/MetricsTests.cs b/Tests.NetCore/MetricsTests.cs index 22669383..fe2d1e35 100644 --- a/Tests.NetCore/MetricsTests.cs +++ b/Tests.NetCore/MetricsTests.cs @@ -71,10 +71,10 @@ public async Task CreateCounter_WithDifferentRegistry_CreatesIndependentCounters await registry2.CollectAndSerializeAsync(serializer2, default); await serializer1.ReceivedWithAnyArgs().WriteFamilyDeclarationAsync(default, default); - await serializer1.ReceivedWithAnyArgs().WriteMetricAsync(default, default, default); + await serializer1.ReceivedWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default); await serializer2.ReceivedWithAnyArgs().WriteFamilyDeclarationAsync(default, default); - await serializer2.ReceivedWithAnyArgs().WriteMetricAsync(default, default, default); + await serializer2.ReceivedWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default); } [TestMethod] @@ -90,7 +90,7 @@ public async Task Export_FamilyWithOnlyNonpublishedUnlabeledMetrics_ExportsFamil await _registry.CollectAndSerializeAsync(serializer, default); await serializer.ReceivedWithAnyArgs(1).WriteFamilyDeclarationAsync(default, default); - await serializer.DidNotReceiveWithAnyArgs().WriteMetricAsync(default, default, default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default,default); serializer.ClearReceivedCalls(); metric.Inc(); @@ -99,7 +99,7 @@ public async Task Export_FamilyWithOnlyNonpublishedUnlabeledMetrics_ExportsFamil await _registry.CollectAndSerializeAsync(serializer, default); await serializer.ReceivedWithAnyArgs(1).WriteFamilyDeclarationAsync(default, default); - await serializer.DidNotReceiveWithAnyArgs().WriteMetricAsync(default, default, default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default,default); } [TestMethod] @@ -117,7 +117,7 @@ public async Task Export_FamilyWithOnlyNonpublishedLabeledMetrics_ExportsFamilyD await _registry.CollectAndSerializeAsync(serializer, default); await serializer.ReceivedWithAnyArgs(1).WriteFamilyDeclarationAsync(default, default); - await serializer.DidNotReceiveWithAnyArgs().WriteMetricAsync(default, default, default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default,default); serializer.ClearReceivedCalls(); instance.Inc(); @@ -126,7 +126,7 @@ public async Task Export_FamilyWithOnlyNonpublishedLabeledMetrics_ExportsFamilyD await _registry.CollectAndSerializeAsync(serializer, default); await serializer.ReceivedWithAnyArgs(1).WriteFamilyDeclarationAsync(default, default); - await serializer.DidNotReceiveWithAnyArgs().WriteMetricAsync(default, default, default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default,default); } [TestMethod] @@ -144,15 +144,15 @@ public async Task DisposeChild_RemovesMetric() await _registry.CollectAndSerializeAsync(serializer, default); await serializer.ReceivedWithAnyArgs(1).WriteFamilyDeclarationAsync(default, default); - await serializer.ReceivedWithAnyArgs(1).WriteMetricAsync(default, default, default); + await serializer.ReceivedWithAnyArgs(1).WriteMetricPointAsync(default, default, default, default, default); serializer.ClearReceivedCalls(); instance.Dispose(); - + await _registry.CollectAndSerializeAsync(serializer, default); - + await serializer.ReceivedWithAnyArgs(1).WriteFamilyDeclarationAsync(default, default); - await serializer.DidNotReceiveWithAnyArgs().WriteMetricAsync(default, default, default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default); } [TestMethod] diff --git a/Tests.NetCore/TextSerializerTests.cs b/Tests.NetCore/TextSerializerTests.cs new file mode 100644 index 00000000..08a63aca --- /dev/null +++ b/Tests.NetCore/TextSerializerTests.cs @@ -0,0 +1,195 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Prometheus.Tests; + +[TestClass] +public class TextSerializerTests +{ + [TestMethod] + public async Task ValidateTextFmtSummaryExposition_Labels() + { + var result = await TestCase.Run(factory => + { + var summary = factory.CreateSummary("boom_bam", "", new SummaryConfiguration + { + LabelNames = new[] { "blah" }, + Objectives = new[] + { + new QuantileEpsilonPair(0.5, 0.05), + } + }); + + summary.WithLabels("foo").Observe(3); + }); + + result.ShouldBe(@"# HELP boom_bam +# TYPE boom_bam summary +boom_bam_sum{blah=""foo""} 3 +boom_bam_count{blah=""foo""} 1 +boom_bam{blah=""foo"",quantile=""0.5""} 3 +"); + } + + [TestMethod] + public async Task ValidateTextFmtSummaryExposition_NoLabels() + { + var result = await TestCase.Run(factory => + { + var summary = factory.CreateSummary("boom_bam", "something", new SummaryConfiguration + { + Objectives = new[] + { + new QuantileEpsilonPair(0.5, 0.05), + } + }); + summary.Observe(3); + }); + + result.ShouldBe(@"# HELP boom_bam something +# TYPE boom_bam summary +boom_bam_sum 3 +boom_bam_count 1 +boom_bam{quantile=""0.5""} 3 +"); + } + + + [TestMethod] + public async Task ValidateTextFmtGaugeExposition_Labels() + { + var result = await TestCase.Run(factory => + { + var gauge = factory.CreateGauge("boom_bam", "", new GaugeConfiguration + { + LabelNames = new[] { "blah" } + }); + + gauge.WithLabels("foo").IncTo(10); + }); + + result.ShouldBe(@"# HELP boom_bam +# TYPE boom_bam gauge +boom_bam{blah=""foo""} 10 +"); + } + + [TestMethod] + public async Task ValidateTextFmtCounterExposition_Labels() + { + var result = await TestCase.Run(factory => + { + var counter = factory.CreateCounter("boom_bam", "", new CounterConfiguration + { + LabelNames = new[] { "blah" } + }); + + counter.WithLabels("foo").IncTo(10); + }); + + result.ShouldBe("# HELP boom_bam\n" + + "# TYPE boom_bam counter\n" + + "boom_bam{blah=\"foo\"} 10\n"); + } + + [TestMethod] + public async Task ValidateTextFmtHistogramExposition_Labels() + { + var result = await TestCase.Run(factory => + { + var counter = factory.CreateHistogram("boom_bam", "", new HistogramConfiguration + { + LabelNames = new[] { "blah" }, + Buckets = new[] { 1.0 } + }); + + counter.WithLabels("foo").Observe(0.5); + }); + + result.ShouldBe(@"# HELP boom_bam +# TYPE boom_bam histogram +boom_bam_sum{blah=""foo""} 0.5 +boom_bam_count{blah=""foo""} 1 +boom_bam_bucket{blah=""foo"",le=""1""} 1 +boom_bam_bucket{blah=""foo"",le=""+Inf""} 1 +"); + } + + [TestMethod] + public async Task ValidateTextFmtHistogramExposition_NoLabels() + { + var result = await TestCase.Run(factory => + { + var counter = factory.CreateHistogram("boom_bam", "something", new HistogramConfiguration + { + Buckets = new[] { 1.0, 2 } + }); + + counter.Observe(0.5); + }); + + result.ShouldBe(@"# HELP boom_bam something +# TYPE boom_bam histogram +boom_bam_sum 0.5 +boom_bam_count 1 +boom_bam_bucket{le=""1""} 1 +boom_bam_bucket{le=""2""} 1 +boom_bam_bucket{le=""+Inf""} 1 +"); + } + + + private class TestCase + { + private readonly String raw; + private readonly List lines; + + private TestCase(List lines, string raw) + { + this.lines = lines; + this.raw = raw; + } + + + public static async Task Run(Action register) + { + var registry = Metrics.NewCustomRegistry(); + var factory = Metrics.WithCustomRegistry(registry); + + register(factory); + + using var stream = new MemoryStream(); + await registry.CollectAndExportAsTextAsync(stream); + + var lines = new List(); + stream.Position = 0; + var raw = new StreamReader(stream).ReadToEnd(); + stream.Position = 0; + + using StreamReader reader = new StreamReader(stream); + while (!reader.EndOfStream) + { + lines.Add(reader.ReadLine()); + } + + return new TestCase(lines, raw); + } + + public void DumpExposition() + { + foreach (var line in lines) + { + Console.WriteLine(line); + } + } + + public void ShouldBe(string expected) + { + expected = expected.Replace("\r", ""); + Assert.AreEqual(expected, raw); + } + } +} \ No newline at end of file diff --git a/Tests.NetFramework/tests.netframework.csproj b/Tests.NetFramework/tests.netframework.csproj index 78c0cc0f..a5d59337 100644 --- a/Tests.NetFramework/tests.netframework.csproj +++ b/Tests.NetFramework/tests.netframework.csproj @@ -156,6 +156,9 @@ TestExtensions.cs + + TextSerializerTests.cs + ThreadSafeDoubleTests.cs From 7eec46bbe81f5eb425e1ab9e544a9137672996df Mon Sep 17 00:00:00 2001 From: Denis Date: Wed, 30 Nov 2022 09:43:29 +0300 Subject: [PATCH 011/230] fix MeterAdapter System.FormatException: Input string was not in a correct format. (#390) Co-authored-by: Denis Porotikov --- Prometheus/MeterAdapter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Prometheus/MeterAdapter.cs b/Prometheus/MeterAdapter.cs index 73e60077..87dc960a 100644 --- a/Prometheus/MeterAdapter.cs +++ b/Prometheus/MeterAdapter.cs @@ -264,7 +264,7 @@ private static string TranslateInstrumentDescriptionToPrometheusHelp(Instrument var sb = new StringBuilder(); if (!string.IsNullOrWhiteSpace(instrument.Unit)) - sb.AppendFormat($"({instrument.Unit}) "); + sb.Append($"({instrument.Unit}) "); sb.Append(instrument.Description); From 8cdca071261c8f11d05a3e1121e2afb9a74d5f3c Mon Sep 17 00:00:00 2001 From: Hassan Syed <91477794+hsyed-dojo@users.noreply.github.com> Date: Thu, 29 Dec 2022 06:14:30 +0000 Subject: [PATCH 012/230] Openmetrics encoding support and Exemplars. (#388) --- Benchmark.NetCore/MeasurementBenchmarks.cs | 16 +- Benchmark.NetCore/SerializationBenchmarks.cs | 11 +- .../MetricServerMiddleware.cs | 31 +++- .../MetricServerMiddlewareExtensions.cs | 19 ++- .../AspNetMetricServer.cs | 2 +- Prometheus/AutoLeasingCounter.cs | 9 +- Prometheus/AutoLeasingHistogram.cs | 7 +- Prometheus/Collector.cs | 10 +- Prometheus/CollectorRegistry.cs | 6 +- Prometheus/Counter.cs | 44 ++++- Prometheus/Exemplar.cs | 74 +++++++++ Prometheus/ExpositionFormats.cs | 13 ++ Prometheus/Gauge.cs | 2 +- Prometheus/Histogram.cs | 36 +++- Prometheus/ICollectorRegistry.cs | 2 +- Prometheus/ICounter.cs | 12 +- Prometheus/IHistogram.cs | 7 + Prometheus/IMetricsSerializer.cs | 10 +- Prometheus/ObservedExemplar.cs | 87 ++++++++++ Prometheus/Prometheus.csproj | 1 + Prometheus/PrometheusConstants.cs | 15 +- Prometheus/Summary.cs | 5 +- Prometheus/TextSerializer.cs | 157 +++++++++++++++--- Sample.Web/Program.cs | 2 +- Sample.Web/SampleService.cs | 12 +- Sample.Web/run_prometheus_scrape.sh | 19 +++ Tests.NetCore/HistogramTests.cs | 42 ++++- Tests.NetCore/MetricInitializationTests.cs | 48 +++--- Tests.NetCore/MetricsTests.cs | 32 ++-- Tests.NetCore/TextSerializerTests.cs | 145 +++++++++++++++- 30 files changed, 757 insertions(+), 119 deletions(-) create mode 100644 Prometheus/Exemplar.cs create mode 100644 Prometheus/ExpositionFormats.cs create mode 100644 Prometheus/ObservedExemplar.cs create mode 100644 Sample.Web/run_prometheus_scrape.sh diff --git a/Benchmark.NetCore/MeasurementBenchmarks.cs b/Benchmark.NetCore/MeasurementBenchmarks.cs index 89cd9f88..1bbc3250 100644 --- a/Benchmark.NetCore/MeasurementBenchmarks.cs +++ b/Benchmark.NetCore/MeasurementBenchmarks.cs @@ -30,6 +30,9 @@ public enum MetricType [Params(MetricType.Counter, MetricType.Gauge, MetricType.Histogram, MetricType.Summary)] public MetricType TargetMetricType { get; set; } + [Params(true, false)] + public bool WithExemplars { get; set; } + private readonly CollectorRegistry _registry; private readonly MetricFactory _factory; @@ -38,6 +41,8 @@ public enum MetricType private readonly Summary.Child _summary; private readonly Histogram.Child _histogram; + private Exemplar.LabelPair[] _exemplars = Array.Empty(); + public MeasurementBenchmarks() { _registry = Metrics.NewCustomRegistry(); @@ -68,6 +73,13 @@ public MeasurementBenchmarks() _histogram = histogramTemplate.WithLabels("label value"); } + [GlobalSetup] + public void GlobalSetup() + { + if (WithExemplars) + _exemplars = new[] { Exemplar.Key("traceID").WithValue("bar"), Exemplar.Key("traceID2").WithValue("foo") }; + } + [IterationSetup] public void Setup() { @@ -121,7 +133,7 @@ private void MeasurementThreadCounter(object state) for (var i = 0; i < MeasurementCount; i++) { - _counter.Inc(); + _counter.Inc(_exemplars); } } @@ -147,7 +159,7 @@ private void MeasurementThreadHistogram(object state) for (var i = 0; i < MeasurementCount; i++) { - _histogram.Observe(i); + _histogram.Observe(i, _exemplars); } } diff --git a/Benchmark.NetCore/SerializationBenchmarks.cs b/Benchmark.NetCore/SerializationBenchmarks.cs index 049accbd..a653d55f 100644 --- a/Benchmark.NetCore/SerializationBenchmarks.cs +++ b/Benchmark.NetCore/SerializationBenchmarks.cs @@ -63,13 +63,14 @@ public SerializationBenchmarks() [GlobalSetup] public void GenerateData() { + var exemplar = Exemplar.Key("traceID").WithValue("bar"); for (var metricIndex = 0; metricIndex < _metricCount; metricIndex++) for (var variantIndex = 0; variantIndex < _variantCount; variantIndex++) { - _counters[metricIndex].Labels(_labelValueRows[metricIndex][variantIndex]).Inc(); + _counters[metricIndex].Labels(_labelValueRows[metricIndex][variantIndex]).Inc(exemplar); _gauges[metricIndex].Labels(_labelValueRows[metricIndex][variantIndex]).Inc(); _summaries[metricIndex].Labels(_labelValueRows[metricIndex][variantIndex]).Observe(variantIndex); - _histograms[metricIndex].Labels(_labelValueRows[metricIndex][variantIndex]).Observe(variantIndex); + _histograms[metricIndex].Labels(_labelValueRows[metricIndex][variantIndex]).Observe(variantIndex, exemplar); } } @@ -78,5 +79,11 @@ public async Task CollectAndSerialize() { await _registry.CollectAndSerializeAsync(new TextSerializer(Stream.Null), default); } + + [Benchmark] + public async Task CollectAndSerializeOpenMetrics() + { + await _registry.CollectAndSerializeAsync(new TextSerializer(Stream.Null, ExpositionFormat.OpenMetricsText), default); + } } } diff --git a/Prometheus.AspNetCore/MetricServerMiddleware.cs b/Prometheus.AspNetCore/MetricServerMiddleware.cs index ac9e12fa..4668c336 100644 --- a/Prometheus.AspNetCore/MetricServerMiddleware.cs +++ b/Prometheus.AspNetCore/MetricServerMiddleware.cs @@ -1,4 +1,6 @@ -using Microsoft.AspNetCore.Http; +using System.Net.Http.Headers; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Formatters; namespace Prometheus { @@ -12,29 +14,50 @@ public sealed class MetricServerMiddleware public MetricServerMiddleware(RequestDelegate next, Settings settings) { _registry = settings.Registry ?? Metrics.DefaultRegistry; + _enableOpenMetrics = settings.EnableOpenMetrics; } public sealed class Settings { public CollectorRegistry? Registry { get; set; } + public bool EnableOpenMetrics { get; set; } = false; } private readonly CollectorRegistry _registry; + private readonly bool _enableOpenMetrics; + private (ExpositionFormat, string) Negotiate(string header) + { + foreach (var candidate in header.Split(',') + .Select(MediaTypeWithQualityHeaderValue.Parse) + .OrderByDescending(mt => mt.Quality.GetValueOrDefault(1))) + if (candidate.MediaType == PrometheusConstants.TextFmtContentType) + { + break; + } + else if (_enableOpenMetrics && candidate.MediaType == PrometheusConstants.OpenMetricsContentType) + { + return (ExpositionFormat.OpenMetricsText, PrometheusConstants.ExporterOpenMetricsContentType); + } + + return (ExpositionFormat.Text, PrometheusConstants.ExporterContentType); + } + public async Task Invoke(HttpContext context) { var response = context.Response; - + try { + var (fmt, contentType) = Negotiate(context.Request.Headers.Accept.ToString()); // We first touch the response.Body only in the callback because touching // it means we can no longer send headers (the status code). var serializer = new TextSerializer(delegate { - response.ContentType = PrometheusConstants.ExporterContentType; + response.ContentType = contentType; response.StatusCode = StatusCodes.Status200OK; return response.Body; - }); + }, fmt: fmt); await _registry.CollectAndSerializeAsync(serializer, context.RequestAborted); } diff --git a/Prometheus.AspNetCore/MetricServerMiddlewareExtensions.cs b/Prometheus.AspNetCore/MetricServerMiddlewareExtensions.cs index 0bdec01e..05da02d0 100644 --- a/Prometheus.AspNetCore/MetricServerMiddlewareExtensions.cs +++ b/Prometheus.AspNetCore/MetricServerMiddlewareExtensions.cs @@ -16,7 +16,8 @@ public static class MetricServerMiddlewareExtensions public static IEndpointConventionBuilder MapMetrics( this IEndpointRouteBuilder endpoints, string pattern = "/metrics", - CollectorRegistry? registry = null + CollectorRegistry? registry = null, + bool enableOpenMetrics = false ) { var pipeline = endpoints @@ -24,6 +25,7 @@ public static IEndpointConventionBuilder MapMetrics( .UseMiddleware( new MetricServerMiddleware.Settings { + EnableOpenMetrics = enableOpenMetrics, Registry = registry } ) @@ -39,13 +41,14 @@ public static IEndpointConventionBuilder MapMetrics( /// The default URL is /metrics, which is a Prometheus convention. /// Use static methods on the class to create your metrics. /// - public static IApplicationBuilder UseMetricServer(this IApplicationBuilder builder, int port, string? url = "/metrics", CollectorRegistry? registry = null) + public static IApplicationBuilder UseMetricServer(this IApplicationBuilder builder, int port, + string? url = "/metrics", CollectorRegistry? registry = null, bool enableOpenMetrics = false) { // If no URL, use root URL. url ??= "/"; return builder - .Map(url, b => b.MapWhen(PortMatches(), b1 => b1.InternalUseMiddleware(registry))); + .Map(url, b => b.MapWhen(PortMatches(), b1 => b1.InternalUseMiddleware(enableOpenMetrics, registry))); Func PortMatches() { @@ -58,19 +61,21 @@ Func PortMatches() /// The default URL is /metrics, which is a Prometheus convention. /// Use static methods on the class to create your metrics. /// - public static IApplicationBuilder UseMetricServer(this IApplicationBuilder builder, string? url = "/metrics", CollectorRegistry? registry = null) + public static IApplicationBuilder UseMetricServer(this IApplicationBuilder builder, string? url = "/metrics", + CollectorRegistry? registry = null, bool enableOpenMetrics = false) { // If there is a URL to map, map it and re-enter without the URL. if (url != null) - return builder.Map(url, b => b.InternalUseMiddleware(registry)); + return builder.Map(url, b => b.InternalUseMiddleware(enableOpenMetrics, registry)); else - return builder.InternalUseMiddleware(registry); + return builder.InternalUseMiddleware(enableOpenMetrics, registry); } - private static IApplicationBuilder InternalUseMiddleware(this IApplicationBuilder builder, CollectorRegistry? registry = null) + private static IApplicationBuilder InternalUseMiddleware(this IApplicationBuilder builder, bool enableOpenMetrics, CollectorRegistry? registry = null) { return builder.UseMiddleware(new MetricServerMiddleware.Settings { + EnableOpenMetrics = enableOpenMetrics, Registry = registry }); } diff --git a/Prometheus.NetFramework.AspNet/AspNetMetricServer.cs b/Prometheus.NetFramework.AspNet/AspNetMetricServer.cs index 5cdc7804..46abf391 100644 --- a/Prometheus.NetFramework.AspNet/AspNetMetricServer.cs +++ b/Prometheus.NetFramework.AspNet/AspNetMetricServer.cs @@ -62,7 +62,7 @@ protected override Task SendAsync(HttpRequestMessage reques { try { - await _registry.CollectAndExportAsTextAsync(stream, cancellationToken); + await _registry.CollectAndExportAsTextAsync(stream, ExpositionFormat.Text, cancellationToken); } finally { diff --git a/Prometheus/AutoLeasingCounter.cs b/Prometheus/AutoLeasingCounter.cs index 4ab852dd..a8facedd 100644 --- a/Prometheus/AutoLeasingCounter.cs +++ b/Prometheus/AutoLeasingCounter.cs @@ -39,9 +39,14 @@ public Instance(IManagedLifetimeMetricHandle inner, string[] labelValu public double Value => throw new NotSupportedException("Read operations on a lifetime-extending-on-use expiring metric are not supported."); - public void Inc(double increment = 1) + public void Inc(params Exemplar.LabelPair[] exemplar) { - _inner.WithLease(x => x.Inc(increment), _labelValues); + Inc(increment:1, exemplar: exemplar); + } + + public void Inc(double increment = 1, params Exemplar.LabelPair[] exemplar) + { + _inner.WithLease(x => x.Inc(increment, exemplar), _labelValues); } public void IncTo(double targetValue) diff --git a/Prometheus/AutoLeasingHistogram.cs b/Prometheus/AutoLeasingHistogram.cs index 22cfe682..23787231 100644 --- a/Prometheus/AutoLeasingHistogram.cs +++ b/Prometheus/AutoLeasingHistogram.cs @@ -45,9 +45,14 @@ public void Observe(double val, long count) _inner.WithLease(x => x.Observe(val, count), _labelValues); } + public void Observe(double val, params Exemplar.LabelPair[] exemplar) + { + _inner.WithLease(x => x.Observe(val, exemplar), _labelValues); + } + public void Observe(double val) { - _inner.WithLease(x => x.Observe(val), _labelValues); + Observe(val, Array.Empty()); } } } diff --git a/Prometheus/Collector.cs b/Prometheus/Collector.cs index 5378f7c7..3dd8c45f 100644 --- a/Prometheus/Collector.cs +++ b/Prometheus/Collector.cs @@ -21,6 +21,8 @@ public abstract class Collector /// public string Help { get; } + internal byte[] HelpBytes { get; } + /// /// Names of the instance-specific labels (name-value pairs) that apply to this metric. /// When the values are added to the names, you get a instance. @@ -38,6 +40,8 @@ public abstract class Collector internal LabelSequence StaticLabels; internal abstract MetricType Type { get; } + + internal byte[] TypeBytes { get; } internal abstract int ChildCount { get; } internal abstract int TimeseriesCount { get; } @@ -62,7 +66,11 @@ internal Collector(string name, string help, StringSequence instanceLabelNames, Name = name; NameBytes = PrometheusConstants.ExportEncoding.GetBytes(Name); + TypeBytes = PrometheusConstants.ExportEncoding.GetBytes(Type.ToString().ToLowerInvariant()); Help = help; + HelpBytes = String.IsNullOrWhiteSpace(help) + ? Array.Empty() + : PrometheusConstants.ExportEncoding.GetBytes(help); InstanceLabelNames = instanceLabelNames; StaticLabels = staticLabels; @@ -245,7 +253,7 @@ internal override async Task CollectAndSerializeAsync(IMetricsSerializer seriali { EnsureUnlabelledMetricCreatedIfNoLabels(); - await serializer.WriteFamilyDeclarationAsync(_familyHeaderLines, cancel); + await serializer.WriteFamilyDeclarationAsync(Name, NameBytes, HelpBytes, Type, TypeBytes, cancel); foreach (var child in _labelledMetrics.Values) await child.CollectAndSerializeAsync(serializer, cancel); diff --git a/Prometheus/CollectorRegistry.cs b/Prometheus/CollectorRegistry.cs index aa3f774e..404cdf18 100644 --- a/Prometheus/CollectorRegistry.cs +++ b/Prometheus/CollectorRegistry.cs @@ -136,12 +136,12 @@ internal LabelSequence GetStaticLabels() /// /// This method is designed to be used with custom output mechanisms that do not use an IMetricServer. /// - public Task CollectAndExportAsTextAsync(Stream to, CancellationToken cancel = default) + public Task CollectAndExportAsTextAsync(Stream to, ExpositionFormat format= ExpositionFormat.Text, CancellationToken cancel = default) { if (to == null) throw new ArgumentNullException(nameof(to)); - return CollectAndSerializeAsync(new TextSerializer(to), cancel); + return CollectAndSerializeAsync(new TextSerializer(to, format), cancel); } // We pass this thing to GetOrAdd to avoid allocating a collector or a closure. @@ -248,7 +248,7 @@ internal async Task CollectAndSerializeAsync(IMetricsSerializer serializer, Canc foreach (var collector in _collectors.Values) await collector.CollectAndSerializeAsync(serializer, cancel); - + await serializer.WriteEnd(cancel); await serializer.FlushAsync(cancel); } diff --git a/Prometheus/Counter.cs b/Prometheus/Counter.cs index a02ad097..83a5e88f 100644 --- a/Prometheus/Counter.cs +++ b/Prometheus/Counter.cs @@ -10,22 +10,49 @@ internal Child(Collector parent, LabelSequence instanceLabels, LabelSequence fla } private ThreadSafeDouble _value; + private ObservedExemplar _observedExemplar = ObservedExemplar.Empty; - private protected override async Task CollectAndSerializeImplAsync(IMetricsSerializer serializer, CancellationToken cancel) + private protected override async Task CollectAndSerializeImplAsync(IMetricsSerializer serializer, + CancellationToken cancel) { + // Borrow the current exemplar + ObservedExemplar cp = + Interlocked.CompareExchange(ref _observedExemplar, ObservedExemplar.Empty, _observedExemplar); + await serializer.WriteMetricPointAsync( Parent.NameBytes, FlattenedLabelsBytes, CanonicalLabel.Empty, - cancel, - Value); + cancel, + Value, + cp); + + if (cp != ObservedExemplar.Empty) + { + // attempt to return the exemplar to the pool unless a new one has arrived. + var prev = Interlocked.CompareExchange(ref _observedExemplar, cp, ObservedExemplar.Empty); + if (prev != ObservedExemplar.Empty) // a new exemplar is present so we return ours back to the pool. + ObservedExemplar.ReturnPooled(cp); + } + } + + public void Inc(params Exemplar.LabelPair[] exemplar) + { + Inc(increment: 1, exemplar: exemplar); } - public void Inc(double increment = 1.0) + public void Inc(double increment = 1.0, params Exemplar.LabelPair[] exemplar) { if (increment < 0.0) throw new ArgumentOutOfRangeException(nameof(increment), "Counter value cannot decrease."); + if (exemplar is { Length: > 0 }) + { + var ex = ObservedExemplar.CreatePooled(exemplar, increment); + var current = Interlocked.Exchange(ref _observedExemplar, ex); + if (current != ObservedExemplar.Empty) + ObservedExemplar.ReturnPooled(current); + } _value.Add(increment); Publish(); } @@ -38,6 +65,7 @@ public void IncTo(double targetValue) public double Value => _value.Value; } + private protected override Child NewChild(LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish) { @@ -56,6 +84,14 @@ internal Counter(string name, string help, StringSequence instanceLabelNames, La public void Publish() => Unlabelled.Publish(); public void Unpublish() => Unlabelled.Unpublish(); + public void Inc(params Exemplar.LabelPair[] exemplar) + { + Inc(increment: 1, exemplar: exemplar); + } + + public void Inc(double increment = 1, params Exemplar.LabelPair[] exemplar) => + Unlabelled.Inc(increment, exemplar); + internal override MetricType Type => MetricType.Counter; internal override int TimeseriesCount => ChildCount; diff --git a/Prometheus/Exemplar.cs b/Prometheus/Exemplar.cs new file mode 100644 index 00000000..82a55a13 --- /dev/null +++ b/Prometheus/Exemplar.cs @@ -0,0 +1,74 @@ +using System.Text; + +namespace Prometheus; + +public static class Exemplar +{ + /// + /// An exemplar label key. + /// + public readonly struct LabelKey + { + internal LabelKey(byte[] key, int runeCount) + { + Bytes = key; + RuneCount = runeCount; + } + + internal int RuneCount { get; } + + internal byte[] Bytes { get; } + + /// + /// Create a LabelPair once a value is available + /// + /// The string is expected to only contain runes in the ASCII range, runes outside the ASCII range will get replaced + /// with placeholders. This constraint may be relaxed with future versions. + /// + public LabelPair WithValue(string value) + { + var asciiBytes = Encoding.ASCII.GetBytes(value); + return new LabelPair(Bytes, asciiBytes, RuneCount + asciiBytes.Length); + } + } + + /// + /// A single exemplar label pair in a form suitable for efficient serialization. + /// + public readonly struct LabelPair + { + internal LabelPair(byte[] keyBytes, byte[] valueBytes, int runeCount) + { + KeyBytes = keyBytes; + ValueBytes = valueBytes; + RuneCount = runeCount; + } + + internal int RuneCount { get; } + internal byte[] KeyBytes { get; } + internal byte[] ValueBytes { get; } + } + + /// + /// Return an exemplar label key, this may be curried with a value to produce a LabelPair. + /// + /// The string is expected to only contain runes in the ASCII range, runes outside the ASCII range will get replaced + /// with placeholders. This constraint may be relaxed with future versions. + /// + public static LabelKey Key(string key) + { + if (string.IsNullOrWhiteSpace(key)) + throw new ArgumentException("empty key"); + var asciiBytes = Encoding.ASCII.GetBytes(key); + return new LabelKey(asciiBytes, asciiBytes.Length); + } + + /// + /// Pair constructs a LabelPair, it is advisable to memoize a "Key" (eg: "traceID") and then to derive "LabelPair"s + /// from these. + /// + public static LabelPair Pair(string key, string value) + { + return Key(key).WithValue(value); + } +} \ No newline at end of file diff --git a/Prometheus/ExpositionFormats.cs b/Prometheus/ExpositionFormats.cs new file mode 100644 index 00000000..b3e0b78a --- /dev/null +++ b/Prometheus/ExpositionFormats.cs @@ -0,0 +1,13 @@ +namespace Prometheus; + +public enum ExpositionFormat +{ + /// + /// The traditional prometheus exposition format. + /// + Text, + /// + /// The OpenMetrics text exposition format + /// + OpenMetricsText +} \ No newline at end of file diff --git a/Prometheus/Gauge.cs b/Prometheus/Gauge.cs index 04267c4b..6375172f 100644 --- a/Prometheus/Gauge.cs +++ b/Prometheus/Gauge.cs @@ -14,7 +14,7 @@ internal Child(Collector parent, LabelSequence instanceLabels, LabelSequence fla private protected override async Task CollectAndSerializeImplAsync(IMetricsSerializer serializer, CancellationToken cancel) { await serializer.WriteMetricPointAsync( - Parent.NameBytes, FlattenedLabelsBytes, CanonicalLabel.Empty, cancel, Value); + Parent.NameBytes, FlattenedLabelsBytes, CanonicalLabel.Empty, cancel, Value, ObservedExemplar.Empty); } public void Inc(double increment = 1) diff --git a/Prometheus/Histogram.cs b/Prometheus/Histogram.cs index 645c8677..33838b37 100644 --- a/Prometheus/Histogram.cs +++ b/Prometheus/Histogram.cs @@ -50,7 +50,7 @@ internal Child(Histogram parent, LabelSequence instanceLabels, LabelSequence fla : base(parent, instanceLabels, flattenedLabels, publish) { Parent = parent; - + _upperBounds = Parent._buckets; _bucketCounts = new ThreadSafeLong[_upperBounds.Length]; _leLabels = new CanonicalLabel[_upperBounds.Length]; @@ -58,6 +58,11 @@ internal Child(Histogram parent, LabelSequence instanceLabels, LabelSequence fla { _leLabels[i] = TextSerializer.EncodeValueAsCanonicalLabel(LeLabelName, Parent._buckets[i]); } + _exemplars = new ObservedExemplar[_upperBounds.Length]; + for (var i = 0; i < _upperBounds.Length; i++) + { + _exemplars[i] = ObservedExemplar.Empty; + } } internal new readonly Histogram Parent; @@ -70,6 +75,7 @@ internal Child(Histogram parent, LabelSequence instanceLabels, LabelSequence fla private static readonly byte[] CountSuffix = PrometheusConstants.ExportEncoding.GetBytes("count"); private static readonly byte[] BucketSuffix = PrometheusConstants.ExportEncoding.GetBytes("bucket"); private static readonly byte[] LeLabelName = PrometheusConstants.ExportEncoding.GetBytes("le"); + private readonly ObservedExemplar[] _exemplars; private protected override async Task CollectAndSerializeImplAsync(IMetricsSerializer serializer, CancellationToken cancel) @@ -83,6 +89,7 @@ await serializer.WriteMetricPointAsync( CanonicalLabel.Empty, cancel, _sum.Value, + ObservedExemplar.Empty, suffix: SumSuffix); await serializer.WriteMetricPointAsync( Parent.NameBytes, @@ -90,12 +97,15 @@ await serializer.WriteMetricPointAsync( CanonicalLabel.Empty, cancel, _bucketCounts.Sum(b => b.Value), + ObservedExemplar.Empty, suffix: CountSuffix); var cumulativeCount = 0L; for (var i = 0; i < _bucketCounts.Length; i++) { + // borrow the current exemplar. + ObservedExemplar cp = Interlocked.CompareExchange(ref _exemplars[i], ObservedExemplar.Empty, _exemplars[i]); cumulativeCount += _bucketCounts[i].Value; await serializer.WriteMetricPointAsync( Parent.NameBytes, @@ -103,16 +113,29 @@ await serializer.WriteMetricPointAsync( _leLabels[i], cancel, cumulativeCount, + cp, suffix: BucketSuffix); + + if (cp != ObservedExemplar.Empty) + { + // attempt to return the exemplar to the pool unless a new one has arrived. + var prev = Interlocked.CompareExchange(ref _exemplars[i], cp, ObservedExemplar.Empty); + if (prev != ObservedExemplar.Empty) // a new exemplar is present so we return ours back to the pool. + ObservedExemplar.ReturnPooled(cp); + } } } public double Sum => _sum.Value; public long Count => _bucketCounts.Sum(b => b.Value); + public void Observe(double val, params Exemplar.LabelPair[] exemplar) => ObserveInternal(val, 1, exemplar); + public void Observe(double val) => Observe(val, 1); - public void Observe(double val, long count) + public void Observe(double val, long count) => ObserveInternal(val, count); + + private void ObserveInternal(double val, long count, params Exemplar.LabelPair[] exemplar) { if (double.IsNaN(val)) { @@ -124,6 +147,14 @@ public void Observe(double val, long count) if (val <= _upperBounds[i]) { _bucketCounts[i].Add(count); + if (exemplar is { Length: > 0 }) + { + var observedExemplar = ObservedExemplar.CreatePooled(exemplar, val); + var current = Interlocked.Exchange(ref _exemplars[i], observedExemplar); + if (current != ObservedExemplar.Empty) + ObservedExemplar.ReturnPooled(current); + } + break; } } @@ -138,6 +169,7 @@ public void Observe(double val, long count) public long Count => Unlabelled.Count; public void Observe(double val) => Unlabelled.Observe(val, 1); public void Observe(double val, long count) => Unlabelled.Observe(val, count); + public void Observe(double val, params Exemplar.LabelPair[] exemplar) => Unlabelled.Observe(val, exemplar); public void Publish() => Unlabelled.Publish(); public void Unpublish() => Unlabelled.Unpublish(); diff --git a/Prometheus/ICollectorRegistry.cs b/Prometheus/ICollectorRegistry.cs index d6ec5c94..0ab8d779 100644 --- a/Prometheus/ICollectorRegistry.cs +++ b/Prometheus/ICollectorRegistry.cs @@ -12,6 +12,6 @@ public interface ICollectorRegistry IEnumerable> StaticLabels { get; } void SetStaticLabels(IDictionary labels); - Task CollectAndExportAsTextAsync(Stream to, CancellationToken cancel = default); + Task CollectAndExportAsTextAsync(Stream to, ExpositionFormat format = ExpositionFormat.Text, CancellationToken cancel = default); } } diff --git a/Prometheus/ICounter.cs b/Prometheus/ICounter.cs index 8803d4be..41d66d75 100644 --- a/Prometheus/ICounter.cs +++ b/Prometheus/ICounter.cs @@ -2,7 +2,17 @@ { public interface ICounter : ICollectorChild { - void Inc(double increment = 1); + /// + /// Increment a counter by 1. + /// + /// A set of labels representing an exemplar. + void Inc(params Exemplar.LabelPair[] exemplar); + /// + /// Increment a counter + /// + /// The increment. + /// A set of labels representing an exemplar. + void Inc(double increment = 1, params Exemplar.LabelPair[] exemplar); void IncTo(double targetValue); double Value { get; } } diff --git a/Prometheus/IHistogram.cs b/Prometheus/IHistogram.cs index 83c88dc9..5abe5521 100644 --- a/Prometheus/IHistogram.cs +++ b/Prometheus/IHistogram.cs @@ -11,6 +11,13 @@ public interface IHistogram : IObserver /// Number of observations with this value. void Observe(double val, long count); + /// + /// Observe an event with an exemplar + /// + /// Measured value. + /// A set of labels representing an exemplar + void Observe(double val, params Exemplar.LabelPair[] exemplar); + /// /// Gets the sum of all observed events. /// diff --git a/Prometheus/IMetricsSerializer.cs b/Prometheus/IMetricsSerializer.cs index 0dc39a04..3424b0ac 100644 --- a/Prometheus/IMetricsSerializer.cs +++ b/Prometheus/IMetricsSerializer.cs @@ -10,15 +10,21 @@ internal interface IMetricsSerializer /// /// Writes the lines that declare the metric family. /// - Task WriteFamilyDeclarationAsync(byte[][] headerLines, CancellationToken cancel); + Task WriteFamilyDeclarationAsync(string name, byte[] nameBytes, byte[] helpBytes, MetricType type, + byte[] typeBytes, CancellationToken cancel); /// /// Writes out a single metric point /// /// Task WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, CanonicalLabel canonicalLabel, - CancellationToken cancel, double value, byte[]? suffix = null); + CancellationToken cancel, double value, ObservedExemplar exemplar, byte[]? suffix = null); + /// + /// Writes out terminal lines + /// + Task WriteEnd(CancellationToken cancel); + /// /// Flushes any pending buffers. Always call this after all your write calls. /// diff --git a/Prometheus/ObservedExemplar.cs b/Prometheus/ObservedExemplar.cs new file mode 100644 index 00000000..4421adf2 --- /dev/null +++ b/Prometheus/ObservedExemplar.cs @@ -0,0 +1,87 @@ +using System.Diagnostics; +using Microsoft.Extensions.ObjectPool; + +namespace Prometheus; + +/// +/// Internal representation of an Exemplar ready to be serialized. +/// +internal class ObservedExemplar +{ + private const int MaxRunes = 128; + + private static readonly ObjectPool Pool = ObjectPool.Create(); + + public static readonly ObservedExemplar Empty = new(); + + internal static INowProvider NowProvider = new RealNowProvider(); + + public Exemplar.LabelPair[]? Labels { get; private set; } + public double Val { get; private set; } + public double Timestamp { get; private set; } + + public ObservedExemplar() + { + Labels = null; + Val = 0; + Timestamp = 0; + } + + internal interface INowProvider + { + double Now(); + } + + private sealed class RealNowProvider : INowProvider + { + public double Now() + { + return DateTimeOffset.Now.ToUnixTimeMilliseconds() / 1e3; + } + } + + public bool IsValid => Labels != null; + + private void Update(Exemplar.LabelPair[] labels, double val) + { + Debug.Assert(this != Empty, "do not mutate the sentinel"); + var tally = 0; + for (var i = 0; i < labels.Length; i++) + { + tally += labels[i].RuneCount; + for (var j = 0; j < labels.Length; j++) + { + if (i == j) continue; + if (Equal(labels[i].KeyBytes, labels[j].KeyBytes)) + throw new ArgumentException("exemplar contains duplicate keys"); + } + } + + if (tally > MaxRunes) + throw new ArgumentException($"exemplar labels has {tally} runes, exceeding the limit of {MaxRunes}."); + + Labels = labels; + Val = val; + Timestamp = NowProvider.Now(); + } + + private static bool Equal(byte[] a, byte[] b) + { + // see https://www.syncfusion.com/succinctly-free-ebooks/application-security-in-net-succinctly/comparing-byte-arrays + var x = a.Length ^ b.Length; + for (var i = 0; i < a.Length && i < b.Length; ++i) x |= a[i] ^ b[i]; + return x == 0; + } + + public static ObservedExemplar CreatePooled(Exemplar.LabelPair[] labelPairs, double val) + { + var oe = Pool.Get(); + oe.Update(labelPairs, val); + return oe; + } + + public static void ReturnPooled(ObservedExemplar observedExemplar) + { + Pool.Return(observedExemplar); + } +} \ No newline at end of file diff --git a/Prometheus/Prometheus.csproj b/Prometheus/Prometheus.csproj index 30f0cfe5..91f1afbe 100644 --- a/Prometheus/Prometheus.csproj +++ b/Prometheus/Prometheus.csproj @@ -45,6 +45,7 @@ + diff --git a/Prometheus/PrometheusConstants.cs b/Prometheus/PrometheusConstants.cs index 6fa05eda..f015ef54 100644 --- a/Prometheus/PrometheusConstants.cs +++ b/Prometheus/PrometheusConstants.cs @@ -5,13 +5,22 @@ namespace Prometheus { public static class PrometheusConstants { - public const string ExporterContentType = "text/plain; version=0.0.4; charset=utf-8"; + public const string + TextFmtContentType = "text/plain", + OpenMetricsContentType = "application/openmetrics-text"; + + public const string + ExporterContentType = TextFmtContentType + "; version=0.0.4; charset=utf-8", + ExporterOpenMetricsContentType = OpenMetricsContentType + "; version=0.0.1; charset=utf-8"; + // ASP.NET requires a MediaTypeHeaderValue object - public static readonly MediaTypeHeaderValue ExporterContentTypeValue = MediaTypeHeaderValue.Parse(ExporterContentType); + public static readonly MediaTypeHeaderValue + ExporterContentTypeValue = MediaTypeHeaderValue.Parse(ExporterContentType), + ExporterOpenMetricsContentTypeValue = MediaTypeHeaderValue.Parse(ExporterOpenMetricsContentType); // Use UTF-8 encoding, but provide the flag to ensure the Unicode Byte Order Mark is never // pre-pended to the output stream. public static readonly Encoding ExportEncoding = new UTF8Encoding(false); } -} +} \ No newline at end of file diff --git a/Prometheus/Summary.cs b/Prometheus/Summary.cs index 4a85024d..ecd653f8 100644 --- a/Prometheus/Summary.cs +++ b/Prometheus/Summary.cs @@ -148,6 +148,7 @@ await serializer.WriteMetricPointAsync( CanonicalLabel.Empty, cancel, sum, + ObservedExemplar.Empty, suffix: SumSuffix); await serializer.WriteMetricPointAsync( Parent.NameBytes, @@ -155,6 +156,7 @@ await serializer.WriteMetricPointAsync( CanonicalLabel.Empty, cancel, count, + ObservedExemplar.Empty, suffix: CountSuffix); for (var i = 0; i < values.Count; i++) @@ -164,7 +166,8 @@ await serializer.WriteMetricPointAsync( FlattenedLabelsBytes, _quantileLabels[i], cancel, - values[i].value); + values[i].value, + ObservedExemplar.Empty); } } diff --git a/Prometheus/TextSerializer.cs b/Prometheus/TextSerializer.cs index f05ad9ea..46b6f5bc 100644 --- a/Prometheus/TextSerializer.cs +++ b/Prometheus/TextSerializer.cs @@ -15,16 +15,31 @@ internal sealed class TextSerializer : IMetricsSerializer private static readonly byte[] LeftBrace = { (byte)'{' }; private static readonly byte[] RightBraceSpace = { (byte)'}', (byte)' ' }; private static readonly byte[] Space = { (byte)' ' }; + private static readonly byte[] SpaceHashSpaceLeftBrace = { (byte)' ', (byte)'#', (byte)' ', (byte)'{' }; private static readonly byte[] PositiveInfinity = PrometheusConstants.ExportEncoding.GetBytes("+Inf"); - - public TextSerializer(Stream stream) + private static readonly byte[] NegativeInfinity = PrometheusConstants.ExportEncoding.GetBytes("-Inf"); + private static readonly byte[] NotANumber = PrometheusConstants.ExportEncoding.GetBytes("NaN"); + private static readonly byte[] PositiveOne = PrometheusConstants.ExportEncoding.GetBytes("1.0"); + private static readonly byte[] Zero = PrometheusConstants.ExportEncoding.GetBytes("0.0"); + private static readonly byte[] NegativeOne = PrometheusConstants.ExportEncoding.GetBytes("-1.0"); + private static readonly byte[] EofNewLine = PrometheusConstants.ExportEncoding.GetBytes("# EOF\n"); + private static readonly byte[] HashHelpSpace = PrometheusConstants.ExportEncoding.GetBytes("# HELP "); + private static readonly byte[] NewlineHashTypeSpace = PrometheusConstants.ExportEncoding.GetBytes("\n# TYPE "); + private static readonly byte[] Unknown = PrometheusConstants.ExportEncoding.GetBytes("unknown"); + + private static readonly char[] DotEChar = { '.', 'e' }; + + public TextSerializer(Stream stream, ExpositionFormat fmt = ExpositionFormat.Text) { + _fmt = fmt; _stream = new Lazy(() => stream); } // Enables delay-loading of the stream, because touching stream in HTTP handler triggers some behavior. - public TextSerializer(Func streamFactory) + public TextSerializer(Func streamFactory, + ExpositionFormat fmt = ExpositionFormat.Text) { + _fmt = fmt; _stream = new Lazy(streamFactory); } @@ -39,23 +54,107 @@ public async Task FlushAsync(CancellationToken cancel) private readonly Lazy _stream; - // # HELP name help - // # TYPE name type - public async Task WriteFamilyDeclarationAsync(byte[][] headerLines, CancellationToken cancel) + public async Task WriteFamilyDeclarationAsync(string name, byte[]nameBytes, byte[] helpBytes, MetricType type, + byte[] typeBytes, CancellationToken cancel) { - foreach (var line in headerLines) + var nameLen = nameBytes.Length; + if (_fmt == ExpositionFormat.OpenMetricsText && type == MetricType.Counter) + { + if (name.EndsWith("_total")) + { + nameLen -= 6; // in OpenMetrics the counter name does not include the _total prefix. + } + else + { + typeBytes = Unknown; // if the total prefix is missing the _total prefix it is out of spec + } + } + + await _stream.Value.WriteAsync(HashHelpSpace, 0, HashHelpSpace.Length, cancel); + await _stream.Value.WriteAsync(nameBytes, 0, nameLen, cancel); + if (helpBytes.Length > 0) { - await _stream.Value.WriteAsync(line, 0, line.Length, cancel); - await _stream.Value.WriteAsync(NewLine, 0, NewLine.Length, cancel); + await _stream.Value.WriteAsync(Space, 0, Space.Length, cancel); + await _stream.Value.WriteAsync(helpBytes, 0, helpBytes.Length, cancel); } + await _stream.Value.WriteAsync(NewlineHashTypeSpace, 0, NewlineHashTypeSpace.Length, cancel); + await _stream.Value.WriteAsync(nameBytes, 0, nameLen, cancel); + await _stream.Value.WriteAsync(Space, 0, Space.Length, cancel); + await _stream.Value.WriteAsync(typeBytes, 0, typeBytes.Length, cancel); + await _stream.Value.WriteAsync(NewLine, 0, NewLine.Length, cancel); + } + + public async Task WriteEnd(CancellationToken cancel) + { + if (_fmt == ExpositionFormat.OpenMetricsText) + await _stream.Value.WriteAsync(EofNewLine, 0, EofNewLine.Length, cancel); } public async Task WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, CanonicalLabel canonicalLabel, - CancellationToken cancel, - double value, byte[]? suffix = null) + CancellationToken cancel, double value, ObservedExemplar exemplar, byte[]? suffix = null) + { + await WriteIdentifierPartAsync(name, flattenedLabels, cancel, canonicalLabel, exemplar, suffix); + await WriteValuePartAsync(value, exemplar, cancel); + } + + private async Task WriteExemplarAsync(CancellationToken cancel, ObservedExemplar exemplar) { - await WriteIdentifierPartAsync(name, flattenedLabels, cancel, canonicalLabel, suffix); - await WriteValuePartAsync(value, cancel); + await _stream.Value.WriteAsync(SpaceHashSpaceLeftBrace, 0, SpaceHashSpaceLeftBrace.Length, cancel); + for (var i = 0; i < exemplar.Labels!.Length; i++) + { + if (i > 0) + await _stream.Value.WriteAsync(Comma, 0, Comma.Length, cancel); + await WriteLabel(exemplar.Labels[i].KeyBytes, exemplar.Labels[i].ValueBytes, cancel); + } + + await _stream.Value.WriteAsync(RightBraceSpace, 0, RightBraceSpace.Length, cancel); + await WriteValue(exemplar.Val, cancel); + await _stream.Value.WriteAsync(Space, 0, Space.Length, cancel); + await WriteValue(exemplar.Timestamp, cancel); + } + + private async Task WriteLabel(byte[] label, byte[] value, CancellationToken cancel) + { + await _stream.Value.WriteAsync(label, 0, label.Length, cancel); + await _stream.Value.WriteAsync(Equal, 0, Equal.Length, cancel); + await _stream.Value.WriteAsync(Quote, 0, Quote.Length, cancel); + await _stream.Value.WriteAsync(value, 0, value.Length, cancel); + await _stream.Value.WriteAsync(Quote, 0, Quote.Length, cancel); + } + + private async Task WriteValue(double value, CancellationToken cancel) + { + if (_fmt == ExpositionFormat.OpenMetricsText) + { + switch (value) + { + case 0: + await _stream.Value.WriteAsync(Zero, 0, Zero.Length, cancel); + return; + case 1: + await _stream.Value.WriteAsync(PositiveOne, 0, PositiveOne.Length, cancel); + return; + case -1: + await _stream.Value.WriteAsync(NegativeOne, 0, NegativeOne.Length, cancel); + return; + case double.PositiveInfinity: + await _stream.Value.WriteAsync(PositiveInfinity, 0, PositiveInfinity.Length, cancel); + return; + case double.NegativeInfinity: + await _stream.Value.WriteAsync(NegativeInfinity, 0, NegativeInfinity.Length, cancel); + return; + case double.NaN: + await _stream.Value.WriteAsync(NotANumber, 0, NotANumber.Length, cancel); + return; + } + } + + var valueAsString = value.ToString("g", CultureInfo.InvariantCulture); + if (_fmt == ExpositionFormat.OpenMetricsText && valueAsString.IndexOfAny(DotEChar)==-1 /* did not contain .|e */) + valueAsString += ".0"; + var numBytes = PrometheusConstants.ExportEncoding + .GetBytes(valueAsString, 0, valueAsString.Length, _stringBytesBuffer, 0); + await _stream.Value.WriteAsync(_stringBytesBuffer, 0, numBytes, cancel); } // Reuse a buffer to do the UTF-8 encoding. @@ -63,18 +162,21 @@ public async Task WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, Can // https://github.com/dotnet/corefx/issues/28379 // Size limit guided by https://stackoverflow.com/questions/21146544/what-is-the-maximum-length-of-double-tostringd private readonly byte[] _stringBytesBuffer = new byte[32]; + private readonly ExpositionFormat _fmt; // 123.456 // Note: Terminates with a NEWLINE - private async Task WriteValuePartAsync(double value, CancellationToken cancel) + private async Task WriteValuePartAsync(double value, ObservedExemplar exemplar, CancellationToken cancel) { - var valueAsString = value.ToString(CultureInfo.InvariantCulture); - var numBytes = PrometheusConstants.ExportEncoding - .GetBytes(valueAsString, 0, valueAsString.Length, _stringBytesBuffer, 0); + await WriteValue(value, cancel); + if (_fmt == ExpositionFormat.OpenMetricsText && exemplar.IsValid) + { + await WriteExemplarAsync(cancel, exemplar); + } - await _stream.Value.WriteAsync(_stringBytesBuffer, 0, numBytes, cancel); await _stream.Value.WriteAsync(NewLine, 0, NewLine.Length, cancel); } + /// /// Creates a metric identifier, with an optional name postfix and an optional extra label to append to the end. @@ -82,7 +184,7 @@ private async Task WriteValuePartAsync(double value, CancellationToken cancel) /// Note: Terminates with a SPACE /// private async Task WriteIdentifierPartAsync(byte[] name, byte[] flattenedLabels, CancellationToken cancel, - CanonicalLabel canonicalLabel, byte[]? suffix = null) + CanonicalLabel canonicalLabel, ObservedExemplar observedExemplar, byte[]? suffix = null) { await _stream.Value.WriteAsync(name, 0, name.Length, cancel); if (suffix != null && suffix.Length > 0) @@ -110,8 +212,12 @@ private async Task WriteIdentifierPartAsync(byte[] name, byte[] flattenedLabels, await _stream.Value.WriteAsync(canonicalLabel.Name, 0, canonicalLabel.Name.Length, cancel); await _stream.Value.WriteAsync(Equal, 0, Equal.Length, cancel); await _stream.Value.WriteAsync(Quote, 0, Quote.Length, cancel); - await _stream.Value.WriteAsync(canonicalLabel.Prometheus, 0, canonicalLabel.Prometheus.Length, - cancel); + if (_fmt == ExpositionFormat.OpenMetricsText) + await _stream.Value.WriteAsync( + canonicalLabel.OpenMetrics, 0, canonicalLabel.OpenMetrics.Length, cancel); + else + await _stream.Value.WriteAsync( + canonicalLabel.Prometheus, 0, canonicalLabel.Prometheus.Length, cancel); await _stream.Value.WriteAsync(Quote, 0, Quote.Length, cancel); } @@ -133,9 +239,12 @@ internal static CanonicalLabel EncodeValueAsCanonicalLabel(byte[] name, double v if (double.IsPositiveInfinity(value)) return new CanonicalLabel(name, PositiveInfinity, PositiveInfinity); - var valueAsString = value.ToString(CultureInfo.InvariantCulture); - var bytes = PrometheusConstants.ExportEncoding.GetBytes(valueAsString); - return new CanonicalLabel(name, bytes, bytes); + var valueAsString = value.ToString("g", CultureInfo.InvariantCulture); + var prom = PrometheusConstants.ExportEncoding.GetBytes(valueAsString); + + return new CanonicalLabel(name, prom, valueAsString.IndexOfAny(DotEChar) != -1 // contained .|e + ? prom + : PrometheusConstants.ExportEncoding.GetBytes(valueAsString + ".0")); } } } \ No newline at end of file diff --git a/Sample.Web/Program.cs b/Sample.Web/Program.cs index 451322cc..ff716b3f 100644 --- a/Sample.Web/Program.cs +++ b/Sample.Web/Program.cs @@ -53,7 +53,7 @@ // * metrics about requests handled by the web app (configured above) // * ASP.NET health check statuses (configured above) // * custom business logic metrics published by the SampleService class - endpoints.MapMetrics(); + endpoints.MapMetrics(enableOpenMetrics:true); }); app.Run(); diff --git a/Sample.Web/SampleService.cs b/Sample.Web/SampleService.cs index 4f5ce248..31add63a 100644 --- a/Sample.Web/SampleService.cs +++ b/Sample.Web/SampleService.cs @@ -68,16 +68,18 @@ private async Task ReadySetGoAsync(CancellationToken cancel) await Task.WhenAll(googleTask, microsoftTask); + var exemplar = Exemplar.Pair("traceID", "1234"); + // Determine the winner and report the change in score. if (googleStopwatch.Elapsed < microsoftStopwatch.Elapsed) { - WinsByEndpoint.WithLabels(googleUrl).Inc(); - LossesByEndpoint.WithLabels(microsoftUrl).Inc(); + WinsByEndpoint.WithLabels(googleUrl).Inc(exemplar); + LossesByEndpoint.WithLabels(microsoftUrl).Inc(exemplar); } else if (googleStopwatch.Elapsed < microsoftStopwatch.Elapsed) { - WinsByEndpoint.WithLabels(microsoftUrl).Inc(); - LossesByEndpoint.WithLabels(googleUrl).Inc(); + WinsByEndpoint.WithLabels(microsoftUrl).Inc(exemplar); + LossesByEndpoint.WithLabels(googleUrl).Inc(exemplar); } else { @@ -86,7 +88,7 @@ private async Task ReadySetGoAsync(CancellationToken cancel) // Report the difference. var difference = Math.Abs(googleStopwatch.Elapsed.TotalSeconds - microsoftStopwatch.Elapsed.TotalSeconds); - Difference.Observe(difference); + Difference.Observe(difference, exemplar: exemplar); // We finished one iteration of the service's work. IterationCount.Inc(); diff --git a/Sample.Web/run_prometheus_scrape.sh b/Sample.Web/run_prometheus_scrape.sh new file mode 100644 index 00000000..a499e60b --- /dev/null +++ b/Sample.Web/run_prometheus_scrape.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +# Runs a prometheus scrape on an endpoint with exemplar storage enabled. localhost:9090 will take you to the prometheus +# ui for verification. + +cat < prometheus.yml +scrape_configs: + - job_name: 'prometheus-net' + scrape_interval: 5s + static_configs: + - targets: ['$1'] +EOF + +docker run --rm -it --name prometheus -p 9090:9090 \ + -v $PWD/prometheus.yml:/etc/prometheus/prometheus.yml \ + prom/prometheus \ + --config.file=/etc/prometheus/prometheus.yml \ + --log.level=debug \ + --enable-feature=exemplar-storage \ No newline at end of file diff --git a/Tests.NetCore/HistogramTests.cs b/Tests.NetCore/HistogramTests.cs index 57aeadab..702724fb 100644 --- a/Tests.NetCore/HistogramTests.cs +++ b/Tests.NetCore/HistogramTests.cs @@ -1,4 +1,5 @@ -using System.Threading; +using System; +using System.Threading; using Microsoft.VisualStudio.TestTools.UnitTesting; using NSubstitute; using System.Threading.Tasks; @@ -8,6 +9,34 @@ namespace Prometheus.Tests [TestClass] public sealed class HistogramTests { + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void ObserveExemplarDuplicateKeys() + { + var registry = Metrics.NewCustomRegistry(); + var factory = Metrics.WithCustomRegistry(registry); + + var histogram = factory.CreateHistogram("xxx", ""); + histogram.Observe(1, Exemplar.Pair("traceID", "123"), Exemplar.Pair("traceID", "1")); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentException))] + public void ObserveExemplarTooManyRunes() + { + var registry = Metrics.NewCustomRegistry(); + var factory = Metrics.WithCustomRegistry(registry); + + var key1 = "0123456789" + "0123456789" + "0123456789" + "0123456789" + "0123456789"; // 50 + var key2 = "0123456789" + "0123456789" + "0123456789" + "0123456789" + "0123456780"; // 50 + var val1 = "01234567890123"; // 14 + var val2 = "012345678901234"; // 15 (= 129) + + var histogram = factory.CreateHistogram("xxx", ""); + histogram.Observe(1, Exemplar.Pair(key1, val1), Exemplar.Pair(key2, val2)); + } + + [TestMethod] public async Task Observe_IncrementsCorrectBucketsAndCountAndSum() { @@ -31,12 +60,11 @@ public async Task Observe_IncrementsCorrectBucketsAndCountAndSum() // 2.0 // 3.0 // +inf - await serializer.Received().WriteMetricPointAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(),5.0, Arg.Any()); - await serializer.Received().WriteMetricPointAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(),2.0, Arg.Any()); - await serializer.Received().WriteMetricPointAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(),0, Arg.Any()); - await serializer.Received().WriteMetricPointAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(),1, Arg.Any()); - await serializer.Received().WriteMetricPointAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(),2, Arg.Any()); - await serializer.Received().WriteMetricPointAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(),2, Arg.Any()); + await serializer.Received().WriteMetricPointAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(),2.0,Arg.Any(), Arg.Any()); + await serializer.Received().WriteMetricPointAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(),0,Arg.Any(), Arg.Any()); + await serializer.Received().WriteMetricPointAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(),1,Arg.Any(), Arg.Any()); + await serializer.Received().WriteMetricPointAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(),2,Arg.Any(), Arg.Any()); + await serializer.Received().WriteMetricPointAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(),2,Arg.Any(), Arg.Any()); } [TestMethod] diff --git a/Tests.NetCore/MetricInitializationTests.cs b/Tests.NetCore/MetricInitializationTests.cs index bba5b19e..492bb55d 100644 --- a/Tests.NetCore/MetricInitializationTests.cs +++ b/Tests.NetCore/MetricInitializationTests.cs @@ -47,8 +47,8 @@ public async Task CreatingUnlabelledMetric_WithoutObservingAnyData_ExportsImmedi // Without touching any metrics, there should be output for all because default config publishes immediately. - await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default); - await serializer.ReceivedWithAnyArgs(9).WriteMetricPointAsync(default, default, default, default, default); + await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default, default, default, default, default); + await serializer.ReceivedWithAnyArgs(9).WriteMetricPointAsync(default, default, default, default, default, default); } [TestMethod] @@ -79,8 +79,8 @@ public async Task CreatingUnlabelledMetric_WithInitialValueSuppression_ExportsNo await registry.CollectAndSerializeAsync(serializer, default); // There is a family for each of the above, in each family we expect to see 0 metrics. - await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default); - await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default,default); + await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default, default, default, default, default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default,default, default); } [TestMethod] @@ -116,8 +116,8 @@ public async Task CreatingUnlabelledMetric_WithInitialValueSuppression_ExportsAf await registry.CollectAndSerializeAsync(serializer, default); // Even though suppressed, they all now have values so should all be published. - await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default); - await serializer.ReceivedWithAnyArgs(9).WriteMetricPointAsync(default, default, default, default, default); + await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default, default, default, default, default); + await serializer.ReceivedWithAnyArgs(9).WriteMetricPointAsync(default, default, default, default, default, default); } [TestMethod] @@ -153,8 +153,8 @@ public async Task CreatingUnlabelledMetric_WithInitialValueSuppression_ExportsAf await registry.CollectAndSerializeAsync(serializer, default); // Even though suppressed, they were all explicitly published. - await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default); - await serializer.ReceivedWithAnyArgs(9).WriteMetricPointAsync(default, default, default, default, default); + await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default, default, default, default, default); + await serializer.ReceivedWithAnyArgs(9).WriteMetricPointAsync(default, default, default, default, default, default); } #endregion @@ -179,8 +179,8 @@ public async Task CreatingLabelledMetric_WithoutObservingAnyData_ExportsImmediat await registry.CollectAndSerializeAsync(serializer, default); // Metrics are published as soon as label values are defined. - await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default); - await serializer.ReceivedWithAnyArgs(9).WriteMetricPointAsync(default, default, default, default, default);; + await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default, default, default, default, default); + await serializer.ReceivedWithAnyArgs(9).WriteMetricPointAsync(default, default, default, default, default, default);; } [TestMethod] @@ -211,8 +211,8 @@ public async Task CreatingLabelledMetric_WithInitialValueSuppression_ExportsNoth await registry.CollectAndSerializeAsync(serializer, default); // Publishing was suppressed. - await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default); - await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default,default); + await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default, default, default, default, default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default,default, default); } [TestMethod] @@ -248,8 +248,8 @@ public async Task CreatingLabelledMetric_WithInitialValueSuppression_ExportsAfte await registry.CollectAndSerializeAsync(serializer, default); // Metrics are published because value was set. - await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default); - await serializer.ReceivedWithAnyArgs(9).WriteMetricPointAsync(default, default, default, default, default); + await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default, default, default, default, default); + await serializer.ReceivedWithAnyArgs(9).WriteMetricPointAsync(default, default, default, default, default, default); } [TestMethod] @@ -285,8 +285,8 @@ public async Task CreatingLabelledMetric_WithInitialValueSuppression_ExportsAfte await registry.CollectAndSerializeAsync(serializer, default); // Metrics are published because of explicit publish. - await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default); - await serializer.ReceivedWithAnyArgs(9).WriteMetricPointAsync(default, default, default, default, default); + await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default, default, default, default, default); + await serializer.ReceivedWithAnyArgs(9).WriteMetricPointAsync(default, default, default, default, default, default); } [TestMethod] @@ -303,8 +303,8 @@ public async Task CreatingLabelledMetric_AndUnpublishingAfterObservingData_DoesN var serializer = Substitute.For(); await registry.CollectAndSerializeAsync(serializer, default); - await serializer.ReceivedWithAnyArgs(1).WriteFamilyDeclarationAsync(default, default); - await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default,default); + await serializer.ReceivedWithAnyArgs(1).WriteFamilyDeclarationAsync(default, default, default, default, default, default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default,default, default); } #endregion @@ -328,8 +328,8 @@ public async Task CreatingLabelledMetric_WithoutObservingAnyData_DoesNotExportUn await registry.CollectAndSerializeAsync(serializer, default); // Family for each of the above, in each is 0 metrics. - await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default); - await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default,default); + await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default, default, default, default, default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default,default, default); } [TestMethod] @@ -357,8 +357,8 @@ public async Task CreatingLabelledMetric_AfterObservingLabelledData_DoesNotExpor await registry.CollectAndSerializeAsync(serializer, default); // Family for each of the above, in each is 4 metrics (labelled only). - await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default); - await serializer.ReceivedWithAnyArgs(9).WriteMetricPointAsync(default, default, default, default, default); + await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default, default, default, default, default); + await serializer.ReceivedWithAnyArgs(9).WriteMetricPointAsync(default, default, default, default, default, default); // Only after touching unlabelled do they get published. gauge.Inc(); @@ -370,8 +370,8 @@ public async Task CreatingLabelledMetric_AfterObservingLabelledData_DoesNotExpor await registry.CollectAndSerializeAsync(serializer, default); // Family for each of the above, in each is 8 metrics (unlabelled+labelled). - await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default); - await serializer.ReceivedWithAnyArgs(18).WriteMetricPointAsync(default, default, default, default, default); + await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default, default, default, default, default); + await serializer.ReceivedWithAnyArgs(18).WriteMetricPointAsync(default, default, default, default, default, default); } #endregion diff --git a/Tests.NetCore/MetricsTests.cs b/Tests.NetCore/MetricsTests.cs index fe2d1e35..7b680733 100644 --- a/Tests.NetCore/MetricsTests.cs +++ b/Tests.NetCore/MetricsTests.cs @@ -70,11 +70,11 @@ public async Task CreateCounter_WithDifferentRegistry_CreatesIndependentCounters var serializer2 = Substitute.For(); await registry2.CollectAndSerializeAsync(serializer2, default); - await serializer1.ReceivedWithAnyArgs().WriteFamilyDeclarationAsync(default, default); - await serializer1.ReceivedWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default); + await serializer1.ReceivedWithAnyArgs().WriteFamilyDeclarationAsync(default, default, default, default, default, default); + await serializer1.ReceivedWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default, default); - await serializer2.ReceivedWithAnyArgs().WriteFamilyDeclarationAsync(default, default); - await serializer2.ReceivedWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default); + await serializer2.ReceivedWithAnyArgs().WriteFamilyDeclarationAsync(default, default, default, default, default, default); + await serializer2.ReceivedWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default, default); } [TestMethod] @@ -89,8 +89,8 @@ public async Task Export_FamilyWithOnlyNonpublishedUnlabeledMetrics_ExportsFamil var serializer = Substitute.For(); await _registry.CollectAndSerializeAsync(serializer, default); - await serializer.ReceivedWithAnyArgs(1).WriteFamilyDeclarationAsync(default, default); - await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default,default); + await serializer.ReceivedWithAnyArgs(1).WriteFamilyDeclarationAsync(default, default, default, default, default, default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default,default, default); serializer.ClearReceivedCalls(); metric.Inc(); @@ -98,8 +98,8 @@ public async Task Export_FamilyWithOnlyNonpublishedUnlabeledMetrics_ExportsFamil await _registry.CollectAndSerializeAsync(serializer, default); - await serializer.ReceivedWithAnyArgs(1).WriteFamilyDeclarationAsync(default, default); - await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default,default); + await serializer.ReceivedWithAnyArgs(1).WriteFamilyDeclarationAsync(default, default, default, default, default, default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default,default, default); } [TestMethod] @@ -116,8 +116,8 @@ public async Task Export_FamilyWithOnlyNonpublishedLabeledMetrics_ExportsFamilyD var serializer = Substitute.For(); await _registry.CollectAndSerializeAsync(serializer, default); - await serializer.ReceivedWithAnyArgs(1).WriteFamilyDeclarationAsync(default, default); - await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default,default); + await serializer.ReceivedWithAnyArgs(1).WriteFamilyDeclarationAsync(default, default, default, default, default, default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default,default, default); serializer.ClearReceivedCalls(); instance.Inc(); @@ -125,8 +125,8 @@ public async Task Export_FamilyWithOnlyNonpublishedLabeledMetrics_ExportsFamilyD await _registry.CollectAndSerializeAsync(serializer, default); - await serializer.ReceivedWithAnyArgs(1).WriteFamilyDeclarationAsync(default, default); - await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default,default); + await serializer.ReceivedWithAnyArgs(1).WriteFamilyDeclarationAsync(default, default, default, default, default, default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default,default, default); } [TestMethod] @@ -143,16 +143,16 @@ public async Task DisposeChild_RemovesMetric() var serializer = Substitute.For(); await _registry.CollectAndSerializeAsync(serializer, default); - await serializer.ReceivedWithAnyArgs(1).WriteFamilyDeclarationAsync(default, default); - await serializer.ReceivedWithAnyArgs(1).WriteMetricPointAsync(default, default, default, default, default); + await serializer.ReceivedWithAnyArgs(1).WriteFamilyDeclarationAsync(default, default, default, default, default, default); + await serializer.ReceivedWithAnyArgs(1).WriteMetricPointAsync(default, default, default, default, default, default); serializer.ClearReceivedCalls(); instance.Dispose(); await _registry.CollectAndSerializeAsync(serializer, default); - await serializer.ReceivedWithAnyArgs(1).WriteFamilyDeclarationAsync(default, default); - await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default); + await serializer.ReceivedWithAnyArgs(1).WriteFamilyDeclarationAsync(default, default, default, default, default, default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default, default); } [TestMethod] diff --git a/Tests.NetCore/TextSerializerTests.cs b/Tests.NetCore/TextSerializerTests.cs index 08a63aca..d130c8a3 100644 --- a/Tests.NetCore/TextSerializerTests.cs +++ b/Tests.NetCore/TextSerializerTests.cs @@ -9,6 +9,12 @@ namespace Prometheus.Tests; [TestClass] public class TextSerializerTests { + [ClassInitialize] + public static void BeforeClass(TestContext testContext) + { + ObservedExemplar.NowProvider = new TestNowProvider(); + } + [TestMethod] public async Task ValidateTextFmtSummaryExposition_Labels() { @@ -94,6 +100,26 @@ public async Task ValidateTextFmtCounterExposition_Labels() "# TYPE boom_bam counter\n" + "boom_bam{blah=\"foo\"} 10\n"); } + + [TestMethod] + public async Task ValidateTextFmtCounterExposition_TotalSuffixInName() + { + var result = await TestCase.Run(factory => + { + var counter = factory.CreateCounter("boom_bam_total", "", new CounterConfiguration + { + LabelNames = new[] { "blah" } + }); + + counter.WithLabels("foo").IncTo(10); + }); + + // This tests that the counter exposition format isn't influenced by openmetrics codepaths when it comes to the + // _total suffix + result.ShouldBe("# HELP boom_bam_total\n" + + "# TYPE boom_bam_total counter\n" + + "boom_bam_total{blah=\"foo\"} 10\n"); + } [TestMethod] public async Task ValidateTextFmtHistogramExposition_Labels() @@ -125,7 +151,7 @@ public async Task ValidateTextFmtHistogramExposition_NoLabels() { var counter = factory.CreateHistogram("boom_bam", "something", new HistogramConfiguration { - Buckets = new[] { 1.0, 2 } + Buckets = new[] { 1.0, Math.Pow(10, 45) } }); counter.Observe(0.5); @@ -136,12 +162,119 @@ public async Task ValidateTextFmtHistogramExposition_NoLabels() boom_bam_sum 0.5 boom_bam_count 1 boom_bam_bucket{le=""1""} 1 -boom_bam_bucket{le=""2""} 1 +boom_bam_bucket{le=""1e+45""} 1 boom_bam_bucket{le=""+Inf""} 1 "); } + [TestMethod] + public async Task ValidateOpenMetricsFmtHistogram_Basic() + { + var result = await TestCase.RunOpenMetrics(factory => + { + var counter = factory.CreateHistogram("boom_bam", "something", new HistogramConfiguration + { + Buckets = new[] { 1, 2.5 } + }); + counter.Observe(1.5); + counter.Observe(1); + }); + + // This asserts that the le label has been modified and that we have a EOF + result.ShouldBe(@"# HELP boom_bam something +# TYPE boom_bam histogram +boom_bam_sum 2.5 +boom_bam_count 2.0 +boom_bam_bucket{le=""1.0""} 1.0 +boom_bam_bucket{le=""2.5""} 2.0 +boom_bam_bucket{le=""+Inf""} 2.0 +# EOF +"); + } + + [TestMethod] + public async Task ValidateOpenMetricsFmtHistogram_WithExemplar() + { + var result = await TestCase.RunOpenMetrics(factory => + { + var counter = factory.CreateHistogram("boom_bam", "something", new HistogramConfiguration + { + Buckets = new[] { 1, 2.5, 3, Math.Pow(10, 45)} + }); + + counter.Observe(1, Exemplar.Pair("traceID", "1")); + counter.Observe(1.5, Exemplar.Pair("traceID", "2")); + counter.Observe(4, Exemplar.Pair("traceID", "3")); + counter.Observe(Math.Pow(10,44), Exemplar.Pair("traceID", "4")); + }); + + // This asserts histogram OpenMetrics form with exemplars and also using numbers which are large enough for + // scientific notation + result.ShouldBe(@"# HELP boom_bam something +# TYPE boom_bam histogram +boom_bam_sum 1e+44 +boom_bam_count 4.0 +boom_bam_bucket{le=""1.0""} 1.0 # {traceID=""1""} 1.0 1668779954.714 +boom_bam_bucket{le=""2.5""} 2.0 # {traceID=""2""} 1.5 1668779954.714 +boom_bam_bucket{le=""3.0""} 2.0 +boom_bam_bucket{le=""1e+45""} 4.0 # {traceID=""4""} 1e+44 1668779954.714 +boom_bam_bucket{le=""+Inf""} 4.0 +# EOF +"); + } + + [TestMethod] + public async Task ValidateOpenMetricsFmtCounter_MultiItemExemplar() + { + var result = await TestCase.RunOpenMetrics(factory => + { + var counter = factory.CreateCounter("boom_bam", "", new CounterConfiguration + { + LabelNames = new[] { "blah" } + }); + + counter.WithLabels("foo").Inc(1, + Exemplar.Pair("traceID", "1234"), Exemplar.Pair("yaay", "4321")); + }); + // This asserts that multi-labeled exemplars work as well not supplying a _total suffix in the counter name. + result.ShouldBe(@"# HELP boom_bam +# TYPE boom_bam unknown +boom_bam{blah=""foo""} 1.0 # {traceID=""1234"",yaay=""4321""} 1.0 1668779954.714 +# EOF +"); + } + + [TestMethod] + public async Task ValidateOpenMetricsFmtCounter_TotalInNameSuffix() + { + var result = await TestCase.RunOpenMetrics(factory => + { + var counter = factory.CreateCounter("boom_bam_total", "", new CounterConfiguration + { + LabelNames = new[] { "blah" } + }); + + counter.WithLabels("foo").Inc(1, + Exemplar.Pair("traceID", "1234"), Exemplar.Pair("yaay", "4321")); + }); + // This tests the shape of OpenMetrics when _total suffix is supplied + result.ShouldBe(@"# HELP boom_bam +# TYPE boom_bam counter +boom_bam_total{blah=""foo""} 1.0 # {traceID=""1234"",yaay=""4321""} 1.0 1668779954.714 +# EOF +"); + } + + private class TestNowProvider : ObservedExemplar.INowProvider + { + public readonly double TestNow = 1668779954.714; + public double Now() + { + return TestNow; + } + } + private class TestCase { private readonly String raw; @@ -153,8 +286,12 @@ private TestCase(List lines, string raw) this.raw = raw; } + public static async Task RunOpenMetrics(Action register) + { + return await Run(register, ExpositionFormat.OpenMetricsText); + } - public static async Task Run(Action register) + public static async Task Run(Action register, ExpositionFormat format = ExpositionFormat.Text) { var registry = Metrics.NewCustomRegistry(); var factory = Metrics.WithCustomRegistry(registry); @@ -162,7 +299,7 @@ public static async Task Run(Action register) register(factory); using var stream = new MemoryStream(); - await registry.CollectAndExportAsTextAsync(stream); + await registry.CollectAndExportAsTextAsync(stream, format); var lines = new List(); stream.Position = 0; From c12bd9e9f008189b78b4faac2901320f2a2a7309 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 29 Dec 2022 09:40:17 +0200 Subject: [PATCH 013/230] History file update --- History | 3 +++ 1 file changed, 3 insertions(+) diff --git a/History b/History index 654678bf..001bed21 100644 --- a/History +++ b/History @@ -1,3 +1,6 @@ +* 8.0.0 +- Added OpenMetrics exposition format support (#388). +- Added exemplar support (#388). * 7.1.0 - Added back .NET Standard 2.0 support as some customers had a hard dependency on .NET Standard 2.0 (despite not being a documented feature even earlier). - Added (Observable)UpDownCounter support to MeterAdapter (.NET 7 specific feature). From c8ccdcb7b69db4a599722dc7e878491b38564e9c Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 29 Dec 2022 10:06:10 +0200 Subject: [PATCH 014/230] Add sample for exemplar export Refactor API surface of metric server setup to be more flexible Ignore invalid "Accept" header values in protocol negotiation --- Prometheus.AspNetCore/KestrelMetricServer.cs | 151 ++++++----- .../KestrelMetricServerOptions.cs | 37 ++- .../MetricServerMiddleware.cs | 165 +++++++----- .../MetricServerMiddlewareExtensions.cs | 95 +++++-- Prometheus/MetricHandler.cs | 7 +- Prometheus/MetricPusher.cs | 246 +++++++++--------- Prometheus/MetricServer.cs | 185 ++++++------- Prometheus/PrometheusConstants.cs | 23 +- README.md | 1 + Sample.Console.Exemplars/Program.cs | 47 ++++ .../Sample.Console.Exemplars.csproj | 21 ++ Sample.Web/Program.cs | 2 +- prometheus-net.sln | 6 + 13 files changed, 586 insertions(+), 400 deletions(-) create mode 100644 Sample.Console.Exemplars/Program.cs create mode 100644 Sample.Console.Exemplars/Sample.Console.Exemplars.csproj diff --git a/Prometheus.AspNetCore/KestrelMetricServer.cs b/Prometheus.AspNetCore/KestrelMetricServer.cs index fd3c8d91..3dca354a 100644 --- a/Prometheus.AspNetCore/KestrelMetricServer.cs +++ b/Prometheus.AspNetCore/KestrelMetricServer.cs @@ -6,90 +6,109 @@ using System.Net; using System.Security.Cryptography.X509Certificates; -namespace Prometheus +namespace Prometheus; + +/// +/// A stand-alone Kestrel based metric server that saves you the effort of setting up the ASP.NET Core pipeline. +/// For all practical purposes, this is just a regular ASP.NET Core server that only serves Prometheus requests. +/// +public sealed class KestrelMetricServer : MetricHandler { - /// - /// A stand-alone Kestrel based metric server that saves you the effort of setting up the ASP.NET Core pipeline. - /// For all practical purposes, this is just a regular ASP.NET Core server that only serves Prometheus requests. - /// - public sealed class KestrelMetricServer : MetricHandler + public KestrelMetricServer(int port, string url = "/metrics", CollectorRegistry? registry = null, X509Certificate2? certificate = null) : this("+", port, url, registry, certificate) { - public KestrelMetricServer(int port, string url = "/metrics", CollectorRegistry? registry = null, X509Certificate2? certificate = null) : this("+", port, url, registry, certificate) - { - } + } + + public KestrelMetricServer(string hostname, int port, string url = "/metrics", CollectorRegistry? registry = null, X509Certificate2? certificate = null) : this(LegacyOptions(hostname, port, url, registry, certificate)) + { + } - public KestrelMetricServer(string hostname, int port, string url = "/metrics", CollectorRegistry? registry = null, X509Certificate2? certificate = null) : base(registry) + private static KestrelMetricServerOptions LegacyOptions(string hostname, int port, string url, CollectorRegistry? registry, X509Certificate2? certificate) => + new KestrelMetricServerOptions { - _hostname = hostname; - _port = port; - _url = url; + Hostname = hostname, + Port = (ushort)port, + Url = url, + Registry = registry, + TlsCertificate = certificate, + }; - _certificate = certificate; - } + public KestrelMetricServer(KestrelMetricServerOptions options) + { + _hostname = options.Hostname; + _port = options.Port; + _url = options.Url; + _certificate = options.TlsCertificate; - public KestrelMetricServer(KestrelMetricServerOptions options) : this(options.Hostname, options.Port, options.Url, options.Registry, options.TlsCertificate) + // We use one callback to apply the legacy settings, and from within this we call the real callback. + _configureExporter = settings => { - } + // Legacy setting, may be overridden by ConfigureExporter. + settings.Registry = options.Registry; - private readonly string _hostname; - private readonly int _port; - private readonly string _url; + if (options.ConfigureExporter != null) + options.ConfigureExporter(settings); + }; + } - private readonly X509Certificate2? _certificate; + private readonly string _hostname; + private readonly int _port; + private readonly string _url; - protected override Task StartServer(CancellationToken cancel) - { - var s = _certificate != null ? "s" : ""; - var hostAddress = $"http{s}://{_hostname}:{_port}"; - - // If the caller needs to customize any of this, they can just set up their own web host and inject the middleware. - var builder = new WebHostBuilder() - .UseKestrel() - .UseIISIntegration() - .Configure(app => + private readonly X509Certificate2? _certificate; + + private readonly Action _configureExporter; + + protected override Task StartServer(CancellationToken cancel) + { + var s = _certificate != null ? "s" : ""; + var hostAddress = $"http{s}://{_hostname}:{_port}"; + + // If the caller needs to customize any of this, they can just set up their own web host and inject the middleware. + var builder = new WebHostBuilder() + .UseKestrel() + .UseIISIntegration() + .Configure(app => + { + app.UseMetricServer(_configureExporter, _url); + + // If there is any URL prefix, we just redirect people going to root URL to our prefix. + if (!string.IsNullOrWhiteSpace(_url.Trim('/'))) { - // _registry will already be pre-configured by MetricHandler. - app.UseMetricServer(_url, _registry); - - // If there is any URL prefix, we just redirect people going to root URL to our prefix. - if (!string.IsNullOrWhiteSpace(_url.Trim('/'))) - { - app.MapWhen(context => context.Request.Path.Value?.Trim('/') == "", - configuration => + app.MapWhen(context => context.Request.Path.Value?.Trim('/') == "", + configuration => + { + configuration.Use((HttpContext context, RequestDelegate next) => { - configuration.Use((HttpContext context, RequestDelegate next) => - { - context.Response.Redirect(_url); - return Task.CompletedTask; - }); + context.Response.Redirect(_url); + return Task.CompletedTask; }); - } - }); + }); + } + }); - if (_certificate != null) + if (_certificate != null) + { + builder = builder.ConfigureServices(services => { - builder = builder.ConfigureServices(services => + Action configureEndpoint = options => { - Action configureEndpoint = options => - { - options.UseHttps(_certificate); - }; - - services.Configure(options => - { - options.Listen(IPAddress.Any, _port, configureEndpoint); - }); + options.UseHttps(_certificate); + }; + + services.Configure(options => + { + options.Listen(IPAddress.Any, _port, configureEndpoint); }); - } - else - { - builder = builder.UseUrls(hostAddress); - } + }); + } + else + { + builder = builder.UseUrls(hostAddress); + } - var webHost = builder.Build(); - webHost.Start(); + var webHost = builder.Build(); + webHost.Start(); - return webHost.WaitForShutdownAsync(cancel); - } + return webHost.WaitForShutdownAsync(cancel); } } diff --git a/Prometheus.AspNetCore/KestrelMetricServerOptions.cs b/Prometheus.AspNetCore/KestrelMetricServerOptions.cs index 3118631e..817eebf5 100644 --- a/Prometheus.AspNetCore/KestrelMetricServerOptions.cs +++ b/Prometheus.AspNetCore/KestrelMetricServerOptions.cs @@ -1,18 +1,27 @@ -using System.Security.Cryptography.X509Certificates; +using System.ComponentModel; +using System.Security.Cryptography.X509Certificates; -namespace Prometheus +namespace Prometheus; + +public sealed class KestrelMetricServerOptions { - public sealed class KestrelMetricServerOptions - { - /// - /// Will listen for requests using this hostname. "+" indicates listen on all hostnames. - /// By setting this to "localhost", you can easily prevent access from remote systems.- - /// - public string Hostname { get; set; } = "+"; + /// + /// Will listen for requests using this hostname. "+" indicates listen on all hostnames. + /// By setting this to "localhost", you can easily prevent access from remote systems.- + /// + public string Hostname { get; set; } = "+"; + + public ushort Port { get; set; } + public string Url { get; set; } = "/metrics"; + public X509Certificate2? TlsCertificate { get; set; } + + // May be overridden by ConfigureExporter. + [EditorBrowsable(EditorBrowsableState.Never)] // It is not exactly obsolete but let's de-emphasize it and prefer ConfigureExporter. + public CollectorRegistry? Registry { get; set; } - public ushort Port { get; set; } - public string Url { get; set; } = "/metrics"; - public CollectorRegistry? Registry { get; set; } - public X509Certificate2? TlsCertificate { get; set; } - } + /// + /// Allows metric exporter options to be configured in a flexible manner. + /// The callback is called after applying any values in KestrelMetricServerOptions. + /// + public Action? ConfigureExporter { get; set; } } diff --git a/Prometheus.AspNetCore/MetricServerMiddleware.cs b/Prometheus.AspNetCore/MetricServerMiddleware.cs index 4668c336..bed4101b 100644 --- a/Prometheus.AspNetCore/MetricServerMiddleware.cs +++ b/Prometheus.AspNetCore/MetricServerMiddleware.cs @@ -1,82 +1,121 @@ -using System.Net.Http.Headers; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.AspNetCore.Http; +using System.Net.Http.Headers; -namespace Prometheus +namespace Prometheus; + +/// +/// Prometheus metrics export middleware for ASP.NET Core. +/// +/// You should use IApplicationBuilder.UseMetricServer extension method instead of using this class directly. +/// +public sealed class MetricServerMiddleware { - /// - /// Prometheus metrics export middleware for ASP.NET Core. - /// - /// You should use IApplicationBuilder.UseMetricServer extension method instead of using this class directly. - /// - public sealed class MetricServerMiddleware + public MetricServerMiddleware(RequestDelegate next, Settings settings) { - public MetricServerMiddleware(RequestDelegate next, Settings settings) - { - _registry = settings.Registry ?? Metrics.DefaultRegistry; - _enableOpenMetrics = settings.EnableOpenMetrics; - } + _registry = settings.Registry ?? Metrics.DefaultRegistry; + _enableOpenMetrics = settings.EnableOpenMetrics; + } + + public sealed class Settings + { + /// + /// Where do we take the metrics from. By default, we will take them from the global singleton registry. + /// + public CollectorRegistry? Registry { get; set; } + + /// + /// Whether we support the OpenMetrics exposition format. Required to publish exemplars. Defaults to enabled. + /// Use of OpenMetrics also requires that the client negotiate the OpenMetrics format via the HTTP "Accept" request header. + /// + public bool EnableOpenMetrics { get; set; } = true; + } + + private readonly CollectorRegistry _registry; + private readonly bool _enableOpenMetrics; - public sealed class Settings + private sealed record ProtocolNegotiationResult + { + public ExpositionFormat ExpositionFormat { get; } + public string ContentType { get; } + + public ProtocolNegotiationResult(ExpositionFormat expositionFormat, string contentType) { - public CollectorRegistry? Registry { get; set; } - public bool EnableOpenMetrics { get; set; } = false; + ExpositionFormat = expositionFormat; + ContentType = contentType; } + } - private readonly CollectorRegistry _registry; - private readonly bool _enableOpenMetrics; + private IEnumerable ExtractAcceptableMediaTypes(string acceptHeaderValue) + { + var candidates = acceptHeaderValue.Split(','); - private (ExpositionFormat, string) Negotiate(string header) + foreach (var candidate in candidates) { - foreach (var candidate in header.Split(',') - .Select(MediaTypeWithQualityHeaderValue.Parse) - .OrderByDescending(mt => mt.Quality.GetValueOrDefault(1))) - if (candidate.MediaType == PrometheusConstants.TextFmtContentType) - { - break; - } - else if (_enableOpenMetrics && candidate.MediaType == PrometheusConstants.OpenMetricsContentType) - { - return (ExpositionFormat.OpenMetricsText, PrometheusConstants.ExporterOpenMetricsContentType); - } - - return (ExpositionFormat.Text, PrometheusConstants.ExporterContentType); + // It is conceivably possible that some value is invalid - we filter them out here and only return valid values. + // A common case is a missing/empty "Accept" header, in which case we just get 1 candidate of empty string (which is invalid). + if (MediaTypeWithQualityHeaderValue.TryParse(candidate, out var mediaType)) + yield return mediaType; } - - public async Task Invoke(HttpContext context) + } + + private ProtocolNegotiationResult NegotiateComminucationProtocol(HttpRequest request) + { + var acceptHeader = request.Headers.Accept.ToString(); + + foreach (var candidate in ExtractAcceptableMediaTypes(acceptHeader) + .OrderByDescending(mt => mt.Quality.GetValueOrDefault(1))) { - var response = context.Response; - - try + if (candidate.MediaType == PrometheusConstants.TextContentType) { - var (fmt, contentType) = Negotiate(context.Request.Headers.Accept.ToString()); - // We first touch the response.Body only in the callback because touching - // it means we can no longer send headers (the status code). - var serializer = new TextSerializer(delegate - { - response.ContentType = contentType; - response.StatusCode = StatusCodes.Status200OK; - return response.Body; - }, fmt: fmt); - - await _registry.CollectAndSerializeAsync(serializer, context.RequestAborted); + // The first preference is the text format. Fall throgh to the default case. + break; } - catch (OperationCanceledException) when (context.RequestAborted.IsCancellationRequested) + else if (_enableOpenMetrics && candidate.MediaType == PrometheusConstants.OpenMetricsContentType) { - // The scrape was cancalled by the client. This is fine. Just swallow the exception to not generate pointless spam. + return new ProtocolNegotiationResult(ExpositionFormat.OpenMetricsText, PrometheusConstants.OpenMetricsContentTypeWithVersionAndEncoding); } - catch (ScrapeFailedException ex) + } + + return new ProtocolNegotiationResult(ExpositionFormat.Text, PrometheusConstants.TextContentTypeWithVersionAndEncoding); + } + + public async Task Invoke(HttpContext context) + { + var response = context.Response; + + try + { + var negotiationResult = NegotiateComminucationProtocol(context.Request); + + Stream GetResponseBodyStream() + { + // We first touch the response.Body only in the callback here because touching it means we can no longer send headers (the status code). + // The collection logic will delay calling this method until it is reasonably confident that nothing will go wrong will the collection. + response.ContentType = negotiationResult.ContentType; + response.StatusCode = StatusCodes.Status200OK; + + return response.Body; + } + + var serializer = new TextSerializer(GetResponseBodyStream(), negotiationResult.ExpositionFormat); + + await _registry.CollectAndSerializeAsync(serializer, context.RequestAborted); + } + catch (OperationCanceledException) when (context.RequestAborted.IsCancellationRequested) + { + // The scrape was cancalled by the client. This is fine. Just swallow the exception to not generate pointless spam. + } + catch (ScrapeFailedException ex) + { + // This can only happen before any serialization occurs, in the pre-collect callbacks. + // So it should still be safe to update the status code and write an error message. + response.StatusCode = StatusCodes.Status503ServiceUnavailable; + + if (!string.IsNullOrWhiteSpace(ex.Message)) { - // This can only happen before any serialization occurs, in the pre-collect callbacks. - // So it should still be safe to update the status code and write an error message. - response.StatusCode = StatusCodes.Status503ServiceUnavailable; - - if (!string.IsNullOrWhiteSpace(ex.Message)) - { - using (var writer = new StreamWriter(response.Body, PrometheusConstants.ExportEncoding, - bufferSize: -1, leaveOpen: true)) - await writer.WriteAsync(ex.Message); - } + using (var writer = new StreamWriter(response.Body, PrometheusConstants.ExportEncoding, + bufferSize: -1, leaveOpen: true)) + await writer.WriteAsync(ex.Message); } } } diff --git a/Prometheus.AspNetCore/MetricServerMiddlewareExtensions.cs b/Prometheus.AspNetCore/MetricServerMiddlewareExtensions.cs index 05da02d0..0271de9a 100644 --- a/Prometheus.AspNetCore/MetricServerMiddlewareExtensions.cs +++ b/Prometheus.AspNetCore/MetricServerMiddlewareExtensions.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; +using System.ComponentModel; namespace Prometheus { @@ -15,20 +16,13 @@ public static class MetricServerMiddlewareExtensions /// public static IEndpointConventionBuilder MapMetrics( this IEndpointRouteBuilder endpoints, - string pattern = "/metrics", - CollectorRegistry? registry = null, - bool enableOpenMetrics = false + Action configure, + string pattern = "/metrics" ) { var pipeline = endpoints .CreateApplicationBuilder() - .UseMiddleware( - new MetricServerMiddleware.Settings - { - EnableOpenMetrics = enableOpenMetrics, - Registry = registry - } - ) + .InternalUseMiddleware(configure) .Build(); return endpoints @@ -41,14 +35,17 @@ public static IEndpointConventionBuilder MapMetrics( /// The default URL is /metrics, which is a Prometheus convention. /// Use static methods on the class to create your metrics. /// - public static IApplicationBuilder UseMetricServer(this IApplicationBuilder builder, int port, - string? url = "/metrics", CollectorRegistry? registry = null, bool enableOpenMetrics = false) + public static IApplicationBuilder UseMetricServer( + this IApplicationBuilder builder, + int port, + Action configure, + string? url = "/metrics") { // If no URL, use root URL. url ??= "/"; return builder - .Map(url, b => b.MapWhen(PortMatches(), b1 => b1.InternalUseMiddleware(enableOpenMetrics, registry))); + .Map(url, b => b.MapWhen(PortMatches(), b1 => b1.InternalUseMiddleware(configure))); Func PortMatches() { @@ -61,23 +58,75 @@ Func PortMatches() /// The default URL is /metrics, which is a Prometheus convention. /// Use static methods on the class to create your metrics. /// - public static IApplicationBuilder UseMetricServer(this IApplicationBuilder builder, string? url = "/metrics", - CollectorRegistry? registry = null, bool enableOpenMetrics = false) + public static IApplicationBuilder UseMetricServer( + this IApplicationBuilder builder, + Action configure, + string? url = "/metrics") { - // If there is a URL to map, map it and re-enter without the URL. if (url != null) - return builder.Map(url, b => b.InternalUseMiddleware(enableOpenMetrics, registry)); + return builder.Map(url, b => b.InternalUseMiddleware(configure)); else - return builder.InternalUseMiddleware(enableOpenMetrics, registry); + return builder.InternalUseMiddleware(configure); } - private static IApplicationBuilder InternalUseMiddleware(this IApplicationBuilder builder, bool enableOpenMetrics, CollectorRegistry? registry = null) + #region Legacy methods without the configure action + /// + /// Starts a Prometheus metrics exporter using endpoint routing. + /// The default URL is /metrics, which is a Prometheus convention. + /// Use static methods on the class to create your metrics. + /// + [EditorBrowsable(EditorBrowsableState.Never)] // It is not exactly obsolete but let's de-emphasize it. + public static IEndpointConventionBuilder MapMetrics( + this IEndpointRouteBuilder endpoints, + string pattern = "/metrics", + CollectorRegistry? registry = null + ) { - return builder.UseMiddleware(new MetricServerMiddleware.Settings + return MapMetrics(endpoints, LegacyConfigure(registry), pattern); + } + + /// + /// Starts a Prometheus metrics exporter, filtering to only handle requests received on a specific port. + /// The default URL is /metrics, which is a Prometheus convention. + /// Use static methods on the class to create your metrics. + /// + [EditorBrowsable(EditorBrowsableState.Never)] // It is not exactly obsolete but let's de-emphasize it. + public static IApplicationBuilder UseMetricServer( + this IApplicationBuilder builder, + int port, + string? url = "/metrics", + CollectorRegistry? registry = null) + { + return UseMetricServer(builder, port, LegacyConfigure(registry), url); + } + + /// + /// Starts a Prometheus metrics exporter. + /// The default URL is /metrics, which is a Prometheus convention. + /// Use static methods on the class to create your metrics. + /// + [EditorBrowsable(EditorBrowsableState.Never)] // It is not exactly obsolete but let's de-emphasize it. + public static IApplicationBuilder UseMetricServer( + this IApplicationBuilder builder, + string? url = "/metrics", + CollectorRegistry? registry = null) + { + return UseMetricServer(builder, LegacyConfigure(registry), url); + } + + private static Action LegacyConfigure(CollectorRegistry? registry) => + (MetricServerMiddleware.Settings settings) => { - EnableOpenMetrics = enableOpenMetrics, - Registry = registry - }); + settings.Registry = registry; + }; + #endregion + + private static IApplicationBuilder InternalUseMiddleware(this IApplicationBuilder builder, Action configure) + { + var settings = new MetricServerMiddleware.Settings(); + configure(settings); + + return builder.UseMiddleware(settings); } } } diff --git a/Prometheus/MetricHandler.cs b/Prometheus/MetricHandler.cs index 9d382307..e57503a2 100644 --- a/Prometheus/MetricHandler.cs +++ b/Prometheus/MetricHandler.cs @@ -6,19 +6,14 @@ /// public abstract class MetricHandler : IMetricServer, IDisposable { - // The registry that contains the collectors to export metrics from. - // Subclasses are expected to use this variable to obtain the correct registry. - protected readonly CollectorRegistry _registry; - // The token is cancelled when the handler is instructed to stop. private CancellationTokenSource? _cts = new CancellationTokenSource(); // This is the task started for the purpose of exporting metrics. private Task? _task; - protected MetricHandler(CollectorRegistry? registry = null) + protected MetricHandler() { - _registry = registry ?? Metrics.DefaultRegistry; } public IMetricServer Start() diff --git a/Prometheus/MetricPusher.cs b/Prometheus/MetricPusher.cs index 7eeded35..fad648ff 100644 --- a/Prometheus/MetricPusher.cs +++ b/Prometheus/MetricPusher.cs @@ -1,162 +1,164 @@ using System.Diagnostics; using System.Text; -namespace Prometheus +namespace Prometheus; + +/// +/// A metric server that regularly pushes metrics to a Prometheus PushGateway. +/// +public class MetricPusher : MetricHandler { - /// - /// A metric server that regularly pushes metrics to a Prometheus PushGateway. - /// - public class MetricPusher : MetricHandler + private readonly TimeSpan _pushInterval; + private readonly HttpMethod _method; + private readonly Uri _targetUrl; + private readonly Func _httpClientProvider; + + public MetricPusher(string endpoint, string job, string? instance = null, long intervalMilliseconds = 1000, IEnumerable>? additionalLabels = null, CollectorRegistry? registry = null, bool pushReplace = false) : this(new MetricPusherOptions { - private readonly TimeSpan _pushInterval; - private readonly HttpMethod _method; - private readonly Uri _targetUrl; - private readonly Func _httpClientProvider; + Endpoint = endpoint, + Job = job, + Instance = instance, + IntervalMilliseconds = intervalMilliseconds, + AdditionalLabels = additionalLabels, + Registry = registry, + ReplaceOnPush = pushReplace, + }) + { + } - public MetricPusher(string endpoint, string job, string? instance = null, long intervalMilliseconds = 1000, IEnumerable>? additionalLabels = null, CollectorRegistry? registry = null, bool pushReplace = false) : this(new MetricPusherOptions - { - Endpoint = endpoint, - Job = job, - Instance = instance, - IntervalMilliseconds = intervalMilliseconds, - AdditionalLabels = additionalLabels, - Registry = registry, - ReplaceOnPush = pushReplace, - }) - { - } + public MetricPusher(MetricPusherOptions options) + { + if (string.IsNullOrEmpty(options.Endpoint)) + throw new ArgumentNullException(nameof(options.Endpoint)); - public MetricPusher(MetricPusherOptions options) : base(options.Registry) - { - if (string.IsNullOrEmpty(options.Endpoint)) - throw new ArgumentNullException(nameof(options.Endpoint)); + if (string.IsNullOrEmpty(options.Job)) + throw new ArgumentNullException(nameof(options.Job)); - if (string.IsNullOrEmpty(options.Job)) - throw new ArgumentNullException(nameof(options.Job)); + if (options.IntervalMilliseconds <= 0) + throw new ArgumentException("Interval must be greater than zero", nameof(options.IntervalMilliseconds)); - if (options.IntervalMilliseconds <= 0) - throw new ArgumentException("Interval must be greater than zero", nameof(options.IntervalMilliseconds)); + _registry = options.Registry ?? Metrics.DefaultRegistry; - _httpClientProvider = options.HttpClientProvider ?? (() => _singletonHttpClient); + _httpClientProvider = options.HttpClientProvider ?? (() => _singletonHttpClient); - StringBuilder sb = new StringBuilder(string.Format("{0}/job/{1}", options.Endpoint!.TrimEnd('/'), options.Job)); - if (!string.IsNullOrEmpty(options.Instance)) - sb.AppendFormat("/instance/{0}", options.Instance); + StringBuilder sb = new StringBuilder(string.Format("{0}/job/{1}", options.Endpoint!.TrimEnd('/'), options.Job)); + if (!string.IsNullOrEmpty(options.Instance)) + sb.AppendFormat("/instance/{0}", options.Instance); - if (options.AdditionalLabels != null) + if (options.AdditionalLabels != null) + { + foreach (var pair in options.AdditionalLabels) { - foreach (var pair in options.AdditionalLabels) - { - if (pair == null || string.IsNullOrEmpty(pair.Item1) || string.IsNullOrEmpty(pair.Item2)) - throw new NotSupportedException($"Invalid {nameof(MetricPusher)} additional label: ({pair?.Item1}):({pair?.Item2})"); + if (pair == null || string.IsNullOrEmpty(pair.Item1) || string.IsNullOrEmpty(pair.Item2)) + throw new NotSupportedException($"Invalid {nameof(MetricPusher)} additional label: ({pair?.Item1}):({pair?.Item2})"); - sb.AppendFormat("/{0}/{1}", pair.Item1, pair.Item2); - } + sb.AppendFormat("/{0}/{1}", pair.Item1, pair.Item2); } + } - Uri? targetUrl; - if (!Uri.TryCreate(sb.ToString(), UriKind.Absolute, out targetUrl) || targetUrl == null) - { - throw new ArgumentException("Endpoint must be a valid url", "endpoint"); - } + Uri? targetUrl; + if (!Uri.TryCreate(sb.ToString(), UriKind.Absolute, out targetUrl) || targetUrl == null) + { + throw new ArgumentException("Endpoint must be a valid url", "endpoint"); + } - _targetUrl = targetUrl; + _targetUrl = targetUrl; - _pushInterval = TimeSpan.FromMilliseconds(options.IntervalMilliseconds); - _onError = options.OnError; + _pushInterval = TimeSpan.FromMilliseconds(options.IntervalMilliseconds); + _onError = options.OnError; - _method = options.ReplaceOnPush ? HttpMethod.Put : HttpMethod.Post; - } + _method = options.ReplaceOnPush ? HttpMethod.Put : HttpMethod.Post; + } - private static readonly HttpClient _singletonHttpClient = new HttpClient(); + private static readonly HttpClient _singletonHttpClient = new HttpClient(); - private readonly Action? _onError; + private readonly CollectorRegistry _registry; + private readonly Action? _onError; - protected override Task StartServer(CancellationToken cancel) + protected override Task StartServer(CancellationToken cancel) + { + // Start the server processing loop asynchronously in the background. + return Task.Run(async delegate { - // Start the server processing loop asynchronously in the background. - return Task.Run(async delegate + while (true) { - while (true) + // We schedule approximately at the configured interval. There may be some small accumulation for the + // part of the loop we do not measure but it is close enough to be acceptable for all practical scenarios. + var duration = ValueStopwatch.StartNew(); + + try { - // We schedule approximately at the configured interval. There may be some small accumulation for the - // part of the loop we do not measure but it is close enough to be acceptable for all practical scenarios. - var duration = ValueStopwatch.StartNew(); + var httpClient = _httpClientProvider(); - try + var request = new HttpRequestMessage { - var httpClient = _httpClientProvider(); - - var request = new HttpRequestMessage + Method = _method, + RequestUri = _targetUrl, + // We use a copy-pasted implementation of PushStreamContent here to avoid taking a dependency on the old ASP.NET Web API where it lives. + Content = new PushStreamContentInternal(async (stream, content, context) => { - Method = _method, - RequestUri = _targetUrl, - // We use a copy-pasted implementation of PushStreamContent here to avoid taking a dependency on the old ASP.NET Web API where it lives. - Content = new PushStreamContentInternal(async (stream, content, context) => + try { - try - { - // Do not pass CT because we only want to cancel after pushing, so a flush is always performed. - await _registry.CollectAndExportAsTextAsync(stream, default); - } - finally - { - stream.Close(); - } - }, PrometheusConstants.ExporterContentTypeValue), - }; - - var response = await httpClient.SendAsync(request); - - // If anything goes wrong, we want to get at least an entry in the trace log. - response.EnsureSuccessStatusCode(); - } - catch (ScrapeFailedException ex) - { - // We do not consider failed scrapes a reportable error since the user code that raises the failure should be the one logging it. - Trace.WriteLine($"Skipping metrics push due to failed scrape: {ex.Message}"); - } - catch (Exception ex) - { - HandleFailedPush(ex); - } + // Do not pass CT because we only want to cancel after pushing, so a flush is always performed. + await _registry.CollectAndExportAsTextAsync(stream, default); + } + finally + { + stream.Close(); + } + }, PrometheusConstants.ExporterContentTypeValue), + }; - // We stop only after pushing metrics, to ensure that the latest state is flushed when told to stop. - if (cancel.IsCancellationRequested) - break; + var response = await httpClient.SendAsync(request); - var sleepTime = _pushInterval - duration.GetElapsedTime(); + // If anything goes wrong, we want to get at least an entry in the trace log. + response.EnsureSuccessStatusCode(); + } + catch (ScrapeFailedException ex) + { + // We do not consider failed scrapes a reportable error since the user code that raises the failure should be the one logging it. + Trace.WriteLine($"Skipping metrics push due to failed scrape: {ex.Message}"); + } + catch (Exception ex) + { + HandleFailedPush(ex); + } + + // We stop only after pushing metrics, to ensure that the latest state is flushed when told to stop. + if (cancel.IsCancellationRequested) + break; + + var sleepTime = _pushInterval - duration.GetElapsedTime(); - // Sleep until the interval elapses or the pusher is asked to shut down. - if (sleepTime > TimeSpan.Zero) + // Sleep until the interval elapses or the pusher is asked to shut down. + if (sleepTime > TimeSpan.Zero) + { + try { - try - { - await Task.Delay(sleepTime, cancel); - } - catch (OperationCanceledException) - { - // The task was cancelled. - // We continue the loop here to ensure final state gets pushed. - continue; - } + await Task.Delay(sleepTime, cancel); + } + catch (OperationCanceledException) + { + // The task was cancelled. + // We continue the loop here to ensure final state gets pushed. + continue; } } - }); - } + } + }); + } - private void HandleFailedPush(Exception ex) + private void HandleFailedPush(Exception ex) + { + if (_onError != null) { - if (_onError != null) - { - // Asynchronous because we don't trust the callee to be fast. - Task.Run(() => _onError(ex)); - } - else - { - // If there is no error handler registered, we write to trace to at least hopefully get some attention to the problem. - Trace.WriteLine(string.Format("Error in MetricPusher: {0}", ex)); - } + // Asynchronous because we don't trust the callee to be fast. + Task.Run(() => _onError(ex)); + } + else + { + // If there is no error handler registered, we write to trace to at least hopefully get some attention to the problem. + Trace.WriteLine(string.Format("Error in MetricPusher: {0}", ex)); } } } diff --git a/Prometheus/MetricServer.cs b/Prometheus/MetricServer.cs index 199bf9ed..405a7326 100644 --- a/Prometheus/MetricServer.cs +++ b/Prometheus/MetricServer.cs @@ -1,125 +1,128 @@ using System.Diagnostics; using System.Net; -namespace Prometheus +namespace Prometheus; + +/// +/// Implementation of a Prometheus exporter that serves metrics using HttpListener. +/// This is a stand-alone exporter for apps that do not already have an HTTP server included. +/// +public class MetricServer : MetricHandler { + private readonly HttpListener _httpListener = new HttpListener(); + /// - /// Implementation of a Prometheus exporter that serves metrics using HttpListener. - /// This is a stand-alone exporter for apps that do not already have an HTTP server included. + /// Only requests that match this predicate will be served by the metric server. This allows you to add authorization checks. + /// By default (if null), all requests are served. /// - public class MetricServer : MetricHandler + public Func? RequestPredicate { get; set; } + + public MetricServer(int port, string url = "metrics/", CollectorRegistry? registry = null, bool useHttps = false) : this("+", port, url, registry, useHttps) { - private readonly HttpListener _httpListener = new HttpListener(); + } - /// - /// Only requests that match this predicate will be served by the metric server. This allows you to add authorization checks. - /// By default (if null), all requests are served. - /// - public Func? RequestPredicate { get; set; } + public MetricServer(string hostname, int port, string url = "metrics/", CollectorRegistry? registry = null, bool useHttps = false) + { + var s = useHttps ? "s" : ""; + _httpListener.Prefixes.Add($"http{s}://{hostname}:{port}/{url}"); - public MetricServer(int port, string url = "metrics/", CollectorRegistry? registry = null, bool useHttps = false) : this("+", port, url, registry, useHttps) - { - } + _registry = registry ?? Metrics.DefaultRegistry; + } - public MetricServer(string hostname, int port, string url = "metrics/", CollectorRegistry? registry = null, bool useHttps = false) : base(registry) - { - var s = useHttps ? "s" : ""; - _httpListener.Prefixes.Add($"http{s}://{hostname}:{port}/{url}"); - } + private readonly CollectorRegistry _registry; - protected override Task StartServer(CancellationToken cancel) - { - // This will ensure that any failures to start are nicely thrown from StartServerAsync. - _httpListener.Start(); + protected override Task StartServer(CancellationToken cancel) + { + // This will ensure that any failures to start are nicely thrown from StartServerAsync. + _httpListener.Start(); - // Kick off the actual processing to a new thread and return a Task for the processing thread. - return Task.Factory.StartNew(delegate + // Kick off the actual processing to a new thread and return a Task for the processing thread. + return Task.Factory.StartNew(delegate + { + try { - try + Thread.CurrentThread.Name = "Metric Server"; //Max length 16 chars (Linux limitation) + + while (!cancel.IsCancellationRequested) { - Thread.CurrentThread.Name = "Metric Server"; //Max length 16 chars (Linux limitation) + // There is no way to give a CancellationToken to GCA() so, we need to hack around it a bit. + var getContext = _httpListener.GetContextAsync(); + getContext.Wait(cancel); + var context = getContext.Result; - while (!cancel.IsCancellationRequested) + // Asynchronously process the request. + _ = Task.Factory.StartNew(async delegate { - // There is no way to give a CancellationToken to GCA() so, we need to hack around it a bit. - var getContext = _httpListener.GetContextAsync(); - getContext.Wait(cancel); - var context = getContext.Result; + var request = context.Request; + var response = context.Response; - // Asynchronously process the request. - _ = Task.Factory.StartNew(async delegate + try { - var request = context.Request; - var response = context.Response; + var predicate = RequestPredicate; - try + if (predicate != null && !predicate(request)) { - var predicate = RequestPredicate; + // Request rejected by predicate. + response.StatusCode = (int)HttpStatusCode.Forbidden; + return; + } - if (predicate != null && !predicate(request)) + try + { + // We first touch the response.OutputStream only in the callback because touching + // it means we can no longer send headers (the status code). + var serializer = new TextSerializer(delegate { - // Request rejected by predicate. - response.StatusCode = (int)HttpStatusCode.Forbidden; - return; - } + response.ContentType = PrometheusConstants.TextContentTypeWithVersionAndEncoding; + response.StatusCode = 200; + return response.OutputStream; + }); - try - { - // We first touch the response.OutputStream only in the callback because touching - // it means we can no longer send headers (the status code). - var serializer = new TextSerializer(delegate - { - response.ContentType = PrometheusConstants.ExporterContentType; - response.StatusCode = 200; - return response.OutputStream; - }); - - await _registry.CollectAndSerializeAsync(serializer, cancel); - response.OutputStream.Dispose(); - } - catch (ScrapeFailedException ex) - { - // This can only happen before anything is written to the stream, so it - // should still be safe to update the status code and report an error. - response.StatusCode = 503; - - if (!string.IsNullOrWhiteSpace(ex.Message)) - { - using (var writer = new StreamWriter(response.OutputStream)) - writer.Write(ex.Message); - } - } + await _registry.CollectAndSerializeAsync(serializer, cancel); + response.OutputStream.Dispose(); } - catch (Exception ex) when (!(ex is OperationCanceledException)) + catch (ScrapeFailedException ex) { - if (!_httpListener.IsListening) - return; // We were shut down. - - Trace.WriteLine(string.Format("Error in {0}: {1}", nameof(MetricServer), ex)); + // This can only happen before anything is written to the stream, so it + // should still be safe to update the status code and report an error. + response.StatusCode = 503; - try + if (!string.IsNullOrWhiteSpace(ex.Message)) { - response.StatusCode = 500; - } - catch - { - // Might be too late in request processing to set response code, so just ignore. + using (var writer = new StreamWriter(response.OutputStream)) + writer.Write(ex.Message); } } - finally + } + catch (Exception ex) when (!(ex is OperationCanceledException)) + { + if (!_httpListener.IsListening) + return; // We were shut down. + + Trace.WriteLine(string.Format("Error in {0}: {1}", nameof(MetricServer), ex)); + + try { - response.Close(); + response.StatusCode = 500; } - }); - } - } - finally - { - _httpListener.Stop(); - // This should prevent any currently processed requests from finishing. - _httpListener.Close(); + catch + { + // Might be too late in request processing to set response code, so just ignore. + } + } + finally + { + response.Close(); + } + }); } - }, TaskCreationOptions.LongRunning); - } + } + finally + { + _httpListener.Stop(); + // This should prevent any currently processed requests from finishing. + _httpListener.Close(); + } + }, TaskCreationOptions.LongRunning); } } diff --git a/Prometheus/PrometheusConstants.cs b/Prometheus/PrometheusConstants.cs index f015ef54..8f282a3d 100644 --- a/Prometheus/PrometheusConstants.cs +++ b/Prometheus/PrometheusConstants.cs @@ -5,22 +5,17 @@ namespace Prometheus { public static class PrometheusConstants { - public const string - TextFmtContentType = "text/plain", - OpenMetricsContentType = "application/openmetrics-text"; - - public const string - ExporterContentType = TextFmtContentType + "; version=0.0.4; charset=utf-8", - ExporterOpenMetricsContentType = OpenMetricsContentType + "; version=0.0.1; charset=utf-8"; - + public const string TextContentType = "text/plain"; + public const string OpenMetricsContentType = "application/openmetrics-text"; + + public const string TextContentTypeWithVersionAndEncoding = TextContentType + "; version=0.0.4; charset=utf-8"; + public const string OpenMetricsContentTypeWithVersionAndEncoding = OpenMetricsContentType + "; version=0.0.1; charset=utf-8"; // ASP.NET requires a MediaTypeHeaderValue object - public static readonly MediaTypeHeaderValue - ExporterContentTypeValue = MediaTypeHeaderValue.Parse(ExporterContentType), - ExporterOpenMetricsContentTypeValue = MediaTypeHeaderValue.Parse(ExporterOpenMetricsContentType); + public static readonly MediaTypeHeaderValue ExporterContentTypeValue = MediaTypeHeaderValue.Parse(TextContentTypeWithVersionAndEncoding); + public static readonly MediaTypeHeaderValue ExporterOpenMetricsContentTypeValue = MediaTypeHeaderValue.Parse(OpenMetricsContentTypeWithVersionAndEncoding); - // Use UTF-8 encoding, but provide the flag to ensure the Unicode Byte Order Mark is never - // pre-pended to the output stream. - public static readonly Encoding ExportEncoding = new UTF8Encoding(false); + // Use UTF-8 encoding, but provide the flag to ensure the Unicode Byte Order Mark is never prepended to the output stream. + public static readonly Encoding ExportEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); } } \ No newline at end of file diff --git a/README.md b/README.md index 40882f60..cbb8f20a 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,7 @@ Refer to the sample projects for quick start instructions: | [Sample.Web](Sample.Web/Program.cs) | ASP.NET Core application that produces custom metrics and uses multiple integrations to publish built-in metrics | | [Sample.Console](Sample.Console/Program.cs) | .NET console application that exports custom metrics | | [Sample.Console.DotNetMeters](Sample.Console.DotNetMeters/Program.cs) | Demonstrates how to [publish custom metrics via the .NET Meters API](#net-meters-integration) | +| [Sample.Console.Exemplars](Sample.Console.Exemplars/Program.cs) | .NET console application that attaches exemplars to some metrics | | [Sample.Console.NetFramework](Sample.Console.NetFramework/Program.cs) | Same as above but targeting .NET Framework | | [Sample.Console.NoAspNetCore](Sample.Console.NoAspNetCore/Program.cs) | .NET console application that exports custom metrics without requiring the ASP.NET Core runtime to be installed | | [Sample.Grpc](Sample.Grpc/Program.cs) | ASP.NET Core application that publishes a gRPC service | diff --git a/Sample.Console.Exemplars/Program.cs b/Sample.Console.Exemplars/Program.cs new file mode 100644 index 00000000..35268e02 --- /dev/null +++ b/Sample.Console.Exemplars/Program.cs @@ -0,0 +1,47 @@ +using Prometheus; + +// This sample demonstrates how to attach exemplars to metrics exposed by a .NET console app. +// +// NuGet packages required: +// * prometheus-net.AspNetCore + +// Suppress some default metrics to make the output cleaner, so the exemplars are easier to see. +Metrics.SuppressDefaultMetrics(new SuppressDefaultMetricOptions +{ + SuppressEventCounters = true, + SuppressMeters = true, + SuppressProcessMetrics = true +}); + +// Start the metrics server on your preferred port number. +using var server = new KestrelMetricServer(port: 1234); +server.Start(); + +// Generate some sample data from fake business logic. +var recordsProcessed = Metrics.CreateCounter("sample_records_processed_total", "Total number of records processed."); + +// The key from an exemplar key-value pair should be created once and reused to minimize memory allocations. +var recordIdKey = Exemplar.Key("record-id"); + +_ = Task.Run(async delegate +{ + while (true) + { + // Pretend to process a record approximately every second, just for changing sample data. + var recordId = Guid.NewGuid(); + + // We pass the record ID key-value pair when we increment the metric. + // When the metric data is published to Prometheus, the most recent record ID will be attached to it. + var exemplar = recordIdKey.WithValue(recordId.ToString()); + recordsProcessed.Inc(exemplar); + + await Task.Delay(TimeSpan.FromSeconds(1)); + } +}); + +// Metrics published in this sample: +// * the custom sample counter defined above, with exemplars +// * internal debug metrics from prometheus-net, without exemplars +Console.WriteLine("Open http://localhost:1234/metrics in a web browser."); +Console.WriteLine("Press enter to exit."); +Console.ReadLine(); \ No newline at end of file diff --git a/Sample.Console.Exemplars/Sample.Console.Exemplars.csproj b/Sample.Console.Exemplars/Sample.Console.Exemplars.csproj new file mode 100644 index 00000000..62836eb9 --- /dev/null +++ b/Sample.Console.Exemplars/Sample.Console.Exemplars.csproj @@ -0,0 +1,21 @@ + + + + Exe + net6.0 + + enable + enable + True + True + 1591 + + latest + 9999 + + + + + + + diff --git a/Sample.Web/Program.cs b/Sample.Web/Program.cs index ff716b3f..451322cc 100644 --- a/Sample.Web/Program.cs +++ b/Sample.Web/Program.cs @@ -53,7 +53,7 @@ // * metrics about requests handled by the web app (configured above) // * ASP.NET health check statuses (configured above) // * custom business logic metrics published by the SampleService class - endpoints.MapMetrics(enableOpenMetrics:true); + endpoints.MapMetrics(); }); app.Run(); diff --git a/prometheus-net.sln b/prometheus-net.sln index 8679b40a..33799d9e 100644 --- a/prometheus-net.sln +++ b/prometheus-net.sln @@ -57,6 +57,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.Console.NoAspNetCore EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.NetStandard", "Sample.NetStandard\Sample.NetStandard.csproj", "{4F91DAA7-9DD3-418F-A276-84ABFED388A5}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample.Console.Exemplars", "Sample.Console.Exemplars\Sample.Console.Exemplars.csproj", "{7F4947F1-C9DD-42F9-867D-5AC931478205}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -155,6 +157,10 @@ Global {4F91DAA7-9DD3-418F-A276-84ABFED388A5}.Debug|Any CPU.Build.0 = Debug|Any CPU {4F91DAA7-9DD3-418F-A276-84ABFED388A5}.Release|Any CPU.ActiveCfg = Release|Any CPU {4F91DAA7-9DD3-418F-A276-84ABFED388A5}.Release|Any CPU.Build.0 = Release|Any CPU + {7F4947F1-C9DD-42F9-867D-5AC931478205}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7F4947F1-C9DD-42F9-867D-5AC931478205}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7F4947F1-C9DD-42F9-867D-5AC931478205}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7F4947F1-C9DD-42F9-867D-5AC931478205}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 421e45453aa916c78817e803068fa72df6ecfe7f Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 29 Dec 2022 10:11:59 +0200 Subject: [PATCH 015/230] OpenMetrics spec says use version 1.0.0 --- Prometheus/PrometheusConstants.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Prometheus/PrometheusConstants.cs b/Prometheus/PrometheusConstants.cs index 8f282a3d..11f65a4e 100644 --- a/Prometheus/PrometheusConstants.cs +++ b/Prometheus/PrometheusConstants.cs @@ -9,7 +9,7 @@ public static class PrometheusConstants public const string OpenMetricsContentType = "application/openmetrics-text"; public const string TextContentTypeWithVersionAndEncoding = TextContentType + "; version=0.0.4; charset=utf-8"; - public const string OpenMetricsContentTypeWithVersionAndEncoding = OpenMetricsContentType + "; version=0.0.1; charset=utf-8"; + public const string OpenMetricsContentTypeWithVersionAndEncoding = OpenMetricsContentType + "; version=1.0.0; charset=utf-8"; // ASP.NET requires a MediaTypeHeaderValue object public static readonly MediaTypeHeaderValue ExporterContentTypeValue = MediaTypeHeaderValue.Parse(TextContentTypeWithVersionAndEncoding); From 67859724b717f491100faff5c02628b3ec3c0c56 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 29 Dec 2022 10:23:24 +0200 Subject: [PATCH 016/230] Enable exemplars for LocalMetricsCollector --- LocalMetricsCollector/Dockerfile | 4 ++-- LocalMetricsCollector/prometheus.yml | 1 + LocalMetricsCollector/run.sh | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/LocalMetricsCollector/Dockerfile b/LocalMetricsCollector/Dockerfile index 5a4bce22..61ff408b 100644 --- a/LocalMetricsCollector/Dockerfile +++ b/LocalMetricsCollector/Dockerfile @@ -13,8 +13,8 @@ RUN tdnf install -y \ ################### Based on https://github.com/prometheus/prometheus/blob/main/Dockerfile -ARG PROMETHEUS_PACKAGE_NAME=prometheus-2.37.1.linux-amd64 -RUN wget --no-verbose -O prometheus.tar.gz https://github.com/prometheus/prometheus/releases/download/v2.37.1/$PROMETHEUS_PACKAGE_NAME.tar.gz +ARG PROMETHEUS_PACKAGE_NAME=prometheus-2.41.0.linux-amd64 +RUN wget --no-verbose -O prometheus.tar.gz https://github.com/prometheus/prometheus/releases/download/v2.41.0/$PROMETHEUS_PACKAGE_NAME.tar.gz RUN tar xvfz prometheus.tar.gz RUN cp /$PROMETHEUS_PACKAGE_NAME/prometheus /bin/ RUN cp /$PROMETHEUS_PACKAGE_NAME/promtool /bin/ diff --git a/LocalMetricsCollector/prometheus.yml b/LocalMetricsCollector/prometheus.yml index da1e0b4d..885d1252 100644 --- a/LocalMetricsCollector/prometheus.yml +++ b/LocalMetricsCollector/prometheus.yml @@ -7,6 +7,7 @@ global: remote_write: - url: https://prometheus-prod-01-eu-west-0.grafana.net/api/prom/push + send_exemplars: true basic_auth: username: $GRAFANA_USER password: $GRAFANA_API_KEY diff --git a/LocalMetricsCollector/run.sh b/LocalMetricsCollector/run.sh index 60fa19af..ab0c4e07 100644 --- a/LocalMetricsCollector/run.sh +++ b/LocalMetricsCollector/run.sh @@ -6,4 +6,4 @@ set -e envsubst < /app/prometheus.yml > /etc/prometheus/prometheus.yml # We must listen on 0.0.0.0 here because otherwise the liveness/readiness probes cannot reach us. -exec /bin/prometheus --web.listen-address=0.0.0.0:$PROMETHEUS_PORT --storage.tsdb.retention.time=5m --storage.tsdb.min-block-duration=2m --config.file=/etc/prometheus/prometheus.yml --storage.tsdb.path=/prometheus --web.console.libraries=/usr/share/prometheus/console_libraries --web.console.templates=/usr/share/prometheus/consoles \ No newline at end of file +exec /bin/prometheus --web.listen-address=0.0.0.0:$PROMETHEUS_PORT --storage.tsdb.retention.time=5m --storage.tsdb.min-block-duration=2m --config.file=/etc/prometheus/prometheus.yml --storage.tsdb.path=/prometheus --web.console.libraries=/usr/share/prometheus/console_libraries --web.console.templates=/usr/share/prometheus/consoles --enable-feature=exemplar-storage \ No newline at end of file From 4a14dd4062eb45c7a8f0ce311400deb8ec0e730e Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 29 Dec 2022 10:23:51 +0200 Subject: [PATCH 017/230] Disallow colon in metric and label names Not allowed by OpenMetrics. Also, Prometheus specs say colon is reserved for recording rules. --- Prometheus/Collector.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Prometheus/Collector.cs b/Prometheus/Collector.cs index 3dd8c45f..8287bf48 100644 --- a/Prometheus/Collector.cs +++ b/Prometheus/Collector.cs @@ -51,8 +51,8 @@ public abstract class Collector // Used by ChildBase.Remove() internal abstract void RemoveLabelled(LabelSequence instanceLabels); - private const string ValidMetricNameExpression = "^[a-zA-Z_:][a-zA-Z0-9_:]*$"; - private const string ValidLabelNameExpression = "^[a-zA-Z_:][a-zA-Z0-9_:]*$"; + private const string ValidMetricNameExpression = "^[a-zA-Z_][a-zA-Z0-9_]*$"; + private const string ValidLabelNameExpression = "^[a-zA-Z_][a-zA-Z0-9_]*$"; private const string ReservedLabelNameExpression = "^__.*$"; private static readonly Regex MetricNameRegex = new Regex(ValidMetricNameExpression, RegexOptions.Compiled); From 4d5ad5230d2f9fe733e69d1704310b2f9cb339d0 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 29 Dec 2022 10:24:26 +0200 Subject: [PATCH 018/230] History --- History | 1 + 1 file changed, 1 insertion(+) diff --git a/History b/History index 001bed21..6837b191 100644 --- a/History +++ b/History @@ -1,6 +1,7 @@ * 8.0.0 - Added OpenMetrics exposition format support (#388). - Added exemplar support (#388). +- The ':' character is no longer allowed in metric or label names. For metric names, Prometheus standard practice is to use colon only in recording rules. * 7.1.0 - Added back .NET Standard 2.0 support as some customers had a hard dependency on .NET Standard 2.0 (despite not being a documented feature even earlier). - Added (Observable)UpDownCounter support to MeterAdapter (.NET 7 specific feature). From f1c4b7df91c2253cf4a655901c2b1c621f6fdc30 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 29 Dec 2022 10:24:41 +0200 Subject: [PATCH 019/230] Validate exemplar key names (==label) --- Prometheus/Exemplar.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Prometheus/Exemplar.cs b/Prometheus/Exemplar.cs index 82a55a13..93ccc5ae 100644 --- a/Prometheus/Exemplar.cs +++ b/Prometheus/Exemplar.cs @@ -51,14 +51,14 @@ internal LabelPair(byte[] keyBytes, byte[] valueBytes, int runeCount) /// /// Return an exemplar label key, this may be curried with a value to produce a LabelPair. - /// - /// The string is expected to only contain runes in the ASCII range, runes outside the ASCII range will get replaced - /// with placeholders. This constraint may be relaxed with future versions. /// public static LabelKey Key(string key) { if (string.IsNullOrWhiteSpace(key)) throw new ArgumentException("empty key"); + + Collector.ValidateLabelName(key); + var asciiBytes = Encoding.ASCII.GetBytes(key); return new LabelKey(asciiBytes, asciiBytes.Length); } From 4b3a16515fb13a531b8f37e2a9e97ab843e72d7e Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 29 Dec 2022 10:26:17 +0200 Subject: [PATCH 020/230] Fix bad exemplar key name in sample app --- Sample.Console.Exemplars/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sample.Console.Exemplars/Program.cs b/Sample.Console.Exemplars/Program.cs index 35268e02..2e16bae2 100644 --- a/Sample.Console.Exemplars/Program.cs +++ b/Sample.Console.Exemplars/Program.cs @@ -21,7 +21,7 @@ var recordsProcessed = Metrics.CreateCounter("sample_records_processed_total", "Total number of records processed."); // The key from an exemplar key-value pair should be created once and reused to minimize memory allocations. -var recordIdKey = Exemplar.Key("record-id"); +var recordIdKey = Exemplar.Key("record_id"); _ = Task.Run(async delegate { From e17fb522c1724de5f26b13ff9a4ffd2301e609aa Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 29 Dec 2022 14:04:41 +0200 Subject: [PATCH 021/230] Add histogram to exemplars example --- Sample.Console.Exemplars/Program.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Sample.Console.Exemplars/Program.cs b/Sample.Console.Exemplars/Program.cs index 2e16bae2..72049dc5 100644 --- a/Sample.Console.Exemplars/Program.cs +++ b/Sample.Console.Exemplars/Program.cs @@ -19,6 +19,10 @@ // Generate some sample data from fake business logic. var recordsProcessed = Metrics.CreateCounter("sample_records_processed_total", "Total number of records processed."); +var recordSizeInPages = Metrics.CreateHistogram("sample_record_size_pages", "Size of a record, in pages.", new HistogramConfiguration +{ + Buckets = Histogram.PowersOfTenDividedBuckets(0, 2, 10) +}); // The key from an exemplar key-value pair should be created once and reused to minimize memory allocations. var recordIdKey = Exemplar.Key("record_id"); @@ -29,11 +33,14 @@ { // Pretend to process a record approximately every second, just for changing sample data. var recordId = Guid.NewGuid(); + var recordPageCount = Random.Shared.Next(minValue: 5, maxValue: 100); // We pass the record ID key-value pair when we increment the metric. // When the metric data is published to Prometheus, the most recent record ID will be attached to it. - var exemplar = recordIdKey.WithValue(recordId.ToString()); - recordsProcessed.Inc(exemplar); + var recordIdKeyValuePair = recordIdKey.WithValue(recordId.ToString()); + + recordsProcessed.Inc(recordIdKeyValuePair); + recordSizeInPages.Observe(recordPageCount, recordIdKeyValuePair); await Task.Delay(TimeSpan.FromSeconds(1)); } From 3ae5f5b8e5036fe72e6107413c17af2c84332e81 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 29 Dec 2022 14:34:03 +0200 Subject: [PATCH 022/230] Fix tests to follow new naming regex rule --- Tests.NetCore/MetricsTests.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Tests.NetCore/MetricsTests.cs b/Tests.NetCore/MetricsTests.cs index 7b680733..90ba000d 100644 --- a/Tests.NetCore/MetricsTests.cs +++ b/Tests.NetCore/MetricsTests.cs @@ -347,20 +347,20 @@ public void metric_names() Assert.ThrowsException(() => _metrics.CreateGauge("my!metric", "help")); Assert.ThrowsException(() => _metrics.CreateGauge("%", "help")); Assert.ThrowsException(() => _metrics.CreateGauge("5a", "help")); + Assert.ThrowsException(() => _metrics.CreateGauge("a:3", "help")); _metrics.CreateGauge("abc", "help"); _metrics.CreateGauge("myMetric2", "help"); - _metrics.CreateGauge("a:3", "help"); } [TestMethod] public void label_names() { - Assert.ThrowsException(() => _metrics.CreateGauge("a", "help1", "my-metric")); - Assert.ThrowsException(() => _metrics.CreateGauge("a", "help1", "my!metric")); - Assert.ThrowsException(() => _metrics.CreateGauge("a", "help1", "my%metric")); + Assert.ThrowsException(() => _metrics.CreateGauge("a", "help1", "my-label")); + Assert.ThrowsException(() => _metrics.CreateGauge("a", "help1", "my!label")); + Assert.ThrowsException(() => _metrics.CreateGauge("a", "help1", "my%label")); Assert.ThrowsException(() => _metrics.CreateHistogram("a", "help1", "le")); - _metrics.CreateGauge("a", "help1", "my:metric"); + Assert.ThrowsException(() => _metrics.CreateHistogram("a", "help1", "my:label")); _metrics.CreateGauge("b", "help1", "good_name"); Assert.ThrowsException(() => _metrics.CreateGauge("c", "help1", "__reserved")); From aab2166b87ff9b9c4c585a9db9aa19427c49445a Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 29 Dec 2022 14:34:26 +0200 Subject: [PATCH 023/230] Code tidy --- Prometheus/ChildBase.cs | 141 +++++++++++++++--------------- Prometheus/Counter.cs | 144 +++++++++++++++---------------- Prometheus/Histogram.cs | 36 ++++---- Prometheus/IMetricsSerializer.cs | 52 ++++++----- Prometheus/ObservedExemplar.cs | 73 +++++++++------- Prometheus/TextSerializer.cs | 2 +- 6 files changed, 230 insertions(+), 218 deletions(-) diff --git a/Prometheus/ChildBase.cs b/Prometheus/ChildBase.cs index f54726cd..a5b1f719 100644 --- a/Prometheus/ChildBase.cs +++ b/Prometheus/ChildBase.cs @@ -1,87 +1,86 @@ -namespace Prometheus +namespace Prometheus; + +/// +/// Base class for labeled instances of metrics (with all label names and label values defined). +/// +public abstract class ChildBase : ICollectorChild, IDisposable { + internal ChildBase(Collector parent, LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish) + { + Parent = parent; + InstanceLabels = instanceLabels; + FlattenedLabels = flattenedLabels; + FlattenedLabelsBytes = PrometheusConstants.ExportEncoding.GetBytes(flattenedLabels.Serialize()); + _publish = publish; + } + /// - /// Base class for labeled instances of metrics (with all label names and label values defined). + /// Marks the metric as one to be published, even if it might otherwise be suppressed. + /// + /// This is useful for publishing zero-valued metrics once you have loaded data on startup and determined + /// that there is no need to increment the value of the metric. /// - public abstract class ChildBase : ICollectorChild, IDisposable + /// + /// Subclasses must call this when their value is first set, to mark the metric as published. + /// + public void Publish() { - internal ChildBase(Collector parent, LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish) - { - Parent = parent; - InstanceLabels = instanceLabels; - FlattenedLabels = flattenedLabels; - FlattenedLabelsBytes = PrometheusConstants.ExportEncoding.GetBytes(flattenedLabels.Serialize()); - _publish = publish; - } - - /// - /// Marks the metric as one to be published, even if it might otherwise be suppressed. - /// - /// This is useful for publishing zero-valued metrics once you have loaded data on startup and determined - /// that there is no need to increment the value of the metric. - /// - /// - /// Subclasses must call this when their value is first set, to mark the metric as published. - /// - public void Publish() - { - Volatile.Write(ref _publish, true); - } - - /// - /// Marks the metric as one to not be published. - /// - /// The metric will be published when Publish() is called or the value is updated. - /// - public void Unpublish() - { - Volatile.Write(ref _publish, false); - } + Volatile.Write(ref _publish, true); + } - /// - /// Removes this labeled instance from metrics. - /// It will no longer be published and any existing measurements/buckets will be discarded. - /// - public void Remove() - { - Parent.RemoveLabelled(InstanceLabels); - } + /// + /// Marks the metric as one to not be published. + /// + /// The metric will be published when Publish() is called or the value is updated. + /// + public void Unpublish() + { + Volatile.Write(ref _publish, false); + } - public void Dispose() => Remove(); + /// + /// Removes this labeled instance from metrics. + /// It will no longer be published and any existing measurements/buckets will be discarded. + /// + public void Remove() + { + Parent.RemoveLabelled(InstanceLabels); + } - /// - /// Labels specific to this metric instance, without any inherited static labels. - /// Internal for testing purposes only. - /// - internal LabelSequence InstanceLabels { get; } + public void Dispose() => Remove(); - /// - /// All labels that materialize on this metric instance, including inherited static labels. - /// Internal for testing purposes only. - /// - internal LabelSequence FlattenedLabels { get; } + /// + /// Labels specific to this metric instance, without any inherited static labels. + /// Internal for testing purposes only. + /// + internal LabelSequence InstanceLabels { get; } - internal byte[] FlattenedLabelsBytes { get; } + /// + /// All labels that materialize on this metric instance, including inherited static labels. + /// Internal for testing purposes only. + /// + internal LabelSequence FlattenedLabels { get; } - internal readonly Collector Parent; + internal byte[] FlattenedLabelsBytes { get; } - private bool _publish; + internal readonly Collector Parent; - /// - /// Collects all the metric data rows from this collector and serializes it using the given serializer. - /// - /// - /// Subclass must check _publish and suppress output if it is false. - /// - internal Task CollectAndSerializeAsync(IMetricsSerializer serializer, CancellationToken cancel) - { - if (!Volatile.Read(ref _publish)) - return Task.CompletedTask; + private bool _publish; - return CollectAndSerializeImplAsync(serializer, cancel); - } + /// + /// Collects all the metric data rows from this collector and serializes it using the given serializer. + /// + /// + /// Subclass must check _publish and suppress output if it is false. + /// + internal Task CollectAndSerializeAsync(IMetricsSerializer serializer, CancellationToken cancel) + { + if (!Volatile.Read(ref _publish)) + return Task.CompletedTask; - // Same as above, just only called if we really need to serialize this metric (if publish is true). - private protected abstract Task CollectAndSerializeImplAsync(IMetricsSerializer serializer, CancellationToken cancel); + return CollectAndSerializeImplAsync(serializer, cancel); } + + // Same as above, just only called if we really need to serialize this metric (if publish is true). + private protected abstract Task CollectAndSerializeImplAsync(IMetricsSerializer serializer, CancellationToken cancel); } \ No newline at end of file diff --git a/Prometheus/Counter.cs b/Prometheus/Counter.cs index 83a5e88f..35ebb82d 100644 --- a/Prometheus/Counter.cs +++ b/Prometheus/Counter.cs @@ -1,99 +1,99 @@ -namespace Prometheus +namespace Prometheus; + +public sealed class Counter : Collector, ICounter { - public sealed class Counter : Collector, ICounter + public sealed class Child : ChildBase, ICounter { - public sealed class Child : ChildBase, ICounter + internal Child(Collector parent, LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish) + : base(parent, instanceLabels, flattenedLabels, publish) { - internal Child(Collector parent, LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish) - : base(parent, instanceLabels, flattenedLabels, publish) - { - } + } - private ThreadSafeDouble _value; - private ObservedExemplar _observedExemplar = ObservedExemplar.Empty; + private ThreadSafeDouble _value; + private ObservedExemplar _observedExemplar = ObservedExemplar.Empty; - private protected override async Task CollectAndSerializeImplAsync(IMetricsSerializer serializer, - CancellationToken cancel) + private protected override async Task CollectAndSerializeImplAsync(IMetricsSerializer serializer, CancellationToken cancel) + { + // Borrow the current exemplar. We take ownership of the exemplar for the duration of the write. + ObservedExemplar exemplar = Interlocked.Exchange(ref _observedExemplar, ObservedExemplar.Empty); + + await serializer.WriteMetricPointAsync( + Parent.NameBytes, + FlattenedLabelsBytes, + CanonicalLabel.Empty, + cancel, + Value, + exemplar); + + if (exemplar != ObservedExemplar.Empty) { - // Borrow the current exemplar - ObservedExemplar cp = - Interlocked.CompareExchange(ref _observedExemplar, ObservedExemplar.Empty, _observedExemplar); - - await serializer.WriteMetricPointAsync( - Parent.NameBytes, - FlattenedLabelsBytes, - CanonicalLabel.Empty, - cancel, - Value, - cp); - - if (cp != ObservedExemplar.Empty) + // Return the exemplar unless a new one has arrived, in which case we discard the old one we were holding. + var foundExemplar = Interlocked.CompareExchange(ref _observedExemplar, exemplar, ObservedExemplar.Empty); + + if (foundExemplar != ObservedExemplar.Empty) { - // attempt to return the exemplar to the pool unless a new one has arrived. - var prev = Interlocked.CompareExchange(ref _observedExemplar, cp, ObservedExemplar.Empty); - if (prev != ObservedExemplar.Empty) // a new exemplar is present so we return ours back to the pool. - ObservedExemplar.ReturnPooled(cp); + // A new exemplar had already been written, so we could not return the borrowed one. That's perfectly fine - discard it. + ObservedExemplar.ReturnPooledIfNotEmpty(exemplar); } } + } - public void Inc(params Exemplar.LabelPair[] exemplar) - { - Inc(increment: 1, exemplar: exemplar); - } - - public void Inc(double increment = 1.0, params Exemplar.LabelPair[] exemplar) - { - if (increment < 0.0) - throw new ArgumentOutOfRangeException(nameof(increment), "Counter value cannot decrease."); + public void Inc(params Exemplar.LabelPair[] exemplarLabels) + { + Inc(increment: 1, exemplarLabels: exemplarLabels); + } - if (exemplar is { Length: > 0 }) - { - var ex = ObservedExemplar.CreatePooled(exemplar, increment); - var current = Interlocked.Exchange(ref _observedExemplar, ex); - if (current != ObservedExemplar.Empty) - ObservedExemplar.ReturnPooled(current); - } - _value.Add(increment); - Publish(); - } + public void Inc(double increment = 1.0, params Exemplar.LabelPair[] exemplarLabels) + { + if (increment < 0.0) + throw new ArgumentOutOfRangeException(nameof(increment), "Counter value cannot decrease."); - public void IncTo(double targetValue) + if (exemplarLabels is { Length: > 0 }) { - _value.IncrementTo(targetValue); - Publish(); + var exemplar = ObservedExemplar.CreatePooled(exemplarLabels, increment); + ObservedExemplar.ReturnPooledIfNotEmpty(Interlocked.Exchange(ref _observedExemplar, exemplar)); } - public double Value => _value.Value; + _value.Add(increment); + Publish(); } - - private protected override Child NewChild(LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish) + public void IncTo(double targetValue) { - return new Child(this, instanceLabels, flattenedLabels, publish); + _value.IncrementTo(targetValue); + Publish(); } - internal Counter(string name, string help, StringSequence instanceLabelNames, LabelSequence staticLabels, bool suppressInitialValue) - : base(name, help, instanceLabelNames, staticLabels, suppressInitialValue) - { - } + public double Value => _value.Value; + } - public void Inc(double increment = 1) => Unlabelled.Inc(increment); - public void IncTo(double targetValue) => Unlabelled.IncTo(targetValue); - public double Value => Unlabelled.Value; - public void Publish() => Unlabelled.Publish(); - public void Unpublish() => Unlabelled.Unpublish(); + private protected override Child NewChild(LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish) + { + return new Child(this, instanceLabels, flattenedLabels, publish); + } - public void Inc(params Exemplar.LabelPair[] exemplar) - { - Inc(increment: 1, exemplar: exemplar); - } + internal Counter(string name, string help, StringSequence instanceLabelNames, LabelSequence staticLabels, bool suppressInitialValue) + : base(name, help, instanceLabelNames, staticLabels, suppressInitialValue) + { + } - public void Inc(double increment = 1, params Exemplar.LabelPair[] exemplar) => - Unlabelled.Inc(increment, exemplar); - - internal override MetricType Type => MetricType.Counter; + public void Inc(double increment = 1) => Unlabelled.Inc(increment); + public void IncTo(double targetValue) => Unlabelled.IncTo(targetValue); + public double Value => Unlabelled.Value; - internal override int TimeseriesCount => ChildCount; + public void Publish() => Unlabelled.Publish(); + public void Unpublish() => Unlabelled.Unpublish(); + + public void Inc(params Exemplar.LabelPair[] exemplar) + { + Inc(increment: 1, exemplar: exemplar); } + + public void Inc(double increment = 1, params Exemplar.LabelPair[] exemplar) => + Unlabelled.Inc(increment, exemplar); + + internal override MetricType Type => MetricType.Counter; + + internal override int TimeseriesCount => ChildCount; } \ No newline at end of file diff --git a/Prometheus/Histogram.cs b/Prometheus/Histogram.cs index 33838b37..9216d64c 100644 --- a/Prometheus/Histogram.cs +++ b/Prometheus/Histogram.cs @@ -104,8 +104,9 @@ await serializer.WriteMetricPointAsync( for (var i = 0; i < _bucketCounts.Length; i++) { - // borrow the current exemplar. - ObservedExemplar cp = Interlocked.CompareExchange(ref _exemplars[i], ObservedExemplar.Empty, _exemplars[i]); + // Borrow the current exemplar. We take ownership of the exemplar for the duration of the write. + ObservedExemplar exemplar = Interlocked.Exchange(ref _exemplars[i], ObservedExemplar.Empty); + cumulativeCount += _bucketCounts[i].Value; await serializer.WriteMetricPointAsync( Parent.NameBytes, @@ -113,15 +114,19 @@ await serializer.WriteMetricPointAsync( _leLabels[i], cancel, cumulativeCount, - cp, + exemplar, suffix: BucketSuffix); - - if (cp != ObservedExemplar.Empty) + + if (exemplar != ObservedExemplar.Empty) { - // attempt to return the exemplar to the pool unless a new one has arrived. - var prev = Interlocked.CompareExchange(ref _exemplars[i], cp, ObservedExemplar.Empty); - if (prev != ObservedExemplar.Empty) // a new exemplar is present so we return ours back to the pool. - ObservedExemplar.ReturnPooled(cp); + // Return the exemplar unless a new one has arrived, in which case we discard the old one we were holding. + var foundExemplar = Interlocked.CompareExchange(ref _exemplars[i], exemplar, ObservedExemplar.Empty); + + if (foundExemplar != ObservedExemplar.Empty) + { + // A new exemplar had already been written, so we could not return the borrowed one. That's perfectly fine - discard it. + ObservedExemplar.ReturnPooledIfNotEmpty(exemplar); + } } } } @@ -129,13 +134,13 @@ await serializer.WriteMetricPointAsync( public double Sum => _sum.Value; public long Count => _bucketCounts.Sum(b => b.Value); - public void Observe(double val, params Exemplar.LabelPair[] exemplar) => ObserveInternal(val, 1, exemplar); + public void Observe(double val, params Exemplar.LabelPair[] exemplarLabels) => ObserveInternal(val, 1, exemplarLabels); public void Observe(double val) => Observe(val, 1); public void Observe(double val, long count) => ObserveInternal(val, count); - private void ObserveInternal(double val, long count, params Exemplar.LabelPair[] exemplar) + private void ObserveInternal(double val, long count, params Exemplar.LabelPair[] exemplarLabels) { if (double.IsNaN(val)) { @@ -147,12 +152,11 @@ private void ObserveInternal(double val, long count, params Exemplar.LabelPair[] if (val <= _upperBounds[i]) { _bucketCounts[i].Add(count); - if (exemplar is { Length: > 0 }) + + if (exemplarLabels is { Length: > 0 }) { - var observedExemplar = ObservedExemplar.CreatePooled(exemplar, val); - var current = Interlocked.Exchange(ref _exemplars[i], observedExemplar); - if (current != ObservedExemplar.Empty) - ObservedExemplar.ReturnPooled(current); + var exemplar = ObservedExemplar.CreatePooled(exemplarLabels, val); + ObservedExemplar.ReturnPooledIfNotEmpty(Interlocked.Exchange(ref _exemplars[i], exemplar)); } break; diff --git a/Prometheus/IMetricsSerializer.cs b/Prometheus/IMetricsSerializer.cs index 3424b0ac..64da8480 100644 --- a/Prometheus/IMetricsSerializer.cs +++ b/Prometheus/IMetricsSerializer.cs @@ -1,33 +1,31 @@ -namespace Prometheus +namespace Prometheus; + +/// +/// The only purpose this serves is to warn the developer when he might be accidentally introducing +/// new serialization-time relationships. The serialization code is very tied to the text format and +/// not intended to be a generic serialization mechanism. +/// +internal interface IMetricsSerializer { /// - /// The only purpose this serves is to warn the developer when he might be accidentally introducing - /// new serialization-time relationships. The serialization code is very tied to the text format and - /// not intended to be a generic serialization mechanism. + /// Writes the lines that declare the metric family. /// - internal interface IMetricsSerializer - { - /// - /// Writes the lines that declare the metric family. - /// - Task WriteFamilyDeclarationAsync(string name, byte[] nameBytes, byte[] helpBytes, MetricType type, - byte[] typeBytes, CancellationToken cancel); + Task WriteFamilyDeclarationAsync(string name, byte[] nameBytes, byte[] helpBytes, MetricType type, + byte[] typeBytes, CancellationToken cancel); - /// - /// Writes out a single metric point - /// - /// - Task WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, CanonicalLabel canonicalLabel, - CancellationToken cancel, double value, ObservedExemplar exemplar, byte[]? suffix = null); + /// + /// Writes out a single metric point + /// + Task WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, CanonicalLabel canonicalLabel, + CancellationToken cancel, double value, ObservedExemplar exemplar, byte[]? suffix = null); - /// - /// Writes out terminal lines - /// - Task WriteEnd(CancellationToken cancel); - - /// - /// Flushes any pending buffers. Always call this after all your write calls. - /// - Task FlushAsync(CancellationToken cancel); - } + /// + /// Writes out terminal lines + /// + Task WriteEnd(CancellationToken cancel); + + /// + /// Flushes any pending buffers. Always call this after all your write calls. + /// + Task FlushAsync(CancellationToken cancel); } \ No newline at end of file diff --git a/Prometheus/ObservedExemplar.cs b/Prometheus/ObservedExemplar.cs index 4421adf2..3b1c018c 100644 --- a/Prometheus/ObservedExemplar.cs +++ b/Prometheus/ObservedExemplar.cs @@ -1,5 +1,5 @@ -using System.Diagnostics; -using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.ObjectPool; +using System.Diagnostics; namespace Prometheus; @@ -8,63 +8,71 @@ namespace Prometheus; /// internal class ObservedExemplar { + /// + /// OpenMetrics places a length limit of 128 runes on the exemplar (sum of all key value pairs). + /// private const int MaxRunes = 128; - private static readonly ObjectPool Pool = ObjectPool.Create(); - + /// + /// We have a pool of unused instances that we can reuse, to avoid constantly allocating memory. Once the set of metrics stabilizes, + /// all allocations should generally be coming from the pool. We expect the default pool configuratiopn to be suitable for this. + /// + private static readonly ObjectPool Pool = ObjectPool.Create(); + public static readonly ObservedExemplar Empty = new(); - + internal static INowProvider NowProvider = new RealNowProvider(); public Exemplar.LabelPair[]? Labels { get; private set; } - public double Val { get; private set; } + public double Value { get; private set; } public double Timestamp { get; private set; } public ObservedExemplar() { Labels = null; - Val = 0; + Value = 0; Timestamp = 0; } - internal interface INowProvider + internal interface INowProvider { double Now(); } - private sealed class RealNowProvider : INowProvider - { - public double Now() - { - return DateTimeOffset.Now.ToUnixTimeMilliseconds() / 1e3; - } - } - + private sealed class RealNowProvider : INowProvider + { + public double Now() + { + return DateTimeOffset.Now.ToUnixTimeMilliseconds() / 1e3; + } + } + public bool IsValid => Labels != null; - private void Update(Exemplar.LabelPair[] labels, double val) + private void Update(Exemplar.LabelPair[] labels, double value) { - Debug.Assert(this != Empty, "do not mutate the sentinel"); - var tally = 0; + Debug.Assert(this != Empty, "Do not mutate the sentinel"); + + var totalRuneCount = 0; for (var i = 0; i < labels.Length; i++) { - tally += labels[i].RuneCount; + totalRuneCount += labels[i].RuneCount; for (var j = 0; j < labels.Length; j++) { if (i == j) continue; if (Equal(labels[i].KeyBytes, labels[j].KeyBytes)) - throw new ArgumentException("exemplar contains duplicate keys"); + throw new ArgumentException("Exemplar contains duplicate keys."); } } - if (tally > MaxRunes) - throw new ArgumentException($"exemplar labels has {tally} runes, exceeding the limit of {MaxRunes}."); + if (totalRuneCount > MaxRunes) + throw new ArgumentException($"Exemplar consists of {totalRuneCount} runes, exceeding the OpenMetrics limit of {MaxRunes}."); Labels = labels; - Val = val; + Value = value; Timestamp = NowProvider.Now(); } - + private static bool Equal(byte[] a, byte[] b) { // see https://www.syncfusion.com/succinctly-free-ebooks/application-security-in-net-succinctly/comparing-byte-arrays @@ -73,15 +81,18 @@ private static bool Equal(byte[] a, byte[] b) return x == 0; } - public static ObservedExemplar CreatePooled(Exemplar.LabelPair[] labelPairs, double val) + public static ObservedExemplar CreatePooled(Exemplar.LabelPair[] labelPairs, double value) { - var oe = Pool.Get(); - oe.Update(labelPairs, val); - return oe; + var instance = Pool.Get(); + instance.Update(labelPairs, value); + return instance; } - public static void ReturnPooled(ObservedExemplar observedExemplar) + public static void ReturnPooledIfNotEmpty(ObservedExemplar instance) { - Pool.Return(observedExemplar); + if (object.ReferenceEquals(instance, Empty)) + return; // We never put the "Empty" instance into the pool. Do the check here to avoid repeating it any time we return instances to the pool. + + Pool.Return(instance); } } \ No newline at end of file diff --git a/Prometheus/TextSerializer.cs b/Prometheus/TextSerializer.cs index 46b6f5bc..a5913670 100644 --- a/Prometheus/TextSerializer.cs +++ b/Prometheus/TextSerializer.cs @@ -108,7 +108,7 @@ private async Task WriteExemplarAsync(CancellationToken cancel, ObservedExemplar } await _stream.Value.WriteAsync(RightBraceSpace, 0, RightBraceSpace.Length, cancel); - await WriteValue(exemplar.Val, cancel); + await WriteValue(exemplar.Value, cancel); await _stream.Value.WriteAsync(Space, 0, Space.Length, cancel); await WriteValue(exemplar.Timestamp, cancel); } From 93187bd1894a089184dbe335d7e72ed7789dea58 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 29 Dec 2022 14:34:56 +0200 Subject: [PATCH 024/230] Code tidy --- Prometheus/Collector.cs | 425 +++++++++++---------- Prometheus/CollectorRegistry.cs | 529 +++++++++++++------------ Prometheus/Gauge.cs | 109 +++--- Prometheus/MetricFactory.cs | 283 +++++++------- Tests.NetCore/MetricsTests.cs | 657 ++++++++++++++++---------------- 5 files changed, 999 insertions(+), 1004 deletions(-) diff --git a/Prometheus/Collector.cs b/Prometheus/Collector.cs index 8287bf48..4df5ab98 100644 --- a/Prometheus/Collector.cs +++ b/Prometheus/Collector.cs @@ -2,275 +2,274 @@ using System.ComponentModel; using System.Text.RegularExpressions; -namespace Prometheus +namespace Prometheus; + +/// +/// Base class for metrics, defining the basic informative API and the internal API. +/// +public abstract class Collector { /// - /// Base class for metrics, defining the basic informative API and the internal API. + /// The metric name, e.g. http_requests_total. /// - public abstract class Collector - { - /// - /// The metric name, e.g. http_requests_total. - /// - public string Name { get; } - - internal byte[] NameBytes { get; } + public string Name { get; } + + internal byte[] NameBytes { get; } - /// - /// The help text describing the metric for a human audience. - /// - public string Help { get; } + /// + /// The help text describing the metric for a human audience. + /// + public string Help { get; } - internal byte[] HelpBytes { get; } + internal byte[] HelpBytes { get; } - /// - /// Names of the instance-specific labels (name-value pairs) that apply to this metric. - /// When the values are added to the names, you get a instance. - /// Does not include any static label names (from metric configuration, factory or registry). - /// - public string[] LabelNames => _instanceLabelNamesAsArrayLazy.Value; + /// + /// Names of the instance-specific labels (name-value pairs) that apply to this metric. + /// When the values are added to the names, you get a instance. + /// Does not include any static label names (from metric configuration, factory or registry). + /// + public string[] LabelNames => _instanceLabelNamesAsArrayLazy.Value; - internal StringSequence InstanceLabelNames; - internal StringSequence FlattenedLabelNames; + internal StringSequence InstanceLabelNames; + internal StringSequence FlattenedLabelNames; - /// - /// All static labels obtained from any hierarchy level (either defined in metric configuration or in registry). - /// These will be merged with the instance-specific labels to arrive at the final flattened label sequence for a specific child. - /// - internal LabelSequence StaticLabels; + /// + /// All static labels obtained from any hierarchy level (either defined in metric configuration or in registry). + /// These will be merged with the instance-specific labels to arrive at the final flattened label sequence for a specific child. + /// + internal LabelSequence StaticLabels; - internal abstract MetricType Type { get; } - - internal byte[] TypeBytes { get; } + internal abstract MetricType Type { get; } + + internal byte[] TypeBytes { get; } - internal abstract int ChildCount { get; } - internal abstract int TimeseriesCount { get; } + internal abstract int ChildCount { get; } + internal abstract int TimeseriesCount { get; } - internal abstract Task CollectAndSerializeAsync(IMetricsSerializer serializer, CancellationToken cancel); + internal abstract Task CollectAndSerializeAsync(IMetricsSerializer serializer, CancellationToken cancel); - // Used by ChildBase.Remove() - internal abstract void RemoveLabelled(LabelSequence instanceLabels); + // Used by ChildBase.Remove() + internal abstract void RemoveLabelled(LabelSequence instanceLabels); - private const string ValidMetricNameExpression = "^[a-zA-Z_][a-zA-Z0-9_]*$"; - private const string ValidLabelNameExpression = "^[a-zA-Z_][a-zA-Z0-9_]*$"; - private const string ReservedLabelNameExpression = "^__.*$"; + private const string ValidMetricNameExpression = "^[a-zA-Z_][a-zA-Z0-9_]*$"; + private const string ValidLabelNameExpression = "^[a-zA-Z_][a-zA-Z0-9_]*$"; + private const string ReservedLabelNameExpression = "^__.*$"; - private static readonly Regex MetricNameRegex = new Regex(ValidMetricNameExpression, RegexOptions.Compiled); - private static readonly Regex LabelNameRegex = new Regex(ValidLabelNameExpression, RegexOptions.Compiled); - private static readonly Regex ReservedLabelRegex = new Regex(ReservedLabelNameExpression, RegexOptions.Compiled); + private static readonly Regex MetricNameRegex = new Regex(ValidMetricNameExpression, RegexOptions.Compiled); + private static readonly Regex LabelNameRegex = new Regex(ValidLabelNameExpression, RegexOptions.Compiled); + private static readonly Regex ReservedLabelRegex = new Regex(ReservedLabelNameExpression, RegexOptions.Compiled); - internal Collector(string name, string help, StringSequence instanceLabelNames, LabelSequence staticLabels) + internal Collector(string name, string help, StringSequence instanceLabelNames, LabelSequence staticLabels) + { + if (!MetricNameRegex.IsMatch(name)) + throw new ArgumentException($"Metric name '{name}' does not match regex '{ValidMetricNameExpression}'."); + + Name = name; + NameBytes = PrometheusConstants.ExportEncoding.GetBytes(Name); + TypeBytes = PrometheusConstants.ExportEncoding.GetBytes(Type.ToString().ToLowerInvariant()); + Help = help; + HelpBytes = String.IsNullOrWhiteSpace(help) + ? Array.Empty() + : PrometheusConstants.ExportEncoding.GetBytes(help); + InstanceLabelNames = instanceLabelNames; + StaticLabels = staticLabels; + + FlattenedLabelNames = instanceLabelNames.Concat(staticLabels.Names); + + // Used to check uniqueness. + var uniqueLabelNames = new HashSet(StringComparer.Ordinal); + + var labelNameEnumerator = FlattenedLabelNames.GetEnumerator(); + while (labelNameEnumerator.MoveNext()) { - if (!MetricNameRegex.IsMatch(name)) - throw new ArgumentException($"Metric name '{name}' does not match regex '{ValidMetricNameExpression}'."); - - Name = name; - NameBytes = PrometheusConstants.ExportEncoding.GetBytes(Name); - TypeBytes = PrometheusConstants.ExportEncoding.GetBytes(Type.ToString().ToLowerInvariant()); - Help = help; - HelpBytes = String.IsNullOrWhiteSpace(help) - ? Array.Empty() - : PrometheusConstants.ExportEncoding.GetBytes(help); - InstanceLabelNames = instanceLabelNames; - StaticLabels = staticLabels; - - FlattenedLabelNames = instanceLabelNames.Concat(staticLabels.Names); - - // Used to check uniqueness. - var uniqueLabelNames = new HashSet(StringComparer.Ordinal); - - var labelNameEnumerator = FlattenedLabelNames.GetEnumerator(); - while (labelNameEnumerator.MoveNext()) - { - var labelName = labelNameEnumerator.Current; - - if (labelName == null) - throw new ArgumentNullException("Label name was null."); - - ValidateLabelName(labelName); - uniqueLabelNames.Add(labelName); - } - - // Here we check for label name collision, ensuring that the same label name is not defined twice on any label inheritance level. - if (uniqueLabelNames.Count != FlattenedLabelNames.Length) - throw new InvalidOperationException("The set of label names includes duplicates: " + string.Join(", ", FlattenedLabelNames.ToArray())); - - _instanceLabelNamesAsArrayLazy = new Lazy(GetInstanceLabelNamesAsStringArray); - } + var labelName = labelNameEnumerator.Current; - private readonly Lazy _instanceLabelNamesAsArrayLazy; + if (labelName == null) + throw new ArgumentNullException("Label name was null."); - private string[] GetInstanceLabelNamesAsStringArray() - { - return InstanceLabelNames.ToArray(); + ValidateLabelName(labelName); + uniqueLabelNames.Add(labelName); } - internal static void ValidateLabelName(string labelName) - { - if (!LabelNameRegex.IsMatch(labelName)) - throw new ArgumentException($"Label name '{labelName}' does not match regex '{ValidLabelNameExpression}'."); + // Here we check for label name collision, ensuring that the same label name is not defined twice on any label inheritance level. + if (uniqueLabelNames.Count != FlattenedLabelNames.Length) + throw new InvalidOperationException("The set of label names includes duplicates: " + string.Join(", ", FlattenedLabelNames.ToArray())); - if (ReservedLabelRegex.IsMatch(labelName)) - throw new ArgumentException($"Label name '{labelName}' is not valid - labels starting with double underscore are reserved!"); - } + _instanceLabelNamesAsArrayLazy = new Lazy(GetInstanceLabelNamesAsStringArray); } - /// - /// Base class for metrics collectors, providing common labeled child management functionality. - /// - public abstract class Collector : Collector, ICollector - where TChild : ChildBase + private readonly Lazy _instanceLabelNamesAsArrayLazy; + + private string[] GetInstanceLabelNamesAsStringArray() { - // Keyed by the instance labels (not by flattened labels!). - private readonly ConcurrentDictionary _labelledMetrics = new(); + return InstanceLabelNames.ToArray(); + } - // Lazy-initialized since not every collector will use a child with no labels. - // Lazy instance will be replaced if the unlabelled timeseries is unpublished. - private Lazy _unlabelledLazy; + internal static void ValidateLabelName(string labelName) + { + if (!LabelNameRegex.IsMatch(labelName)) + throw new ArgumentException($"Label name '{labelName}' does not match regex '{ValidLabelNameExpression}'."); - /// - /// Gets the child instance that has no labels. - /// - protected internal TChild Unlabelled => _unlabelledLazy.Value; + if (ReservedLabelRegex.IsMatch(labelName)) + throw new ArgumentException($"Label name '{labelName}' is not valid - labels starting with double underscore are reserved!"); + } +} - // We need it for the ICollector interface but using this is rarely relevant in client code, so keep it obscured. - TChild ICollector.Unlabelled => Unlabelled; +/// +/// Base class for metrics collectors, providing common labeled child management functionality. +/// +public abstract class Collector : Collector, ICollector + where TChild : ChildBase +{ + // Keyed by the instance labels (not by flattened labels!). + private readonly ConcurrentDictionary _labelledMetrics = new(); - // This servers a slightly silly but useful purpose: by default if you start typing .La... and trigger Intellisense - // it will often for whatever reason focus on LabelNames instead of Labels, leading to tiny but persistent frustration. - // Having WithLabels() instead eliminates the other candidate and allows for a frustration-free typing experience. - public TChild WithLabels(params string[] labelValues) => Labels(labelValues); + // Lazy-initialized since not every collector will use a child with no labels. + // Lazy instance will be replaced if the unlabelled timeseries is unpublished. + private Lazy _unlabelledLazy; - // Discourage it as it can create confusion. But it works fine, so no reason to mark it obsolete, really. - [EditorBrowsable(EditorBrowsableState.Never)] - public TChild Labels(params string[] labelValues) - { - if (labelValues == null) - throw new ArgumentNullException(nameof(labelValues)); + /// + /// Gets the child instance that has no labels. + /// + protected internal TChild Unlabelled => _unlabelledLazy.Value; - var labels = LabelSequence.From(InstanceLabelNames, StringSequence.From(labelValues)); - return GetOrAddLabelled(labels); - } + // We need it for the ICollector interface but using this is rarely relevant in client code, so keep it obscured. + TChild ICollector.Unlabelled => Unlabelled; - public void RemoveLabelled(params string[] labelValues) - { - if (labelValues == null) - throw new ArgumentNullException(nameof(labelValues)); + // This servers a slightly silly but useful purpose: by default if you start typing .La... and trigger Intellisense + // it will often for whatever reason focus on LabelNames instead of Labels, leading to tiny but persistent frustration. + // Having WithLabels() instead eliminates the other candidate and allows for a frustration-free typing experience. + public TChild WithLabels(params string[] labelValues) => Labels(labelValues); - var labels = LabelSequence.From(InstanceLabelNames, StringSequence.From(labelValues)); - RemoveLabelled(labels); - } + // Discourage it as it can create confusion. But it works fine, so no reason to mark it obsolete, really. + [EditorBrowsable(EditorBrowsableState.Never)] + public TChild Labels(params string[] labelValues) + { + if (labelValues == null) + throw new ArgumentNullException(nameof(labelValues)); - internal override void RemoveLabelled(LabelSequence labels) - { - _labelledMetrics.TryRemove(labels, out _); + var labels = LabelSequence.From(InstanceLabelNames, StringSequence.From(labelValues)); + return GetOrAddLabelled(labels); + } - if (labels.Length == 0) - { - // If we remove the unlabeled instance (technically legitimate, to unpublish it) then - // we need to also ensure that the special-casing used for it gets properly wired up the next time. - _unlabelledLazy = GetUnlabelledLazyInitializer(); - } - } + public void RemoveLabelled(params string[] labelValues) + { + if (labelValues == null) + throw new ArgumentNullException(nameof(labelValues)); - private Lazy GetUnlabelledLazyInitializer() - { - return new Lazy(() => GetOrAddLabelled(LabelSequence.Empty)); - } + var labels = LabelSequence.From(InstanceLabelNames, StringSequence.From(labelValues)); + RemoveLabelled(labels); + } - internal override int ChildCount => _labelledMetrics.Count; + internal override void RemoveLabelled(LabelSequence labels) + { + _labelledMetrics.TryRemove(labels, out _); - /// - /// Gets the instance-specific label values of all labelled instances of the collector. - /// Values of any inherited static labels are not returned in the result. - /// - /// Note that during concurrent operation, the set of values returned here - /// may diverge from the latest set of values used by the collector. - /// - public IEnumerable GetAllLabelValues() + if (labels.Length == 0) { - foreach (var labels in _labelledMetrics.Keys) - { - if (labels.Length == 0) - continue; // We do not return the "unlabelled" label set. - - // Defensive copy. - yield return labels.Values.ToArray(); - } + // If we remove the unlabeled instance (technically legitimate, to unpublish it) then + // we need to also ensure that the special-casing used for it gets properly wired up the next time. + _unlabelledLazy = GetUnlabelledLazyInitializer(); } + } - private TChild GetOrAddLabelled(LabelSequence instanceLabels) - { - // NOTE: We do not try to find a metric instance with the same set of label names but in a DIFFERENT order. - // Order of labels matterns in data creation, although does not matter when the exported data set is imported later. - // If we somehow end up registering the same metric with the same label names in different order, we will publish it twice, in two orders... - // That is not ideal but also not that big of a deal to do a lookup every time a metric instance is registered. - - // Don't allocate lambda for GetOrAdd in the common case that the labeled metrics exist. - if (_labelledMetrics.TryGetValue(instanceLabels, out var metric)) - return metric; + private Lazy GetUnlabelledLazyInitializer() + { + return new Lazy(() => GetOrAddLabelled(LabelSequence.Empty)); + } - return _labelledMetrics.GetOrAdd(instanceLabels, CreateLabelledChild); - } + internal override int ChildCount => _labelledMetrics.Count; - private TChild CreateLabelledChild(LabelSequence instanceLabels) + /// + /// Gets the instance-specific label values of all labelled instances of the collector. + /// Values of any inherited static labels are not returned in the result. + /// + /// Note that during concurrent operation, the set of values returned here + /// may diverge from the latest set of values used by the collector. + /// + public IEnumerable GetAllLabelValues() + { + foreach (var labels in _labelledMetrics.Keys) { - // Order of labels is 1) instance labels; 2) static labels. - var flattenedLabels = instanceLabels.Concat(StaticLabels); + if (labels.Length == 0) + continue; // We do not return the "unlabelled" label set. - return NewChild(instanceLabels, flattenedLabels, publish: !_suppressInitialValue); + // Defensive copy. + yield return labels.Values.ToArray(); } + } - /// - /// For tests that want to see what instance-level label values were used when metrics were created. - /// - internal LabelSequence[] GetAllInstanceLabels() => _labelledMetrics.Select(p => p.Key).ToArray(); + private TChild GetOrAddLabelled(LabelSequence instanceLabels) + { + // NOTE: We do not try to find a metric instance with the same set of label names but in a DIFFERENT order. + // Order of labels matterns in data creation, although does not matter when the exported data set is imported later. + // If we somehow end up registering the same metric with the same label names in different order, we will publish it twice, in two orders... + // That is not ideal but also not that big of a deal to do a lookup every time a metric instance is registered. - internal Collector(string name, string help, StringSequence instanceLabelNames, LabelSequence staticLabels, bool suppressInitialValue) - : base(name, help, instanceLabelNames, staticLabels) - { - _suppressInitialValue = suppressInitialValue; + // Don't allocate lambda for GetOrAdd in the common case that the labeled metrics exist. + if (_labelledMetrics.TryGetValue(instanceLabels, out var metric)) + return metric; - _unlabelledLazy = GetUnlabelledLazyInitializer(); + return _labelledMetrics.GetOrAdd(instanceLabels, CreateLabelledChild); + } - _familyHeaderLines = new byte[][] - { - string.IsNullOrWhiteSpace(help) - ? PrometheusConstants.ExportEncoding.GetBytes($"# HELP {name}") - : PrometheusConstants.ExportEncoding.GetBytes($"# HELP {name} {help}"), - PrometheusConstants.ExportEncoding.GetBytes($"# TYPE {name} {Type.ToString().ToLowerInvariant()}") - }; - } + private TChild CreateLabelledChild(LabelSequence instanceLabels) + { + // Order of labels is 1) instance labels; 2) static labels. + var flattenedLabels = instanceLabels.Concat(StaticLabels); - /// - /// Creates a new instance of the child collector type. - /// - private protected abstract TChild NewChild(LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish); + return NewChild(instanceLabels, flattenedLabels, publish: !_suppressInitialValue); + } - private readonly byte[][] _familyHeaderLines; + /// + /// For tests that want to see what instance-level label values were used when metrics were created. + /// + internal LabelSequence[] GetAllInstanceLabels() => _labelledMetrics.Select(p => p.Key).ToArray(); + + internal Collector(string name, string help, StringSequence instanceLabelNames, LabelSequence staticLabels, bool suppressInitialValue) + : base(name, help, instanceLabelNames, staticLabels) + { + _suppressInitialValue = suppressInitialValue; - internal override async Task CollectAndSerializeAsync(IMetricsSerializer serializer, CancellationToken cancel) + _unlabelledLazy = GetUnlabelledLazyInitializer(); + + _familyHeaderLines = new byte[][] { - EnsureUnlabelledMetricCreatedIfNoLabels(); + string.IsNullOrWhiteSpace(help) + ? PrometheusConstants.ExportEncoding.GetBytes($"# HELP {name}") + : PrometheusConstants.ExportEncoding.GetBytes($"# HELP {name} {help}"), + PrometheusConstants.ExportEncoding.GetBytes($"# TYPE {name} {Type.ToString().ToLowerInvariant()}") + }; + } + + /// + /// Creates a new instance of the child collector type. + /// + private protected abstract TChild NewChild(LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish); - await serializer.WriteFamilyDeclarationAsync(Name, NameBytes, HelpBytes, Type, TypeBytes, cancel); + private readonly byte[][] _familyHeaderLines; - foreach (var child in _labelledMetrics.Values) - await child.CollectAndSerializeAsync(serializer, cancel); - } + internal override async Task CollectAndSerializeAsync(IMetricsSerializer serializer, CancellationToken cancel) + { + EnsureUnlabelledMetricCreatedIfNoLabels(); - private readonly bool _suppressInitialValue; + await serializer.WriteFamilyDeclarationAsync(Name, NameBytes, HelpBytes, Type, TypeBytes, cancel); - private void EnsureUnlabelledMetricCreatedIfNoLabels() - { - // We want metrics to exist even with 0 values if they are supposed to be used without labels. - // Labelled metrics are created when label values are assigned. However, as unlabelled metrics are lazy-created - // (they are optional if labels are used) we might lose them for cases where they really are desired. - - // If there are no label names then clearly this metric is supposed to be used unlabelled, so create it. - // Otherwise, we allow unlabelled metrics to be used if the user explicitly does it but omit them by default. - if (!_unlabelledLazy.IsValueCreated && !LabelNames.Any()) - GetOrAddLabelled(LabelSequence.Empty); - } + foreach (var child in _labelledMetrics.Values) + await child.CollectAndSerializeAsync(serializer, cancel); + } + + private readonly bool _suppressInitialValue; + + private void EnsureUnlabelledMetricCreatedIfNoLabels() + { + // We want metrics to exist even with 0 values if they are supposed to be used without labels. + // Labelled metrics are created when label values are assigned. However, as unlabelled metrics are lazy-created + // (they are optional if labels are used) we might lose them for cases where they really are desired. + + // If there are no label names then clearly this metric is supposed to be used unlabelled, so create it. + // Otherwise, we allow unlabelled metrics to be used if the user explicitly does it but omit them by default. + if (!_unlabelledLazy.IsValueCreated && !LabelNames.Any()) + GetOrAddLabelled(LabelSequence.Empty); } } \ No newline at end of file diff --git a/Prometheus/CollectorRegistry.cs b/Prometheus/CollectorRegistry.cs index 404cdf18..0d8d3eec 100644 --- a/Prometheus/CollectorRegistry.cs +++ b/Prometheus/CollectorRegistry.cs @@ -1,340 +1,339 @@ using System.Collections.Concurrent; using System.Diagnostics; -namespace Prometheus +namespace Prometheus; + +/// +/// Maintains references to a set of collectors, from which data for metrics is collected at data export time. +/// +/// Use methods on the class to add metrics to a collector registry. +/// +/// +/// To encourage good concurrency practices, registries are append-only. You can add things to them but not remove. +/// If you wish to remove things from the registry, create a new registry with only the things you wish to keep. +/// +public sealed class CollectorRegistry : ICollectorRegistry { + #region "Before collect" callbacks /// - /// Maintains references to a set of collectors, from which data for metrics is collected at data export time. + /// Registers an action to be called before metrics are collected. + /// This enables you to do last-minute updates to metric values very near the time of collection. + /// Callbacks will delay the metric collection, so do not make them too long or it may time out. /// - /// Use methods on the class to add metrics to a collector registry. + /// The callback will be executed synchronously and should not take more than a few milliseconds. + /// To execute longer-duration callbacks, register an asynchronous callback (Func<Task>). + /// + /// If the callback throws then the entire metric collection will fail. + /// This will result in an appropriate HTTP error code or a skipped push, depending on type of exporter. + /// + /// If multiple concurrent collections occur, the callback may be called multiple times concurrently. /// - /// - /// To encourage good concurrency practices, registries are append-only. You can add things to them but not remove. - /// If you wish to remove things from the registry, create a new registry with only the things you wish to keep. - /// - public sealed class CollectorRegistry : ICollectorRegistry + public void AddBeforeCollectCallback(Action callback) { - #region "Before collect" callbacks - /// - /// Registers an action to be called before metrics are collected. - /// This enables you to do last-minute updates to metric values very near the time of collection. - /// Callbacks will delay the metric collection, so do not make them too long or it may time out. - /// - /// The callback will be executed synchronously and should not take more than a few milliseconds. - /// To execute longer-duration callbacks, register an asynchronous callback (Func<Task>). - /// - /// If the callback throws then the entire metric collection will fail. - /// This will result in an appropriate HTTP error code or a skipped push, depending on type of exporter. - /// - /// If multiple concurrent collections occur, the callback may be called multiple times concurrently. - /// - public void AddBeforeCollectCallback(Action callback) - { - if (callback == null) - throw new ArgumentNullException(nameof(callback)); + if (callback == null) + throw new ArgumentNullException(nameof(callback)); - _beforeCollectCallbacks.Add(callback); - } + _beforeCollectCallbacks.Add(callback); + } - /// - /// Registers an action to be called before metrics are collected. - /// This enables you to do last-minute updates to metric values very near the time of collection. - /// Callbacks will delay the metric collection, so do not make them too long or it may time out. - /// - /// Asynchronous callbacks will be executed concurrently and may last longer than a few milliseconds. - /// - /// If the callback throws then the entire metric collection will fail. - /// This will result in an appropriate HTTP error code or a skipped push, depending on type of exporter. - /// - /// If multiple concurrent collections occur, the callback may be called multiple times concurrently. - /// - public void AddBeforeCollectCallback(Func callback) - { - if (callback == null) - throw new ArgumentNullException(nameof(callback)); + /// + /// Registers an action to be called before metrics are collected. + /// This enables you to do last-minute updates to metric values very near the time of collection. + /// Callbacks will delay the metric collection, so do not make them too long or it may time out. + /// + /// Asynchronous callbacks will be executed concurrently and may last longer than a few milliseconds. + /// + /// If the callback throws then the entire metric collection will fail. + /// This will result in an appropriate HTTP error code or a skipped push, depending on type of exporter. + /// + /// If multiple concurrent collections occur, the callback may be called multiple times concurrently. + /// + public void AddBeforeCollectCallback(Func callback) + { + if (callback == null) + throw new ArgumentNullException(nameof(callback)); - _beforeCollectAsyncCallbacks.Add(callback); - } + _beforeCollectAsyncCallbacks.Add(callback); + } - private readonly ConcurrentBag _beforeCollectCallbacks = new ConcurrentBag(); - private readonly ConcurrentBag> _beforeCollectAsyncCallbacks = new ConcurrentBag>(); - #endregion - - #region Static labels - /// - /// The set of static labels that are applied to all metrics in this registry. - /// Enumeration of the returned collection is thread-safe. - /// - public IEnumerable> StaticLabels => _staticLabels.ToDictionary(); - - /// - /// Defines the set of static labels to apply to all metrics in this registry. - /// The static labels can only be set once on startup, before adding or publishing any metrics. - /// - public void SetStaticLabels(IDictionary labels) - { - if (labels == null) - throw new ArgumentNullException(nameof(labels)); + private readonly ConcurrentBag _beforeCollectCallbacks = new ConcurrentBag(); + private readonly ConcurrentBag> _beforeCollectAsyncCallbacks = new ConcurrentBag>(); + #endregion - // Read lock is taken when creating metrics, so we know that no metrics can be created while we hold this lock. - _staticLabelsLock.EnterWriteLock(); + #region Static labels + /// + /// The set of static labels that are applied to all metrics in this registry. + /// Enumeration of the returned collection is thread-safe. + /// + public IEnumerable> StaticLabels => _staticLabels.ToDictionary(); - try - { - if (_staticLabels.Length != 0) - throw new InvalidOperationException("Static labels have already been defined - you can only do it once per registry."); + /// + /// Defines the set of static labels to apply to all metrics in this registry. + /// The static labels can only be set once on startup, before adding or publishing any metrics. + /// + public void SetStaticLabels(IDictionary labels) + { + if (labels == null) + throw new ArgumentNullException(nameof(labels)); - if (_collectors.Count != 0) - throw new InvalidOperationException("Metrics have already been added to the registry - cannot define static labels anymore."); + // Read lock is taken when creating metrics, so we know that no metrics can be created while we hold this lock. + _staticLabelsLock.EnterWriteLock(); - // Keep the lock for the duration of this method to make sure no publishing happens while we are setting labels. - lock (_firstCollectLock) - { - if (_hasPerformedFirstCollect) - throw new InvalidOperationException("The metrics registry has already been published - cannot define static labels anymore."); + try + { + if (_staticLabels.Length != 0) + throw new InvalidOperationException("Static labels have already been defined - you can only do it once per registry."); - foreach (var pair in labels) - { - if (pair.Key == null) - throw new ArgumentException("The name of a label cannot be null."); + if (_collectors.Count != 0) + throw new InvalidOperationException("Metrics have already been added to the registry - cannot define static labels anymore."); - if (pair.Value == null) - throw new ArgumentException("The value of a label cannot be null."); + // Keep the lock for the duration of this method to make sure no publishing happens while we are setting labels. + lock (_firstCollectLock) + { + if (_hasPerformedFirstCollect) + throw new InvalidOperationException("The metrics registry has already been published - cannot define static labels anymore."); - Collector.ValidateLabelName(pair.Key); - } + foreach (var pair in labels) + { + if (pair.Key == null) + throw new ArgumentException("The name of a label cannot be null."); + + if (pair.Value == null) + throw new ArgumentException("The value of a label cannot be null."); - _staticLabels = LabelSequence.From(labels); + Collector.ValidateLabelName(pair.Key); } + + _staticLabels = LabelSequence.From(labels); } - finally - { - _staticLabelsLock.ExitWriteLock(); - } } + finally + { + _staticLabelsLock.ExitWriteLock(); + } + } - private LabelSequence _staticLabels; - private readonly ReaderWriterLockSlim _staticLabelsLock = new ReaderWriterLockSlim(); + private LabelSequence _staticLabels; + private readonly ReaderWriterLockSlim _staticLabelsLock = new ReaderWriterLockSlim(); - internal LabelSequence GetStaticLabels() - { - _staticLabelsLock.EnterReadLock(); + internal LabelSequence GetStaticLabels() + { + _staticLabelsLock.EnterReadLock(); - try - { - return _staticLabels; - } - finally - { - _staticLabelsLock.ExitReadLock(); - } - } - #endregion - - /// - /// Collects all metrics and exports them in text document format to the provided stream. - /// - /// This method is designed to be used with custom output mechanisms that do not use an IMetricServer. - /// - public Task CollectAndExportAsTextAsync(Stream to, ExpositionFormat format= ExpositionFormat.Text, CancellationToken cancel = default) + try { - if (to == null) - throw new ArgumentNullException(nameof(to)); - - return CollectAndSerializeAsync(new TextSerializer(to, format), cancel); + return _staticLabels; } - - // We pass this thing to GetOrAdd to avoid allocating a collector or a closure. - // This reduces memory usage in situations where the collector is already registered. - internal readonly struct CollectorInitializer - where TCollector : Collector - where TConfiguration : MetricConfiguration + finally { - private readonly CreateInstanceDelegate _createInstance; - private readonly string _name; - private readonly string _help; - private readonly StringSequence _instanceLabelNames; - private readonly LabelSequence _staticLabels; - private readonly TConfiguration _configuration; - - public string Name => _name; - public StringSequence InstanceLabelNames => _instanceLabelNames; - public LabelSequence StaticLabels => _staticLabels; - - public CollectorInitializer(CreateInstanceDelegate createInstance, string name, string help, StringSequence instanceLabelNames, LabelSequence staticLabels, TConfiguration configuration) - { - _createInstance = createInstance; - _name = name; - _help = help; - _instanceLabelNames = instanceLabelNames; - _staticLabels = staticLabels; - _configuration = configuration; - } + _staticLabelsLock.ExitReadLock(); + } + } + #endregion - public TCollector CreateInstance(CollectorIdentity _) => _createInstance(_name, _help, _instanceLabelNames, _staticLabels, _configuration); + /// + /// Collects all metrics and exports them in text document format to the provided stream. + /// + /// This method is designed to be used with custom output mechanisms that do not use an IMetricServer. + /// + public Task CollectAndExportAsTextAsync(Stream to, ExpositionFormat format= ExpositionFormat.Text, CancellationToken cancel = default) + { + if (to == null) + throw new ArgumentNullException(nameof(to)); - public delegate TCollector CreateInstanceDelegate(string name, string help, StringSequence instanceLabelNames, LabelSequence staticLabels, TConfiguration configuration); - } + return CollectAndSerializeAsync(new TextSerializer(to, format), cancel); + } - /// - /// Adds a collector to the registry, returning an existing instance if one with a matching name was already registered. - /// - internal TCollector GetOrAdd(in CollectorInitializer initializer) - where TCollector : Collector - where TConfiguration : MetricConfiguration + // We pass this thing to GetOrAdd to avoid allocating a collector or a closure. + // This reduces memory usage in situations where the collector is already registered. + internal readonly struct CollectorInitializer + where TCollector : Collector + where TConfiguration : MetricConfiguration + { + private readonly CreateInstanceDelegate _createInstance; + private readonly string _name; + private readonly string _help; + private readonly StringSequence _instanceLabelNames; + private readonly LabelSequence _staticLabels; + private readonly TConfiguration _configuration; + + public string Name => _name; + public StringSequence InstanceLabelNames => _instanceLabelNames; + public LabelSequence StaticLabels => _staticLabels; + + public CollectorInitializer(CreateInstanceDelegate createInstance, string name, string help, StringSequence instanceLabelNames, LabelSequence staticLabels, TConfiguration configuration) { - var identity = new CollectorIdentity(initializer.Name, initializer.InstanceLabelNames, initializer.StaticLabels.Names); + _createInstance = createInstance; + _name = name; + _help = help; + _instanceLabelNames = instanceLabelNames; + _staticLabels = staticLabels; + _configuration = configuration; + } - static TCollector Validate(Collector candidate) - { - // We either created a new collector or found one with a matching identity. - // We do some basic validation here to avoid silly API usage mistakes. + public TCollector CreateInstance(CollectorIdentity _) => _createInstance(_name, _help, _instanceLabelNames, _staticLabels, _configuration); - if (!(candidate is TCollector)) - throw new InvalidOperationException("Collector of a different type with the same identity is already registered."); + public delegate TCollector CreateInstanceDelegate(string name, string help, StringSequence instanceLabelNames, LabelSequence staticLabels, TConfiguration configuration); + } - return (TCollector)candidate; - } + /// + /// Adds a collector to the registry, returning an existing instance if one with a matching name was already registered. + /// + internal TCollector GetOrAdd(in CollectorInitializer initializer) + where TCollector : Collector + where TConfiguration : MetricConfiguration + { + var identity = new CollectorIdentity(initializer.Name, initializer.InstanceLabelNames, initializer.StaticLabels.Names); - // Should we optimize for the case where the collector is already registered? It is unlikely to be very common. However, we still should because: - // 1) In scenarios where collector re-registration is common, it could be an expensive persistent cost in terms of allocations. - // 2) In scenarios where collector re-registration is not common, a tiny compute overhead is unlikely to be significant in the big picture, as it only happens once. - if (_collectors.TryGetValue(identity, out var existing)) - return Validate(existing); + static TCollector Validate(Collector candidate) + { + // We either created a new collector or found one with a matching identity. + // We do some basic validation here to avoid silly API usage mistakes. - var collector = _collectors.GetOrAdd(identity, initializer.CreateInstance); + if (!(candidate is TCollector)) + throw new InvalidOperationException("Collector of a different type with the same identity is already registered."); - return Validate(collector); + return (TCollector)candidate; } - private readonly ConcurrentDictionary _collectors = new ConcurrentDictionary(); + // Should we optimize for the case where the collector is already registered? It is unlikely to be very common. However, we still should because: + // 1) In scenarios where collector re-registration is common, it could be an expensive persistent cost in terms of allocations. + // 2) In scenarios where collector re-registration is not common, a tiny compute overhead is unlikely to be significant in the big picture, as it only happens once. + if (_collectors.TryGetValue(identity, out var existing)) + return Validate(existing); + + var collector = _collectors.GetOrAdd(identity, initializer.CreateInstance); + + return Validate(collector); + } + + private readonly ConcurrentDictionary _collectors = new ConcurrentDictionary(); - internal void SetBeforeFirstCollectCallback(Action a) + internal void SetBeforeFirstCollectCallback(Action a) + { + lock (_firstCollectLock) { - lock (_firstCollectLock) - { - if (_hasPerformedFirstCollect) - return; // Avoid keeping a reference to a callback we won't ever use. + if (_hasPerformedFirstCollect) + return; // Avoid keeping a reference to a callback we won't ever use. - _beforeFirstCollectCallback = a; - } + _beforeFirstCollectCallback = a; } + } + + /// + /// Allows us to initialize (or not) the registry with the default metrics before the first collection. + /// + private Action? _beforeFirstCollectCallback; + private bool _hasPerformedFirstCollect; + private readonly object _firstCollectLock = new object(); - /// - /// Allows us to initialize (or not) the registry with the default metrics before the first collection. - /// - private Action? _beforeFirstCollectCallback; - private bool _hasPerformedFirstCollect; - private readonly object _firstCollectLock = new object(); - - /// - /// Collects metrics from all the registered collectors and sends them to the specified serializer. - /// - internal async Task CollectAndSerializeAsync(IMetricsSerializer serializer, CancellationToken cancel) + /// + /// Collects metrics from all the registered collectors and sends them to the specified serializer. + /// + internal async Task CollectAndSerializeAsync(IMetricsSerializer serializer, CancellationToken cancel) + { + lock (_firstCollectLock) { - lock (_firstCollectLock) + if (!_hasPerformedFirstCollect) { - if (!_hasPerformedFirstCollect) - { - _hasPerformedFirstCollect = true; - _beforeFirstCollectCallback?.Invoke(); - _beforeFirstCollectCallback = null; - } + _hasPerformedFirstCollect = true; + _beforeFirstCollectCallback?.Invoke(); + _beforeFirstCollectCallback = null; } + } - await RunBeforeCollectCallbacksAsync(cancel); + await RunBeforeCollectCallbacksAsync(cancel); - UpdateRegistryMetrics(); + UpdateRegistryMetrics(); - foreach (var collector in _collectors.Values) - await collector.CollectAndSerializeAsync(serializer, cancel); - await serializer.WriteEnd(cancel); - await serializer.FlushAsync(cancel); - } + foreach (var collector in _collectors.Values) + await collector.CollectAndSerializeAsync(serializer, cancel); + await serializer.WriteEnd(cancel); + await serializer.FlushAsync(cancel); + } - private async Task RunBeforeCollectCallbacksAsync(CancellationToken cancel) + private async Task RunBeforeCollectCallbacksAsync(CancellationToken cancel) + { + foreach (var callback in _beforeCollectCallbacks) { - foreach (var callback in _beforeCollectCallbacks) + try { - try - { - callback(); - } - catch (Exception ex) - { - Trace.WriteLine($"Metrics before-collect callback failed: {ex}"); - } + callback(); } - - await Task.WhenAll(_beforeCollectAsyncCallbacks.Select(async (callback) => + catch (Exception ex) { - try - { - await callback(cancel); - } - catch (Exception ex) - { - Trace.WriteLine($"Metrics before-collect callback failed: {ex}"); - } - })); + Trace.WriteLine($"Metrics before-collect callback failed: {ex}"); + } } - /// - /// We collect some debug metrics from the registry itself to help indicate how many metrics we are publishing. - /// - internal void StartCollectingRegistryMetrics() + await Task.WhenAll(_beforeCollectAsyncCallbacks.Select(async (callback) => { - var factory = Metrics.WithCustomRegistry(this); + try + { + await callback(cancel); + } + catch (Exception ex) + { + Trace.WriteLine($"Metrics before-collect callback failed: {ex}"); + } + })); + } - _metricFamilies = factory.CreateGauge("prometheus_net_metric_families", "Number of metric families currently registered.", labelNames: new[] { MetricTypeDebugLabel }); - _metricInstances = factory.CreateGauge("prometheus_net_metric_instances", "Number of metric instances currently registered across all metric families.", labelNames: new[] { MetricTypeDebugLabel }); - _metricTimeseries = factory.CreateGauge("prometheus_net_metric_timeseries", "Number of metric timeseries currently generated from all metric instances.", labelNames: new[] { MetricTypeDebugLabel }); + /// + /// We collect some debug metrics from the registry itself to help indicate how many metrics we are publishing. + /// + internal void StartCollectingRegistryMetrics() + { + var factory = Metrics.WithCustomRegistry(this); - _metricFamiliesPerType = new(); - _metricInstancesPerType = new(); - _metricTimeseriesPerType = new(); + _metricFamilies = factory.CreateGauge("prometheus_net_metric_families", "Number of metric families currently registered.", labelNames: new[] { MetricTypeDebugLabel }); + _metricInstances = factory.CreateGauge("prometheus_net_metric_instances", "Number of metric instances currently registered across all metric families.", labelNames: new[] { MetricTypeDebugLabel }); + _metricTimeseries = factory.CreateGauge("prometheus_net_metric_timeseries", "Number of metric timeseries currently generated from all metric instances.", labelNames: new[] { MetricTypeDebugLabel }); - foreach (MetricType type in Enum.GetValues(typeof(MetricType))) - { - var typeName = type.ToString().ToLowerInvariant(); - _metricFamiliesPerType[type] = _metricFamilies.WithLabels(typeName); - _metricInstancesPerType[type] = _metricInstances.WithLabels(typeName); - _metricTimeseriesPerType[type] = _metricTimeseries.WithLabels(typeName); - } + _metricFamiliesPerType = new(); + _metricInstancesPerType = new(); + _metricTimeseriesPerType = new(); + + foreach (MetricType type in Enum.GetValues(typeof(MetricType))) + { + var typeName = type.ToString().ToLowerInvariant(); + _metricFamiliesPerType[type] = _metricFamilies.WithLabels(typeName); + _metricInstancesPerType[type] = _metricInstances.WithLabels(typeName); + _metricTimeseriesPerType[type] = _metricTimeseries.WithLabels(typeName); } + } - private const string MetricTypeDebugLabel = "metric_type"; + private const string MetricTypeDebugLabel = "metric_type"; - private Gauge? _metricFamilies; - private Gauge? _metricInstances; - private Gauge? _metricTimeseries; + private Gauge? _metricFamilies; + private Gauge? _metricInstances; + private Gauge? _metricTimeseries; - private Dictionary? _metricFamiliesPerType; - private Dictionary? _metricInstancesPerType; - private Dictionary? _metricTimeseriesPerType; + private Dictionary? _metricFamiliesPerType; + private Dictionary? _metricInstancesPerType; + private Dictionary? _metricTimeseriesPerType; - private void UpdateRegistryMetrics() + private void UpdateRegistryMetrics() + { + if (_metricFamiliesPerType == null ||_metricInstancesPerType == null || _metricTimeseriesPerType == null) + return; // Debug metrics are not enabled. + + foreach (MetricType type in Enum.GetValues(typeof(MetricType))) { - if (_metricFamiliesPerType == null ||_metricInstancesPerType == null || _metricTimeseriesPerType == null) - return; // Debug metrics are not enabled. + long families = 0; + long instances = 0; + long timeseries = 0; - foreach (MetricType type in Enum.GetValues(typeof(MetricType))) + foreach (var collector in _collectors.Values.Where(c => c.Type == type)) { - long families = 0; - long instances = 0; - long timeseries = 0; - - foreach (var collector in _collectors.Values.Where(c => c.Type == type)) - { - families++; - instances += collector.ChildCount; - timeseries += collector.TimeseriesCount; - } - - _metricFamiliesPerType[type].Set(families); - _metricInstancesPerType[type].Set(instances); - _metricTimeseriesPerType[type].Set(timeseries); + families++; + instances += collector.ChildCount; + timeseries += collector.TimeseriesCount; } + + _metricFamiliesPerType[type].Set(families); + _metricInstancesPerType[type].Set(instances); + _metricTimeseriesPerType[type].Set(timeseries); } } } diff --git a/Prometheus/Gauge.cs b/Prometheus/Gauge.cs index 6375172f..49290053 100644 --- a/Prometheus/Gauge.cs +++ b/Prometheus/Gauge.cs @@ -1,75 +1,74 @@ -namespace Prometheus +namespace Prometheus; + +public sealed class Gauge : Collector, IGauge { - public sealed class Gauge : Collector, IGauge + public sealed class Child : ChildBase, IGauge { - public sealed class Child : ChildBase, IGauge + internal Child(Collector parent, LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish) + : base(parent, instanceLabels, flattenedLabels, publish) { - internal Child(Collector parent, LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish) - : base(parent, instanceLabels, flattenedLabels, publish) - { - } - - private ThreadSafeDouble _value; - - private protected override async Task CollectAndSerializeImplAsync(IMetricsSerializer serializer, CancellationToken cancel) - { - await serializer.WriteMetricPointAsync( - Parent.NameBytes, FlattenedLabelsBytes, CanonicalLabel.Empty, cancel, Value, ObservedExemplar.Empty); - } - - public void Inc(double increment = 1) - { - _value.Add(increment); - Publish(); - } + } - public void Set(double val) - { - _value.Value = val; - Publish(); - } + private ThreadSafeDouble _value; - public void Dec(double decrement = 1) - { - Inc(-decrement); - } + private protected override async Task CollectAndSerializeImplAsync(IMetricsSerializer serializer, CancellationToken cancel) + { + await serializer.WriteMetricPointAsync( + Parent.NameBytes, FlattenedLabelsBytes, CanonicalLabel.Empty, cancel, Value, ObservedExemplar.Empty); + } - public void IncTo(double targetValue) - { - _value.IncrementTo(targetValue); - Publish(); - } + public void Inc(double increment = 1) + { + _value.Add(increment); + Publish(); + } - public void DecTo(double targetValue) - { - _value.DecrementTo(targetValue); - Publish(); - } + public void Set(double val) + { + _value.Value = val; + Publish(); + } - public double Value => _value.Value; + public void Dec(double decrement = 1) + { + Inc(-decrement); } - private protected override Child NewChild(LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish) + public void IncTo(double targetValue) { - return new Child(this, instanceLabels, flattenedLabels, publish); + _value.IncrementTo(targetValue); + Publish(); } - internal Gauge(string name, string help, StringSequence instanceLabelNames, LabelSequence staticLabels, bool suppressInitialValue) - : base(name, help, instanceLabelNames, staticLabels, suppressInitialValue) + public void DecTo(double targetValue) { + _value.DecrementTo(targetValue); + Publish(); } - public void Inc(double increment = 1) => Unlabelled.Inc(increment); - public void Set(double val) => Unlabelled.Set(val); - public void Dec(double decrement = 1) => Unlabelled.Dec(decrement); - public void IncTo(double targetValue) => Unlabelled.IncTo(targetValue); - public void DecTo(double targetValue) => Unlabelled.DecTo(targetValue); - public double Value => Unlabelled.Value; - public void Publish() => Unlabelled.Publish(); - public void Unpublish() => Unlabelled.Unpublish(); + public double Value => _value.Value; + } - internal override MetricType Type => MetricType.Gauge; + private protected override Child NewChild(LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish) + { + return new Child(this, instanceLabels, flattenedLabels, publish); + } - internal override int TimeseriesCount => ChildCount; + internal Gauge(string name, string help, StringSequence instanceLabelNames, LabelSequence staticLabels, bool suppressInitialValue) + : base(name, help, instanceLabelNames, staticLabels, suppressInitialValue) + { } + + public void Inc(double increment = 1) => Unlabelled.Inc(increment); + public void Set(double val) => Unlabelled.Set(val); + public void Dec(double decrement = 1) => Unlabelled.Dec(decrement); + public void IncTo(double targetValue) => Unlabelled.IncTo(targetValue); + public void DecTo(double targetValue) => Unlabelled.DecTo(targetValue); + public double Value => Unlabelled.Value; + public void Publish() => Unlabelled.Publish(); + public void Unpublish() => Unlabelled.Unpublish(); + + internal override MetricType Type => MetricType.Gauge; + + internal override int TimeseriesCount => ChildCount; } \ No newline at end of file diff --git a/Prometheus/MetricFactory.cs b/Prometheus/MetricFactory.cs index 7fbf5f1f..2a3be341 100644 --- a/Prometheus/MetricFactory.cs +++ b/Prometheus/MetricFactory.cs @@ -1,175 +1,174 @@ -namespace Prometheus +namespace Prometheus; + +/// +/// Adds metrics to a registry. +/// +public sealed class MetricFactory : IMetricFactory { + private readonly CollectorRegistry _registry; + + // If set, these labels will be applied to all created metrics, acting as additional static labels scoped to this factory. + // These are appended to the metric-specific static labels set at metric creation time. + private readonly LabelSequence _factoryLabels; + + // Both the factory-defined and the registry-defined static labels. + // TODO: We should validate that registry labels cannot be defined any more once we have already resolved this. + private readonly Lazy _staticLabelsLazy; + + internal MetricFactory(CollectorRegistry registry) : this(registry, LabelSequence.Empty) + { + } + + internal MetricFactory(CollectorRegistry registry, LabelSequence withLabels) + { + _registry = registry ?? throw new ArgumentNullException(nameof(registry)); + _factoryLabels = withLabels; + + _staticLabelsLazy = new Lazy(ResolveStaticLabels); + } + + private LabelSequence ResolveStaticLabels() + { + if (_factoryLabels.Length != 0) + return _factoryLabels.Concat(_registry.GetStaticLabels()); + else + return _registry.GetStaticLabels(); + } + /// - /// Adds metrics to a registry. + /// Counters only increase in value and reset to zero when the process restarts. /// - public sealed class MetricFactory : IMetricFactory - { - private readonly CollectorRegistry _registry; + public Counter CreateCounter(string name, string help, CounterConfiguration? configuration = null) + => CreateCounter(name, help, configuration?.LabelNames ?? Array.Empty(), configuration); - // If set, these labels will be applied to all created metrics, acting as additional static labels scoped to this factory. - // These are appended to the metric-specific static labels set at metric creation time. - private readonly LabelSequence _factoryLabels; + /// + /// Gauges can have any numeric value and change arbitrarily. + /// + public Gauge CreateGauge(string name, string help, GaugeConfiguration? configuration = null) + => CreateGauge(name, help, configuration?.LabelNames ?? Array.Empty(), configuration); - // Both the factory-defined and the registry-defined static labels. - // TODO: We should validate that registry labels cannot be defined any more once we have already resolved this. - private readonly Lazy _staticLabelsLazy; + /// + /// Summaries track the trends in events over time (10 minutes by default). + /// + public Summary CreateSummary(string name, string help, SummaryConfiguration? configuration = null) + => CreateSummary(name, help, configuration?.LabelNames ?? Array.Empty(), configuration); - internal MetricFactory(CollectorRegistry registry) : this(registry, LabelSequence.Empty) - { - } + /// + /// Histograms track the size and number of events in buckets. + /// + public Histogram CreateHistogram(string name, string help, HistogramConfiguration? configuration = null) + => CreateHistogram(name, help, configuration?.LabelNames ?? Array.Empty(), configuration); - internal MetricFactory(CollectorRegistry registry, LabelSequence withLabels) - { - _registry = registry ?? throw new ArgumentNullException(nameof(registry)); - _factoryLabels = withLabels; + /// + /// Counters only increase in value and reset to zero when the process restarts. + /// + public Counter CreateCounter(string name, string help, string[] labelNames, CounterConfiguration? configuration = null) + => CreateCounter(name, help, StringSequence.From(labelNames), configuration); - _staticLabelsLazy = new Lazy(ResolveStaticLabels); - } + /// + /// Gauges can have any numeric value and change arbitrarily. + /// + public Gauge CreateGauge(string name, string help, string[] labelNames, GaugeConfiguration? configuration = null) + => CreateGauge(name, help, StringSequence.From(labelNames), configuration); - private LabelSequence ResolveStaticLabels() - { - if (_factoryLabels.Length != 0) - return _factoryLabels.Concat(_registry.GetStaticLabels()); - else - return _registry.GetStaticLabels(); - } + /// + /// Summaries track the trends in events over time (10 minutes by default). + /// + public Histogram CreateHistogram(string name, string help, string[] labelNames, HistogramConfiguration? configuration = null) + => CreateHistogram(name, help, StringSequence.From(labelNames), configuration); - /// - /// Counters only increase in value and reset to zero when the process restarts. - /// - public Counter CreateCounter(string name, string help, CounterConfiguration? configuration = null) - => CreateCounter(name, help, configuration?.LabelNames ?? Array.Empty(), configuration); - - /// - /// Gauges can have any numeric value and change arbitrarily. - /// - public Gauge CreateGauge(string name, string help, GaugeConfiguration? configuration = null) - => CreateGauge(name, help, configuration?.LabelNames ?? Array.Empty(), configuration); - - /// - /// Summaries track the trends in events over time (10 minutes by default). - /// - public Summary CreateSummary(string name, string help, SummaryConfiguration? configuration = null) - => CreateSummary(name, help, configuration?.LabelNames ?? Array.Empty(), configuration); - - /// - /// Histograms track the size and number of events in buckets. - /// - public Histogram CreateHistogram(string name, string help, HistogramConfiguration? configuration = null) - => CreateHistogram(name, help, configuration?.LabelNames ?? Array.Empty(), configuration); - - /// - /// Counters only increase in value and reset to zero when the process restarts. - /// - public Counter CreateCounter(string name, string help, string[] labelNames, CounterConfiguration? configuration = null) - => CreateCounter(name, help, StringSequence.From(labelNames), configuration); - - /// - /// Gauges can have any numeric value and change arbitrarily. - /// - public Gauge CreateGauge(string name, string help, string[] labelNames, GaugeConfiguration? configuration = null) - => CreateGauge(name, help, StringSequence.From(labelNames), configuration); - - /// - /// Summaries track the trends in events over time (10 minutes by default). - /// - public Histogram CreateHistogram(string name, string help, string[] labelNames, HistogramConfiguration? configuration = null) - => CreateHistogram(name, help, StringSequence.From(labelNames), configuration); - - /// - /// Histograms track the size and number of events in buckets. - /// - public Summary CreateSummary(string name, string help, string[] labelNames, SummaryConfiguration? configuration = null) - => CreateSummary(name, help, StringSequence.From(labelNames), configuration); - - internal Counter CreateCounter(string name, string help, StringSequence instanceLabelNames, CounterConfiguration? configuration) - { - static Counter CreateInstance(string finalName, string finalHelp, StringSequence finalInstanceLabelNames, LabelSequence finalStaticLabels, CounterConfiguration finalConfiguration) - { - return new Counter(finalName, finalHelp, finalInstanceLabelNames, finalStaticLabels, finalConfiguration.SuppressInitialValue); - } + /// + /// Histograms track the size and number of events in buckets. + /// + public Summary CreateSummary(string name, string help, string[] labelNames, SummaryConfiguration? configuration = null) + => CreateSummary(name, help, StringSequence.From(labelNames), configuration); - var initializer = new CollectorRegistry.CollectorInitializer(CreateInstance, name, help, instanceLabelNames, _staticLabelsLazy.Value, configuration ?? CounterConfiguration.Default); - return _registry.GetOrAdd(initializer); + internal Counter CreateCounter(string name, string help, StringSequence instanceLabelNames, CounterConfiguration? configuration) + { + static Counter CreateInstance(string finalName, string finalHelp, StringSequence finalInstanceLabelNames, LabelSequence finalStaticLabels, CounterConfiguration finalConfiguration) + { + return new Counter(finalName, finalHelp, finalInstanceLabelNames, finalStaticLabels, finalConfiguration.SuppressInitialValue); } - internal Gauge CreateGauge(string name, string help, StringSequence instanceLabelNames, GaugeConfiguration? configuration) - { - static Gauge CreateInstance(string finalName, string finalHelp, StringSequence finalInstanceLabelNames, LabelSequence finalStaticLabels, GaugeConfiguration finalConfiguration) - { - return new Gauge(finalName, finalHelp, finalInstanceLabelNames, finalStaticLabels, finalConfiguration.SuppressInitialValue); - } + var initializer = new CollectorRegistry.CollectorInitializer(CreateInstance, name, help, instanceLabelNames, _staticLabelsLazy.Value, configuration ?? CounterConfiguration.Default); + return _registry.GetOrAdd(initializer); + } - var initializer = new CollectorRegistry.CollectorInitializer(CreateInstance, name, help, instanceLabelNames, _staticLabelsLazy.Value, configuration ?? GaugeConfiguration.Default); - return _registry.GetOrAdd(initializer); + internal Gauge CreateGauge(string name, string help, StringSequence instanceLabelNames, GaugeConfiguration? configuration) + { + static Gauge CreateInstance(string finalName, string finalHelp, StringSequence finalInstanceLabelNames, LabelSequence finalStaticLabels, GaugeConfiguration finalConfiguration) + { + return new Gauge(finalName, finalHelp, finalInstanceLabelNames, finalStaticLabels, finalConfiguration.SuppressInitialValue); } - internal Histogram CreateHistogram(string name, string help, StringSequence instanceLabelNames, HistogramConfiguration? configuration) - { - static Histogram CreateInstance(string finalName, string finalHelp, StringSequence finalInstanceLabelNames, LabelSequence finalStaticLabels, HistogramConfiguration finalConfiguration) - { - return new Histogram(finalName, finalHelp, finalInstanceLabelNames, finalStaticLabels, finalConfiguration.SuppressInitialValue, finalConfiguration.Buckets); - } + var initializer = new CollectorRegistry.CollectorInitializer(CreateInstance, name, help, instanceLabelNames, _staticLabelsLazy.Value, configuration ?? GaugeConfiguration.Default); + return _registry.GetOrAdd(initializer); + } - var initializer = new CollectorRegistry.CollectorInitializer(CreateInstance, name, help, instanceLabelNames, _staticLabelsLazy.Value, configuration ?? HistogramConfiguration.Default); - return _registry.GetOrAdd(initializer); + internal Histogram CreateHistogram(string name, string help, StringSequence instanceLabelNames, HistogramConfiguration? configuration) + { + static Histogram CreateInstance(string finalName, string finalHelp, StringSequence finalInstanceLabelNames, LabelSequence finalStaticLabels, HistogramConfiguration finalConfiguration) + { + return new Histogram(finalName, finalHelp, finalInstanceLabelNames, finalStaticLabels, finalConfiguration.SuppressInitialValue, finalConfiguration.Buckets); } - internal Summary CreateSummary(string name, string help, StringSequence instanceLabelNames, SummaryConfiguration? configuration) + var initializer = new CollectorRegistry.CollectorInitializer(CreateInstance, name, help, instanceLabelNames, _staticLabelsLazy.Value, configuration ?? HistogramConfiguration.Default); + return _registry.GetOrAdd(initializer); + } + + internal Summary CreateSummary(string name, string help, StringSequence instanceLabelNames, SummaryConfiguration? configuration) + { + static Summary CreateInstance(string finalName, string finalHelp, StringSequence finalInstanceLabelNames, LabelSequence finalStaticLabels, SummaryConfiguration finalConfiguration) { - static Summary CreateInstance(string finalName, string finalHelp, StringSequence finalInstanceLabelNames, LabelSequence finalStaticLabels, SummaryConfiguration finalConfiguration) - { - return new Summary(finalName, finalHelp, finalInstanceLabelNames, finalStaticLabels, finalConfiguration.SuppressInitialValue, - finalConfiguration.Objectives, finalConfiguration.MaxAge, finalConfiguration.AgeBuckets, finalConfiguration.BufferSize); - } - - var initializer = new CollectorRegistry.CollectorInitializer(CreateInstance, name, help, instanceLabelNames, _staticLabelsLazy.Value, configuration ?? SummaryConfiguration.Default); - return _registry.GetOrAdd(initializer); + return new Summary(finalName, finalHelp, finalInstanceLabelNames, finalStaticLabels, finalConfiguration.SuppressInitialValue, + finalConfiguration.Objectives, finalConfiguration.MaxAge, finalConfiguration.AgeBuckets, finalConfiguration.BufferSize); } - /// - /// Counters only increase in value and reset to zero when the process restarts. - /// - public Counter CreateCounter(string name, string help, params string[] labelNames) => CreateCounter(name, help, labelNames, null); + var initializer = new CollectorRegistry.CollectorInitializer(CreateInstance, name, help, instanceLabelNames, _staticLabelsLazy.Value, configuration ?? SummaryConfiguration.Default); + return _registry.GetOrAdd(initializer); + } - /// - /// Gauges can have any numeric value and change arbitrarily. - /// - public Gauge CreateGauge(string name, string help, params string[] labelNames) => CreateGauge(name, help, labelNames, null); + /// + /// Counters only increase in value and reset to zero when the process restarts. + /// + public Counter CreateCounter(string name, string help, params string[] labelNames) => CreateCounter(name, help, labelNames, null); - /// - /// Summaries track the trends in events over time (10 minutes by default). - /// - public Summary CreateSummary(string name, string help, params string[] labelNames) => CreateSummary(name, help, labelNames, null); + /// + /// Gauges can have any numeric value and change arbitrarily. + /// + public Gauge CreateGauge(string name, string help, params string[] labelNames) => CreateGauge(name, help, labelNames, null); - /// - /// Histograms track the size and number of events in buckets. - /// - public Histogram CreateHistogram(string name, string help, params string[] labelNames) => CreateHistogram(name, help, labelNames, null); + /// + /// Summaries track the trends in events over time (10 minutes by default). + /// + public Summary CreateSummary(string name, string help, params string[] labelNames) => CreateSummary(name, help, labelNames, null); - public IMetricFactory WithLabels(IDictionary labels) - { - if (labels.Count == 0) - return this; + /// + /// Histograms track the size and number of events in buckets. + /// + public Histogram CreateHistogram(string name, string help, params string[] labelNames) => CreateHistogram(name, help, labelNames, null); - var newLabels = LabelSequence.From(labels); + public IMetricFactory WithLabels(IDictionary labels) + { + if (labels.Count == 0) + return this; - // Add any already-inherited labels to the end (rule is that lower levels go first, higher levels last). - var newFactoryLabels = newLabels.Concat(_factoryLabels); + var newLabels = LabelSequence.From(labels); - return new MetricFactory(_registry, newFactoryLabels); - } + // Add any already-inherited labels to the end (rule is that lower levels go first, higher levels last). + var newFactoryLabels = newLabels.Concat(_factoryLabels); - /// - /// Gets all the existing label names predefined either in the factory or in the registry. - /// - internal StringSequence GetAllStaticLabelNames() - { - return _factoryLabels.Names.Concat(_registry.GetStaticLabels().Names); - } + return new MetricFactory(_registry, newFactoryLabels); + } - public IManagedLifetimeMetricFactory WithManagedLifetime(TimeSpan expiresAfter) => - new ManagedLifetimeMetricFactory(this, expiresAfter); + /// + /// Gets all the existing label names predefined either in the factory or in the registry. + /// + internal StringSequence GetAllStaticLabelNames() + { + return _factoryLabels.Names.Concat(_registry.GetStaticLabels().Names); } + + public IManagedLifetimeMetricFactory WithManagedLifetime(TimeSpan expiresAfter) => + new ManagedLifetimeMetricFactory(this, expiresAfter); } \ No newline at end of file diff --git a/Tests.NetCore/MetricsTests.cs b/Tests.NetCore/MetricsTests.cs index 90ba000d..53df00e6 100644 --- a/Tests.NetCore/MetricsTests.cs +++ b/Tests.NetCore/MetricsTests.cs @@ -4,409 +4,408 @@ using System.Linq; using System.Threading.Tasks; -namespace Prometheus.Tests +namespace Prometheus.Tests; + +[TestClass] +public sealed class MetricsTests { - [TestClass] - public sealed class MetricsTests - { - private CollectorRegistry _registry; - private MetricFactory _metrics; + private CollectorRegistry _registry; + private MetricFactory _metrics; - public MetricsTests() - { - _registry = Metrics.NewCustomRegistry(); - _metrics = Metrics.WithCustomRegistry(_registry); - } + public MetricsTests() + { + _registry = Metrics.NewCustomRegistry(); + _metrics = Metrics.WithCustomRegistry(_registry); + } - [TestMethod] - public void api_usage() - { - var gauge = _metrics.CreateGauge("name1", "help1"); - gauge.Inc(); - Assert.AreEqual(1, gauge.Value); - gauge.Inc(3.2); - Assert.AreEqual(4.2, gauge.Value); - gauge.Set(4); - Assert.AreEqual(4, gauge.Value); - gauge.Dec(0.2); - Assert.AreEqual(3.8, gauge.Value); - - Assert.ThrowsException(() => gauge.Labels("1")); - - var counter = _metrics.CreateCounter("name2", "help2", "label1"); - counter.Inc(); - counter.Inc(3.2); - counter.Inc(0); - Assert.ThrowsException(() => counter.Inc(-1)); - Assert.AreEqual(4.2, counter.Value); - - Assert.AreEqual(0, counter.Labels("a").Value); - counter.Labels("a").Inc(3.3); - counter.Labels("a").Inc(1.1); - Assert.AreEqual(4.4, counter.Labels("a").Value); - } + [TestMethod] + public void api_usage() + { + var gauge = _metrics.CreateGauge("name1", "help1"); + gauge.Inc(); + Assert.AreEqual(1, gauge.Value); + gauge.Inc(3.2); + Assert.AreEqual(4.2, gauge.Value); + gauge.Set(4); + Assert.AreEqual(4, gauge.Value); + gauge.Dec(0.2); + Assert.AreEqual(3.8, gauge.Value); + + Assert.ThrowsException(() => gauge.Labels("1")); + + var counter = _metrics.CreateCounter("name2", "help2", "label1"); + counter.Inc(); + counter.Inc(3.2); + counter.Inc(0); + Assert.ThrowsException(() => counter.Inc(-1)); + Assert.AreEqual(4.2, counter.Value); + + Assert.AreEqual(0, counter.Labels("a").Value); + counter.Labels("a").Inc(3.3); + counter.Labels("a").Inc(1.1); + Assert.AreEqual(4.4, counter.Labels("a").Value); + } - [TestMethod] - public async Task CreateCounter_WithDifferentRegistry_CreatesIndependentCounters() - { - var registry1 = Metrics.NewCustomRegistry(); - var registry2 = Metrics.NewCustomRegistry(); - var counter1 = Metrics.WithCustomRegistry(registry1) - .CreateCounter("counter", ""); - var counter2 = Metrics.WithCustomRegistry(registry2) - .CreateCounter("counter", ""); + [TestMethod] + public async Task CreateCounter_WithDifferentRegistry_CreatesIndependentCounters() + { + var registry1 = Metrics.NewCustomRegistry(); + var registry2 = Metrics.NewCustomRegistry(); + var counter1 = Metrics.WithCustomRegistry(registry1) + .CreateCounter("counter", ""); + var counter2 = Metrics.WithCustomRegistry(registry2) + .CreateCounter("counter", ""); - Assert.AreNotSame(counter1, counter2); + Assert.AreNotSame(counter1, counter2); - counter1.Inc(); - counter2.Inc(); + counter1.Inc(); + counter2.Inc(); - Assert.AreEqual(1, counter1.Value); - Assert.AreEqual(1, counter2.Value); + Assert.AreEqual(1, counter1.Value); + Assert.AreEqual(1, counter2.Value); - var serializer1 = Substitute.For(); - await registry1.CollectAndSerializeAsync(serializer1, default); + var serializer1 = Substitute.For(); + await registry1.CollectAndSerializeAsync(serializer1, default); - var serializer2 = Substitute.For(); - await registry2.CollectAndSerializeAsync(serializer2, default); + var serializer2 = Substitute.For(); + await registry2.CollectAndSerializeAsync(serializer2, default); - await serializer1.ReceivedWithAnyArgs().WriteFamilyDeclarationAsync(default, default, default, default, default, default); - await serializer1.ReceivedWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default, default); + await serializer1.ReceivedWithAnyArgs().WriteFamilyDeclarationAsync(default, default, default, default, default, default); + await serializer1.ReceivedWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default, default); - await serializer2.ReceivedWithAnyArgs().WriteFamilyDeclarationAsync(default, default, default, default, default, default); - await serializer2.ReceivedWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default, default); - } + await serializer2.ReceivedWithAnyArgs().WriteFamilyDeclarationAsync(default, default, default, default, default, default); + await serializer2.ReceivedWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default, default); + } - [TestMethod] - public async Task Export_FamilyWithOnlyNonpublishedUnlabeledMetrics_ExportsFamilyDeclaration() + [TestMethod] + public async Task Export_FamilyWithOnlyNonpublishedUnlabeledMetrics_ExportsFamilyDeclaration() + { + // See https://github.com/prometheus-net/prometheus-net/issues/196 + var metric = _metrics.CreateCounter("my_family", "", new CounterConfiguration { - // See https://github.com/prometheus-net/prometheus-net/issues/196 - var metric = _metrics.CreateCounter("my_family", "", new CounterConfiguration - { - SuppressInitialValue = true - }); + SuppressInitialValue = true + }); - var serializer = Substitute.For(); - await _registry.CollectAndSerializeAsync(serializer, default); + var serializer = Substitute.For(); + await _registry.CollectAndSerializeAsync(serializer, default); - await serializer.ReceivedWithAnyArgs(1).WriteFamilyDeclarationAsync(default, default, default, default, default, default); - await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default,default, default); - serializer.ClearReceivedCalls(); + await serializer.ReceivedWithAnyArgs(1).WriteFamilyDeclarationAsync(default, default, default, default, default, default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default,default, default); + serializer.ClearReceivedCalls(); - metric.Inc(); - metric.Unpublish(); + metric.Inc(); + metric.Unpublish(); - await _registry.CollectAndSerializeAsync(serializer, default); + await _registry.CollectAndSerializeAsync(serializer, default); - await serializer.ReceivedWithAnyArgs(1).WriteFamilyDeclarationAsync(default, default, default, default, default, default); - await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default,default, default); - } + await serializer.ReceivedWithAnyArgs(1).WriteFamilyDeclarationAsync(default, default, default, default, default, default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default,default, default); + } - [TestMethod] - public async Task Export_FamilyWithOnlyNonpublishedLabeledMetrics_ExportsFamilyDeclaration() + [TestMethod] + public async Task Export_FamilyWithOnlyNonpublishedLabeledMetrics_ExportsFamilyDeclaration() + { + // See https://github.com/prometheus-net/prometheus-net/issues/196 + var metric = _metrics.CreateCounter("my_family", "", new[] { "labelname" }, new CounterConfiguration { - // See https://github.com/prometheus-net/prometheus-net/issues/196 - var metric = _metrics.CreateCounter("my_family", "", new[] { "labelname" }, new CounterConfiguration - { - SuppressInitialValue = true, - }); + SuppressInitialValue = true, + }); - var instance = metric.WithLabels("labelvalue"); + var instance = metric.WithLabels("labelvalue"); - var serializer = Substitute.For(); - await _registry.CollectAndSerializeAsync(serializer, default); + var serializer = Substitute.For(); + await _registry.CollectAndSerializeAsync(serializer, default); - await serializer.ReceivedWithAnyArgs(1).WriteFamilyDeclarationAsync(default, default, default, default, default, default); - await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default,default, default); - serializer.ClearReceivedCalls(); + await serializer.ReceivedWithAnyArgs(1).WriteFamilyDeclarationAsync(default, default, default, default, default, default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default,default, default); + serializer.ClearReceivedCalls(); - instance.Inc(); - instance.Unpublish(); + instance.Inc(); + instance.Unpublish(); - await _registry.CollectAndSerializeAsync(serializer, default); + await _registry.CollectAndSerializeAsync(serializer, default); - await serializer.ReceivedWithAnyArgs(1).WriteFamilyDeclarationAsync(default, default, default, default, default, default); - await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default,default, default); - } + await serializer.ReceivedWithAnyArgs(1).WriteFamilyDeclarationAsync(default, default, default, default, default, default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default,default, default); + } - [TestMethod] - public async Task DisposeChild_RemovesMetric() + [TestMethod] + public async Task DisposeChild_RemovesMetric() + { + var metric = _metrics.CreateCounter("my_family", "", new[] { "labelname" }, new CounterConfiguration { - var metric = _metrics.CreateCounter("my_family", "", new[] { "labelname" }, new CounterConfiguration - { - SuppressInitialValue = true, - }); + SuppressInitialValue = true, + }); - var instance = metric.WithLabels("labelvalue"); - instance.Inc(); + var instance = metric.WithLabels("labelvalue"); + instance.Inc(); - var serializer = Substitute.For(); - await _registry.CollectAndSerializeAsync(serializer, default); + var serializer = Substitute.For(); + await _registry.CollectAndSerializeAsync(serializer, default); - await serializer.ReceivedWithAnyArgs(1).WriteFamilyDeclarationAsync(default, default, default, default, default, default); - await serializer.ReceivedWithAnyArgs(1).WriteMetricPointAsync(default, default, default, default, default, default); - serializer.ClearReceivedCalls(); + await serializer.ReceivedWithAnyArgs(1).WriteFamilyDeclarationAsync(default, default, default, default, default, default); + await serializer.ReceivedWithAnyArgs(1).WriteMetricPointAsync(default, default, default, default, default, default); + serializer.ClearReceivedCalls(); - instance.Dispose(); - - await _registry.CollectAndSerializeAsync(serializer, default); - - await serializer.ReceivedWithAnyArgs(1).WriteFamilyDeclarationAsync(default, default, default, default, default, default); - await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default, default); - } + instance.Dispose(); + + await _registry.CollectAndSerializeAsync(serializer, default); + + await serializer.ReceivedWithAnyArgs(1).WriteFamilyDeclarationAsync(default, default, default, default, default, default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default, default); + } - [TestMethod] - public void histogram_no_buckets() + [TestMethod] + public void histogram_no_buckets() + { + try { - try - { - _metrics.CreateHistogram("hist", "help", new HistogramConfiguration - { - Buckets = new double[0] - }); - - Assert.Fail("Expected an exception"); - } - catch (ArgumentException ex) + _metrics.CreateHistogram("hist", "help", new HistogramConfiguration { - Assert.AreEqual("Histogram must have at least one bucket", ex.Message); - } + Buckets = new double[0] + }); + + Assert.Fail("Expected an exception"); + } + catch (ArgumentException ex) + { + Assert.AreEqual("Histogram must have at least one bucket", ex.Message); } + } - [TestMethod] - public void histogram_buckets_do_not_increase() + [TestMethod] + public void histogram_buckets_do_not_increase() + { + try { - try + _metrics.CreateHistogram("hist", "help", new HistogramConfiguration { - _metrics.CreateHistogram("hist", "help", new HistogramConfiguration - { - Buckets = new double[] { 0.5, 0.1 } - }); - - Assert.Fail("Expected an exception"); - } - catch (ArgumentException ex) - { - Assert.AreEqual("Bucket values must be increasing", ex.Message); - } - } + Buckets = new double[] { 0.5, 0.1 } + }); - [TestMethod] - public void histogram_exponential_buckets_are_correct() + Assert.Fail("Expected an exception"); + } + catch (ArgumentException ex) { - var bucketsStart = 1.1; - var bucketsFactor = 2.4; - var bucketsCount = 4; + Assert.AreEqual("Bucket values must be increasing", ex.Message); + } + } - var buckets = Histogram.ExponentialBuckets(bucketsStart, bucketsFactor, bucketsCount); + [TestMethod] + public void histogram_exponential_buckets_are_correct() + { + var bucketsStart = 1.1; + var bucketsFactor = 2.4; + var bucketsCount = 4; - Assert.AreEqual(bucketsCount, buckets.Length); - Assert.AreEqual(1.1, buckets[0]); - Assert.AreEqual(2.64, buckets[1]); - Assert.AreEqual(6.336, buckets[2]); - Assert.AreEqual(15.2064, buckets[3]); - } + var buckets = Histogram.ExponentialBuckets(bucketsStart, bucketsFactor, bucketsCount); - [TestMethod] - public void histogram_exponential_buckets_with_non_positive_count_throws() - { - var bucketsStart = 1; - var bucketsFactor = 2; + Assert.AreEqual(bucketsCount, buckets.Length); + Assert.AreEqual(1.1, buckets[0]); + Assert.AreEqual(2.64, buckets[1]); + Assert.AreEqual(6.336, buckets[2]); + Assert.AreEqual(15.2064, buckets[3]); + } - Assert.ThrowsException(() => Histogram.ExponentialBuckets(bucketsStart, bucketsFactor, -1)); - Assert.ThrowsException(() => Histogram.ExponentialBuckets(bucketsStart, bucketsFactor, 0)); - } + [TestMethod] + public void histogram_exponential_buckets_with_non_positive_count_throws() + { + var bucketsStart = 1; + var bucketsFactor = 2; - [TestMethod] - public void histogram_exponential_buckets_with_non_positive_start_throws() - { - var bucketsFactor = 2; - var bucketsCount = 5; + Assert.ThrowsException(() => Histogram.ExponentialBuckets(bucketsStart, bucketsFactor, -1)); + Assert.ThrowsException(() => Histogram.ExponentialBuckets(bucketsStart, bucketsFactor, 0)); + } - Assert.ThrowsException(() => Histogram.ExponentialBuckets(-1, bucketsFactor, bucketsCount)); - Assert.ThrowsException(() => Histogram.ExponentialBuckets(0, bucketsFactor, bucketsCount)); - } + [TestMethod] + public void histogram_exponential_buckets_with_non_positive_start_throws() + { + var bucketsFactor = 2; + var bucketsCount = 5; - [TestMethod] - public void histogram_exponential_buckets_with__factor_less_than_one_throws() - { - var bucketsStart = 1; - var bucketsCount = 5; + Assert.ThrowsException(() => Histogram.ExponentialBuckets(-1, bucketsFactor, bucketsCount)); + Assert.ThrowsException(() => Histogram.ExponentialBuckets(0, bucketsFactor, bucketsCount)); + } - Assert.ThrowsException(() => Histogram.ExponentialBuckets(bucketsStart, 0.9, bucketsCount)); - Assert.ThrowsException(() => Histogram.ExponentialBuckets(bucketsStart, 0, bucketsCount)); - Assert.ThrowsException(() => Histogram.ExponentialBuckets(bucketsStart, -1, bucketsCount)); - } + [TestMethod] + public void histogram_exponential_buckets_with__factor_less_than_one_throws() + { + var bucketsStart = 1; + var bucketsCount = 5; - [TestMethod] - public void histogram_linear_buckets_are_correct() - { - var bucketsStart = 1.1; - var bucketsWidth = 2.4; - var bucketsCount = 4; + Assert.ThrowsException(() => Histogram.ExponentialBuckets(bucketsStart, 0.9, bucketsCount)); + Assert.ThrowsException(() => Histogram.ExponentialBuckets(bucketsStart, 0, bucketsCount)); + Assert.ThrowsException(() => Histogram.ExponentialBuckets(bucketsStart, -1, bucketsCount)); + } - var buckets = Histogram.LinearBuckets(bucketsStart, bucketsWidth, bucketsCount); + [TestMethod] + public void histogram_linear_buckets_are_correct() + { + var bucketsStart = 1.1; + var bucketsWidth = 2.4; + var bucketsCount = 4; - Assert.AreEqual(bucketsCount, buckets.Length); - Assert.AreEqual(1.1, buckets[0]); - Assert.AreEqual(3.5, buckets[1]); - Assert.AreEqual(5.9, buckets[2]); - Assert.AreEqual(8.3, buckets[3]); - } + var buckets = Histogram.LinearBuckets(bucketsStart, bucketsWidth, bucketsCount); - [TestMethod] - public void histogram_linear_buckets_with_non_positive_count_throws() - { - var bucketsStart = 1; - var bucketsWidth = 2; + Assert.AreEqual(bucketsCount, buckets.Length); + Assert.AreEqual(1.1, buckets[0]); + Assert.AreEqual(3.5, buckets[1]); + Assert.AreEqual(5.9, buckets[2]); + Assert.AreEqual(8.3, buckets[3]); + } - Assert.ThrowsException(() => Histogram.LinearBuckets(bucketsStart, bucketsWidth, -1)); - Assert.ThrowsException(() => Histogram.LinearBuckets(bucketsStart, bucketsWidth, 0)); - } + [TestMethod] + public void histogram_linear_buckets_with_non_positive_count_throws() + { + var bucketsStart = 1; + var bucketsWidth = 2; - [TestMethod] - public void same_labels_return_same_instance() - { - var gauge = _metrics.CreateGauge("name1", "help1", "label1"); + Assert.ThrowsException(() => Histogram.LinearBuckets(bucketsStart, bucketsWidth, -1)); + Assert.ThrowsException(() => Histogram.LinearBuckets(bucketsStart, bucketsWidth, 0)); + } - var labelled1 = gauge.Labels("1"); + [TestMethod] + public void same_labels_return_same_instance() + { + var gauge = _metrics.CreateGauge("name1", "help1", "label1"); - var labelled2 = gauge.Labels("1"); + var labelled1 = gauge.Labels("1"); - Assert.AreSame(labelled2, labelled1); - } + var labelled2 = gauge.Labels("1"); - [TestMethod] - public async Task CreateMetric_WithSameMetadataButDifferentLabels_CreatesMetric() - { - // This is a deviation from standard Prometheus practices, where you can only use a metric name with a single set of label names. - // Instead, this library allows the same metric name to be used with different sets of label names, as long as all other metadata exists. - // The reason for this is that we want to support using prometheus-net as a bridge to report data originating from metrics systems - // that do not have such limitations (such as the .NET 6 Meters API). Such scenarios can create any combinations of label names. - // This is permissible by OpenMetrics, though violates the Prometheus client authoring requirements (which is OK - what can you do). - - var gauge1 = _metrics.CreateGauge("name1", "h"); - var gauge2 = _metrics.CreateGauge("name1", "h", "label1"); - var gauge3 = _metrics.CreateGauge("name1", "h", "label2"); - var gauge4 = _metrics.CreateGauge("name1", "h", "label1", "label3"); - - // We expect all the metrics registered to be unique instances. - Assert.AreNotSame(gauge1, gauge2); - Assert.AreNotSame(gauge2, gauge3); - Assert.AreNotSame(gauge3, gauge4); - - var gauge1Again = _metrics.CreateGauge("name1", "h"); - var gauge2Again = _metrics.CreateGauge("name1", "h", "label1"); - var gauge3Again = _metrics.CreateGauge("name1", "h", "label2"); - var gauge4Again = _metrics.CreateGauge("name1", "h", "label1", "label3"); - - // We expect the instances to be sticky to the specific set of label names. - Assert.AreSame(gauge1, gauge1Again); - Assert.AreSame(gauge2, gauge2Again); - Assert.AreSame(gauge3, gauge3Again); - Assert.AreSame(gauge4, gauge4Again); - - var canary1 = 543289; - var canary2 = 735467; - var canary3 = 627864; - var canary4 = 837855; - - gauge1.Set(canary1); - gauge2.Set(canary2); - gauge3.Set(canary3); - gauge4.Set(canary4); - - var serialized = await _registry.CollectAndSerializeToStringAsync(); - - // We expect all of them to work (to publish data) and to work independently. - StringAssert.Contains(serialized, canary1.ToString()); - StringAssert.Contains(serialized, canary2.ToString()); - StringAssert.Contains(serialized, canary3.ToString()); - StringAssert.Contains(serialized, canary4.ToString()); - } + Assert.AreSame(labelled2, labelled1); + } - [TestMethod] - public void cannot_create_metrics_with_the_same_name_and_labels_but_different_type() - { - _metrics.CreateGauge("name1", "h", "label1"); - try - { - _metrics.CreateCounter("name1", "h", "label1"); - Assert.Fail("should have thrown"); - } - catch (InvalidOperationException e) - { - Assert.AreEqual("Collector of a different type with the same identity is already registered.", e.Message); - } - } + [TestMethod] + public async Task CreateMetric_WithSameMetadataButDifferentLabels_CreatesMetric() + { + // This is a deviation from standard Prometheus practices, where you can only use a metric name with a single set of label names. + // Instead, this library allows the same metric name to be used with different sets of label names, as long as all other metadata exists. + // The reason for this is that we want to support using prometheus-net as a bridge to report data originating from metrics systems + // that do not have such limitations (such as the .NET 6 Meters API). Such scenarios can create any combinations of label names. + // This is permissible by OpenMetrics, though violates the Prometheus client authoring requirements (which is OK - what can you do). + + var gauge1 = _metrics.CreateGauge("name1", "h"); + var gauge2 = _metrics.CreateGauge("name1", "h", "label1"); + var gauge3 = _metrics.CreateGauge("name1", "h", "label2"); + var gauge4 = _metrics.CreateGauge("name1", "h", "label1", "label3"); + + // We expect all the metrics registered to be unique instances. + Assert.AreNotSame(gauge1, gauge2); + Assert.AreNotSame(gauge2, gauge3); + Assert.AreNotSame(gauge3, gauge4); + + var gauge1Again = _metrics.CreateGauge("name1", "h"); + var gauge2Again = _metrics.CreateGauge("name1", "h", "label1"); + var gauge3Again = _metrics.CreateGauge("name1", "h", "label2"); + var gauge4Again = _metrics.CreateGauge("name1", "h", "label1", "label3"); + + // We expect the instances to be sticky to the specific set of label names. + Assert.AreSame(gauge1, gauge1Again); + Assert.AreSame(gauge2, gauge2Again); + Assert.AreSame(gauge3, gauge3Again); + Assert.AreSame(gauge4, gauge4Again); + + var canary1 = 543289; + var canary2 = 735467; + var canary3 = 627864; + var canary4 = 837855; + + gauge1.Set(canary1); + gauge2.Set(canary2); + gauge3.Set(canary3); + gauge4.Set(canary4); + + var serialized = await _registry.CollectAndSerializeToStringAsync(); + + // We expect all of them to work (to publish data) and to work independently. + StringAssert.Contains(serialized, canary1.ToString()); + StringAssert.Contains(serialized, canary2.ToString()); + StringAssert.Contains(serialized, canary3.ToString()); + StringAssert.Contains(serialized, canary4.ToString()); + } - [TestMethod] - public void metric_names() + [TestMethod] + public void cannot_create_metrics_with_the_same_name_and_labels_but_different_type() + { + _metrics.CreateGauge("name1", "h", "label1"); + try { - Assert.ThrowsException(() => _metrics.CreateGauge("my-metric", "help")); - Assert.ThrowsException(() => _metrics.CreateGauge("my!metric", "help")); - Assert.ThrowsException(() => _metrics.CreateGauge("%", "help")); - Assert.ThrowsException(() => _metrics.CreateGauge("5a", "help")); - Assert.ThrowsException(() => _metrics.CreateGauge("a:3", "help")); - - _metrics.CreateGauge("abc", "help"); - _metrics.CreateGauge("myMetric2", "help"); + _metrics.CreateCounter("name1", "h", "label1"); + Assert.Fail("should have thrown"); } - - [TestMethod] - public void label_names() + catch (InvalidOperationException e) { - Assert.ThrowsException(() => _metrics.CreateGauge("a", "help1", "my-label")); - Assert.ThrowsException(() => _metrics.CreateGauge("a", "help1", "my!label")); - Assert.ThrowsException(() => _metrics.CreateGauge("a", "help1", "my%label")); - Assert.ThrowsException(() => _metrics.CreateHistogram("a", "help1", "le")); - Assert.ThrowsException(() => _metrics.CreateHistogram("a", "help1", "my:label")); - _metrics.CreateGauge("b", "help1", "good_name"); - - Assert.ThrowsException(() => _metrics.CreateGauge("c", "help1", "__reserved")); + Assert.AreEqual("Collector of a different type with the same identity is already registered.", e.Message); } + } - [TestMethod] - public void label_values() - { - var metric = _metrics.CreateGauge("a", "help1", "mylabelname"); + [TestMethod] + public void metric_names() + { + Assert.ThrowsException(() => _metrics.CreateGauge("my-metric", "help")); + Assert.ThrowsException(() => _metrics.CreateGauge("my!metric", "help")); + Assert.ThrowsException(() => _metrics.CreateGauge("%", "help")); + Assert.ThrowsException(() => _metrics.CreateGauge("5a", "help")); + Assert.ThrowsException(() => _metrics.CreateGauge("a:3", "help")); + + _metrics.CreateGauge("abc", "help"); + _metrics.CreateGauge("myMetric2", "help"); + } - metric.Labels(""); - metric.Labels("mylabelvalue"); - Assert.ThrowsException(() => metric.Labels(new string[] { null })); - } + [TestMethod] + public void label_names() + { + Assert.ThrowsException(() => _metrics.CreateGauge("a", "help1", "my-label")); + Assert.ThrowsException(() => _metrics.CreateGauge("a", "help1", "my!label")); + Assert.ThrowsException(() => _metrics.CreateGauge("a", "help1", "my%label")); + Assert.ThrowsException(() => _metrics.CreateHistogram("a", "help1", "le")); + Assert.ThrowsException(() => _metrics.CreateHistogram("a", "help1", "my:label")); + _metrics.CreateGauge("b", "help1", "good_name"); + + Assert.ThrowsException(() => _metrics.CreateGauge("c", "help1", "__reserved")); + } - [TestMethod] - public void GetAllLabelValues_GetsThemAll() - { - var metric = _metrics.CreateGauge("ahdgfln", "ahegrtijpm", "a", "b", "c"); - metric.Labels("1", "2", "3"); - metric.Labels("4", "5", "6"); + [TestMethod] + public void label_values() + { + var metric = _metrics.CreateGauge("a", "help1", "mylabelname"); - var values = metric.GetAllLabelValues().OrderBy(v => v[0]).ToArray(); + metric.Labels(""); + metric.Labels("mylabelvalue"); + Assert.ThrowsException(() => metric.Labels(new string[] { null })); + } - Assert.AreEqual(2, values.Length); + [TestMethod] + public void GetAllLabelValues_GetsThemAll() + { + var metric = _metrics.CreateGauge("ahdgfln", "ahegrtijpm", "a", "b", "c"); + metric.Labels("1", "2", "3"); + metric.Labels("4", "5", "6"); - Assert.AreEqual(3, values[0].Length); - Assert.AreEqual("1", values[0][0]); - Assert.AreEqual("2", values[0][1]); - Assert.AreEqual("3", values[0][2]); + var values = metric.GetAllLabelValues().OrderBy(v => v[0]).ToArray(); - Assert.AreEqual(3, values[1].Length); - Assert.AreEqual("4", values[1][0]); - Assert.AreEqual("5", values[1][1]); - Assert.AreEqual("6", values[1][2]); - } + Assert.AreEqual(2, values.Length); - [TestMethod] - public void GetAllLabelValues_DoesNotGetUnlabelled() - { - var metric = _metrics.CreateGauge("ahdggfagfln", "ahegrgftijpm"); - metric.Inc(); + Assert.AreEqual(3, values[0].Length); + Assert.AreEqual("1", values[0][0]); + Assert.AreEqual("2", values[0][1]); + Assert.AreEqual("3", values[0][2]); - var values = metric.GetAllLabelValues().ToArray(); + Assert.AreEqual(3, values[1].Length); + Assert.AreEqual("4", values[1][0]); + Assert.AreEqual("5", values[1][1]); + Assert.AreEqual("6", values[1][2]); + } - Assert.AreEqual(0, values.Length); - } + [TestMethod] + public void GetAllLabelValues_DoesNotGetUnlabelled() + { + var metric = _metrics.CreateGauge("ahdggfagfln", "ahegrgftijpm"); + metric.Inc(); + + var values = metric.GetAllLabelValues().ToArray(); + + Assert.AreEqual(0, values.Length); } } From 69f002694f0ca3e81fd54c99291c7a58011da99b Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 29 Dec 2022 14:57:16 +0200 Subject: [PATCH 025/230] Code tidy --- Prometheus/ChildBase.cs | 28 +++ Prometheus/Counter.cs | 15 +- Prometheus/Histogram.cs | 15 +- Prometheus/TextSerializer.cs | 405 ++++++++++++++++++----------------- 4 files changed, 238 insertions(+), 225 deletions(-) diff --git a/Prometheus/ChildBase.cs b/Prometheus/ChildBase.cs index a5b1f719..7b1332e9 100644 --- a/Prometheus/ChildBase.cs +++ b/Prometheus/ChildBase.cs @@ -83,4 +83,32 @@ internal Task CollectAndSerializeAsync(IMetricsSerializer serializer, Cancellati // Same as above, just only called if we really need to serialize this metric (if publish is true). private protected abstract Task CollectAndSerializeImplAsync(IMetricsSerializer serializer, CancellationToken cancel); + + /// + /// Borrows an exemplar temporarily, to be later returned via ReturnBorrowedExemplar. + /// Borrowing ensures that no other thread is modifying it (as exemplars are not thread-safe). + /// You would typically want to do this while serializing the exemplar. + /// + internal ObservedExemplar BorrowExemplar(ref ObservedExemplar storage) + { + return Interlocked.Exchange(ref storage, ObservedExemplar.Empty); + } + + /// + /// Returns a borrowed exemplar to storage or the object pool, with correct handling for cases where it is Empty. + /// + internal void ReturnBorrowedExemplar(ref ObservedExemplar storage, ObservedExemplar borrowed) + { + if (borrowed == ObservedExemplar.Empty) + return; + + // Return the exemplar unless a new one has arrived, in which case we discard the old one we were holding. + var foundExemplar = Interlocked.CompareExchange(ref storage, borrowed, ObservedExemplar.Empty); + + if (foundExemplar != ObservedExemplar.Empty) + { + // A new exemplar had already been written, so we could not return the borrowed one. That's perfectly fine - discard it. + ObservedExemplar.ReturnPooledIfNotEmpty(borrowed); + } + } } \ No newline at end of file diff --git a/Prometheus/Counter.cs b/Prometheus/Counter.cs index 35ebb82d..40ad4f51 100644 --- a/Prometheus/Counter.cs +++ b/Prometheus/Counter.cs @@ -14,8 +14,7 @@ internal Child(Collector parent, LabelSequence instanceLabels, LabelSequence fla private protected override async Task CollectAndSerializeImplAsync(IMetricsSerializer serializer, CancellationToken cancel) { - // Borrow the current exemplar. We take ownership of the exemplar for the duration of the write. - ObservedExemplar exemplar = Interlocked.Exchange(ref _observedExemplar, ObservedExemplar.Empty); + var exemplar = BorrowExemplar(ref _observedExemplar); await serializer.WriteMetricPointAsync( Parent.NameBytes, @@ -25,17 +24,7 @@ await serializer.WriteMetricPointAsync( Value, exemplar); - if (exemplar != ObservedExemplar.Empty) - { - // Return the exemplar unless a new one has arrived, in which case we discard the old one we were holding. - var foundExemplar = Interlocked.CompareExchange(ref _observedExemplar, exemplar, ObservedExemplar.Empty); - - if (foundExemplar != ObservedExemplar.Empty) - { - // A new exemplar had already been written, so we could not return the borrowed one. That's perfectly fine - discard it. - ObservedExemplar.ReturnPooledIfNotEmpty(exemplar); - } - } + ReturnBorrowedExemplar(ref _observedExemplar, exemplar); } public void Inc(params Exemplar.LabelPair[] exemplarLabels) diff --git a/Prometheus/Histogram.cs b/Prometheus/Histogram.cs index 9216d64c..0e2cf4e1 100644 --- a/Prometheus/Histogram.cs +++ b/Prometheus/Histogram.cs @@ -104,8 +104,7 @@ await serializer.WriteMetricPointAsync( for (var i = 0; i < _bucketCounts.Length; i++) { - // Borrow the current exemplar. We take ownership of the exemplar for the duration of the write. - ObservedExemplar exemplar = Interlocked.Exchange(ref _exemplars[i], ObservedExemplar.Empty); + var exemplar = BorrowExemplar(ref _exemplars[i]); cumulativeCount += _bucketCounts[i].Value; await serializer.WriteMetricPointAsync( @@ -117,17 +116,7 @@ await serializer.WriteMetricPointAsync( exemplar, suffix: BucketSuffix); - if (exemplar != ObservedExemplar.Empty) - { - // Return the exemplar unless a new one has arrived, in which case we discard the old one we were holding. - var foundExemplar = Interlocked.CompareExchange(ref _exemplars[i], exemplar, ObservedExemplar.Empty); - - if (foundExemplar != ObservedExemplar.Empty) - { - // A new exemplar had already been written, so we could not return the borrowed one. That's perfectly fine - discard it. - ObservedExemplar.ReturnPooledIfNotEmpty(exemplar); - } - } + ReturnBorrowedExemplar(ref _exemplars[i], exemplar); } } diff --git a/Prometheus/TextSerializer.cs b/Prometheus/TextSerializer.cs index a5913670..6304ed6e 100644 --- a/Prometheus/TextSerializer.cs +++ b/Prometheus/TextSerializer.cs @@ -1,250 +1,257 @@ using System.Globalization; -namespace Prometheus +namespace Prometheus; + +/// +/// Does NOT take ownership of the stream - caller remains the boss. +/// +internal sealed class TextSerializer : IMetricsSerializer { - /// - /// Does NOT take ownership of the stream - caller remains the boss. - /// - internal sealed class TextSerializer : IMetricsSerializer + private static readonly byte[] NewLine = { (byte)'\n' }; + private static readonly byte[] Quote = { (byte)'"' }; + private static readonly byte[] Equal = { (byte)'=' }; + private static readonly byte[] Comma = { (byte)',' }; + private static readonly byte[] Underscore = { (byte)'_' }; + private static readonly byte[] LeftBrace = { (byte)'{' }; + private static readonly byte[] RightBraceSpace = { (byte)'}', (byte)' ' }; + private static readonly byte[] Space = { (byte)' ' }; + private static readonly byte[] SpaceHashSpaceLeftBrace = { (byte)' ', (byte)'#', (byte)' ', (byte)'{' }; + private static readonly byte[] PositiveInfinity = PrometheusConstants.ExportEncoding.GetBytes("+Inf"); + private static readonly byte[] NegativeInfinity = PrometheusConstants.ExportEncoding.GetBytes("-Inf"); + private static readonly byte[] NotANumber = PrometheusConstants.ExportEncoding.GetBytes("NaN"); + private static readonly byte[] PositiveOne = PrometheusConstants.ExportEncoding.GetBytes("1.0"); + private static readonly byte[] Zero = PrometheusConstants.ExportEncoding.GetBytes("0.0"); + private static readonly byte[] NegativeOne = PrometheusConstants.ExportEncoding.GetBytes("-1.0"); + private static readonly byte[] EofNewLine = PrometheusConstants.ExportEncoding.GetBytes("# EOF\n"); + private static readonly byte[] HashHelpSpace = PrometheusConstants.ExportEncoding.GetBytes("# HELP "); + private static readonly byte[] NewlineHashTypeSpace = PrometheusConstants.ExportEncoding.GetBytes("\n# TYPE "); + private static readonly byte[] Unknown = PrometheusConstants.ExportEncoding.GetBytes("unknown"); + + private static readonly char[] DotEChar = { '.', 'e' }; + + public TextSerializer(Stream stream, ExpositionFormat fmt = ExpositionFormat.Text) { - private static readonly byte[] NewLine = { (byte)'\n' }; - private static readonly byte[] Quote = { (byte)'"' }; - private static readonly byte[] Equal = { (byte)'=' }; - private static readonly byte[] Comma = { (byte)',' }; - private static readonly byte[] Underscore = { (byte)'_' }; - private static readonly byte[] LeftBrace = { (byte)'{' }; - private static readonly byte[] RightBraceSpace = { (byte)'}', (byte)' ' }; - private static readonly byte[] Space = { (byte)' ' }; - private static readonly byte[] SpaceHashSpaceLeftBrace = { (byte)' ', (byte)'#', (byte)' ', (byte)'{' }; - private static readonly byte[] PositiveInfinity = PrometheusConstants.ExportEncoding.GetBytes("+Inf"); - private static readonly byte[] NegativeInfinity = PrometheusConstants.ExportEncoding.GetBytes("-Inf"); - private static readonly byte[] NotANumber = PrometheusConstants.ExportEncoding.GetBytes("NaN"); - private static readonly byte[] PositiveOne = PrometheusConstants.ExportEncoding.GetBytes("1.0"); - private static readonly byte[] Zero = PrometheusConstants.ExportEncoding.GetBytes("0.0"); - private static readonly byte[] NegativeOne = PrometheusConstants.ExportEncoding.GetBytes("-1.0"); - private static readonly byte[] EofNewLine = PrometheusConstants.ExportEncoding.GetBytes("# EOF\n"); - private static readonly byte[] HashHelpSpace = PrometheusConstants.ExportEncoding.GetBytes("# HELP "); - private static readonly byte[] NewlineHashTypeSpace = PrometheusConstants.ExportEncoding.GetBytes("\n# TYPE "); - private static readonly byte[] Unknown = PrometheusConstants.ExportEncoding.GetBytes("unknown"); - - private static readonly char[] DotEChar = { '.', 'e' }; - - public TextSerializer(Stream stream, ExpositionFormat fmt = ExpositionFormat.Text) - { - _fmt = fmt; - _stream = new Lazy(() => stream); - } + _fmt = fmt; + _stream = new Lazy(() => stream); + } - // Enables delay-loading of the stream, because touching stream in HTTP handler triggers some behavior. - public TextSerializer(Func streamFactory, - ExpositionFormat fmt = ExpositionFormat.Text) - { - _fmt = fmt; - _stream = new Lazy(streamFactory); - } + // Enables delay-loading of the stream, because touching stream in HTTP handler triggers some behavior. + public TextSerializer(Func streamFactory, + ExpositionFormat fmt = ExpositionFormat.Text) + { + _fmt = fmt; + _stream = new Lazy(streamFactory); + } - public async Task FlushAsync(CancellationToken cancel) - { - // If we never opened the stream, we don't touch it on flush. - if (!_stream.IsValueCreated) - return; + public async Task FlushAsync(CancellationToken cancel) + { + // If we never opened the stream, we don't touch it on flush. + if (!_stream.IsValueCreated) + return; - await _stream.Value.FlushAsync(cancel); - } + await _stream.Value.FlushAsync(cancel); + } - private readonly Lazy _stream; + private readonly Lazy _stream; - public async Task WriteFamilyDeclarationAsync(string name, byte[]nameBytes, byte[] helpBytes, MetricType type, - byte[] typeBytes, CancellationToken cancel) + public async Task WriteFamilyDeclarationAsync(string name, byte[] nameBytes, byte[] helpBytes, MetricType type, + byte[] typeBytes, CancellationToken cancel) + { + var nameLen = nameBytes.Length; + if (_fmt == ExpositionFormat.OpenMetricsText && type == MetricType.Counter) { - var nameLen = nameBytes.Length; - if (_fmt == ExpositionFormat.OpenMetricsText && type == MetricType.Counter) + if (name.EndsWith("_total")) { - if (name.EndsWith("_total")) - { - nameLen -= 6; // in OpenMetrics the counter name does not include the _total prefix. - } - else - { - typeBytes = Unknown; // if the total prefix is missing the _total prefix it is out of spec - } + nameLen -= 6; // in OpenMetrics the counter name does not include the _total prefix. } - - await _stream.Value.WriteAsync(HashHelpSpace, 0, HashHelpSpace.Length, cancel); - await _stream.Value.WriteAsync(nameBytes, 0, nameLen, cancel); - if (helpBytes.Length > 0) + else { - await _stream.Value.WriteAsync(Space, 0, Space.Length, cancel); - await _stream.Value.WriteAsync(helpBytes, 0, helpBytes.Length, cancel); + typeBytes = Unknown; // if the total prefix is missing the _total prefix it is out of spec } - await _stream.Value.WriteAsync(NewlineHashTypeSpace, 0, NewlineHashTypeSpace.Length, cancel); - await _stream.Value.WriteAsync(nameBytes, 0, nameLen, cancel); - await _stream.Value.WriteAsync(Space, 0, Space.Length, cancel); - await _stream.Value.WriteAsync(typeBytes, 0, typeBytes.Length, cancel); - await _stream.Value.WriteAsync(NewLine, 0, NewLine.Length, cancel); } - public async Task WriteEnd(CancellationToken cancel) + await _stream.Value.WriteAsync(HashHelpSpace, 0, HashHelpSpace.Length, cancel); + await _stream.Value.WriteAsync(nameBytes, 0, nameLen, cancel); + if (helpBytes.Length > 0) { - if (_fmt == ExpositionFormat.OpenMetricsText) - await _stream.Value.WriteAsync(EofNewLine, 0, EofNewLine.Length, cancel); + await _stream.Value.WriteAsync(Space, 0, Space.Length, cancel); + await _stream.Value.WriteAsync(helpBytes, 0, helpBytes.Length, cancel); } + await _stream.Value.WriteAsync(NewlineHashTypeSpace, 0, NewlineHashTypeSpace.Length, cancel); + await _stream.Value.WriteAsync(nameBytes, 0, nameLen, cancel); + await _stream.Value.WriteAsync(Space, 0, Space.Length, cancel); + await _stream.Value.WriteAsync(typeBytes, 0, typeBytes.Length, cancel); + await _stream.Value.WriteAsync(NewLine, 0, NewLine.Length, cancel); + } - public async Task WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, CanonicalLabel canonicalLabel, - CancellationToken cancel, double value, ObservedExemplar exemplar, byte[]? suffix = null) + public async Task WriteEnd(CancellationToken cancel) + { + if (_fmt == ExpositionFormat.OpenMetricsText) + await _stream.Value.WriteAsync(EofNewLine, 0, EofNewLine.Length, cancel); + } + + public async Task WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, CanonicalLabel canonicalLabel, + CancellationToken cancel, double value, ObservedExemplar exemplar, byte[]? suffix = null) + { + await WriteIdentifierPartAsync(name, flattenedLabels, cancel, canonicalLabel, exemplar, suffix); + await WriteValuePartAsync(value, exemplar, cancel); + } + + private async Task WriteExemplarAsync(CancellationToken cancel, ObservedExemplar exemplar) + { + await _stream.Value.WriteAsync(SpaceHashSpaceLeftBrace, 0, SpaceHashSpaceLeftBrace.Length, cancel); + for (var i = 0; i < exemplar.Labels!.Length; i++) { - await WriteIdentifierPartAsync(name, flattenedLabels, cancel, canonicalLabel, exemplar, suffix); - await WriteValuePartAsync(value, exemplar, cancel); + if (i > 0) + await _stream.Value.WriteAsync(Comma, 0, Comma.Length, cancel); + await WriteLabel(exemplar.Labels[i].KeyBytes, exemplar.Labels[i].ValueBytes, cancel); } - private async Task WriteExemplarAsync(CancellationToken cancel, ObservedExemplar exemplar) + await _stream.Value.WriteAsync(RightBraceSpace, 0, RightBraceSpace.Length, cancel); + await WriteValue(exemplar.Value, cancel); + await _stream.Value.WriteAsync(Space, 0, Space.Length, cancel); + await WriteValue(exemplar.Timestamp, cancel); + } + + private async Task WriteLabel(byte[] label, byte[] value, CancellationToken cancel) + { + await _stream.Value.WriteAsync(label, 0, label.Length, cancel); + await _stream.Value.WriteAsync(Equal, 0, Equal.Length, cancel); + await _stream.Value.WriteAsync(Quote, 0, Quote.Length, cancel); + await _stream.Value.WriteAsync(value, 0, value.Length, cancel); + await _stream.Value.WriteAsync(Quote, 0, Quote.Length, cancel); + } + + private async Task WriteValue(double value, CancellationToken cancel) + { + if (_fmt == ExpositionFormat.OpenMetricsText) { - await _stream.Value.WriteAsync(SpaceHashSpaceLeftBrace, 0, SpaceHashSpaceLeftBrace.Length, cancel); - for (var i = 0; i < exemplar.Labels!.Length; i++) + switch (value) { - if (i > 0) - await _stream.Value.WriteAsync(Comma, 0, Comma.Length, cancel); - await WriteLabel(exemplar.Labels[i].KeyBytes, exemplar.Labels[i].ValueBytes, cancel); + case 0: + await _stream.Value.WriteAsync(Zero, 0, Zero.Length, cancel); + return; + case 1: + await _stream.Value.WriteAsync(PositiveOne, 0, PositiveOne.Length, cancel); + return; + case -1: + await _stream.Value.WriteAsync(NegativeOne, 0, NegativeOne.Length, cancel); + return; + case double.PositiveInfinity: + await _stream.Value.WriteAsync(PositiveInfinity, 0, PositiveInfinity.Length, cancel); + return; + case double.NegativeInfinity: + await _stream.Value.WriteAsync(NegativeInfinity, 0, NegativeInfinity.Length, cancel); + return; + case double.NaN: + await _stream.Value.WriteAsync(NotANumber, 0, NotANumber.Length, cancel); + return; } - - await _stream.Value.WriteAsync(RightBraceSpace, 0, RightBraceSpace.Length, cancel); - await WriteValue(exemplar.Value, cancel); - await _stream.Value.WriteAsync(Space, 0, Space.Length, cancel); - await WriteValue(exemplar.Timestamp, cancel); } - private async Task WriteLabel(byte[] label, byte[] value, CancellationToken cancel) - { - await _stream.Value.WriteAsync(label, 0, label.Length, cancel); - await _stream.Value.WriteAsync(Equal, 0, Equal.Length, cancel); - await _stream.Value.WriteAsync(Quote, 0, Quote.Length, cancel); - await _stream.Value.WriteAsync(value, 0, value.Length, cancel); - await _stream.Value.WriteAsync(Quote, 0, Quote.Length, cancel); - } + var valueAsString = value.ToString("g", CultureInfo.InvariantCulture); + if (_fmt == ExpositionFormat.OpenMetricsText && valueAsString.IndexOfAny(DotEChar) == -1 /* did not contain .|e */) + valueAsString += ".0"; + var numBytes = PrometheusConstants.ExportEncoding + .GetBytes(valueAsString, 0, valueAsString.Length, _stringBytesBuffer, 0); + await _stream.Value.WriteAsync(_stringBytesBuffer, 0, numBytes, cancel); + } - private async Task WriteValue(double value, CancellationToken cancel) - { - if (_fmt == ExpositionFormat.OpenMetricsText) - { - switch (value) - { - case 0: - await _stream.Value.WriteAsync(Zero, 0, Zero.Length, cancel); - return; - case 1: - await _stream.Value.WriteAsync(PositiveOne, 0, PositiveOne.Length, cancel); - return; - case -1: - await _stream.Value.WriteAsync(NegativeOne, 0, NegativeOne.Length, cancel); - return; - case double.PositiveInfinity: - await _stream.Value.WriteAsync(PositiveInfinity, 0, PositiveInfinity.Length, cancel); - return; - case double.NegativeInfinity: - await _stream.Value.WriteAsync(NegativeInfinity, 0, NegativeInfinity.Length, cancel); - return; - case double.NaN: - await _stream.Value.WriteAsync(NotANumber, 0, NotANumber.Length, cancel); - return; - } - } + // Reuse a buffer to do the UTF-8 encoding. + // Maybe one day also ValueStringBuilder but that would be .NET Core only. + // https://github.com/dotnet/corefx/issues/28379 + // Size limit guided by https://stackoverflow.com/questions/21146544/what-is-the-maximum-length-of-double-tostringd + private readonly byte[] _stringBytesBuffer = new byte[32]; + private readonly ExpositionFormat _fmt; - var valueAsString = value.ToString("g", CultureInfo.InvariantCulture); - if (_fmt == ExpositionFormat.OpenMetricsText && valueAsString.IndexOfAny(DotEChar)==-1 /* did not contain .|e */) - valueAsString += ".0"; - var numBytes = PrometheusConstants.ExportEncoding - .GetBytes(valueAsString, 0, valueAsString.Length, _stringBytesBuffer, 0); - await _stream.Value.WriteAsync(_stringBytesBuffer, 0, numBytes, cancel); + // 123.456 + // Note: Terminates with a NEWLINE + private async Task WriteValuePartAsync(double value, ObservedExemplar exemplar, CancellationToken cancel) + { + await WriteValue(value, cancel); + if (_fmt == ExpositionFormat.OpenMetricsText && exemplar.IsValid) + { + await WriteExemplarAsync(cancel, exemplar); } - // Reuse a buffer to do the UTF-8 encoding. - // Maybe one day also ValueStringBuilder but that would be .NET Core only. - // https://github.com/dotnet/corefx/issues/28379 - // Size limit guided by https://stackoverflow.com/questions/21146544/what-is-the-maximum-length-of-double-tostringd - private readonly byte[] _stringBytesBuffer = new byte[32]; - private readonly ExpositionFormat _fmt; + await _stream.Value.WriteAsync(NewLine, 0, NewLine.Length, cancel); + } - // 123.456 - // Note: Terminates with a NEWLINE - private async Task WriteValuePartAsync(double value, ObservedExemplar exemplar, CancellationToken cancel) - { - await WriteValue(value, cancel); - if (_fmt == ExpositionFormat.OpenMetricsText && exemplar.IsValid) - { - await WriteExemplarAsync(cancel, exemplar); - } - await _stream.Value.WriteAsync(NewLine, 0, NewLine.Length, cancel); + /// + /// Creates a metric identifier, with an optional name postfix and an optional extra label to append to the end. + /// familyname_postfix{labelkey1="labelvalue1",labelkey2="labelvalue2"} + /// Note: Terminates with a SPACE + /// + private async Task WriteIdentifierPartAsync(byte[] name, byte[] flattenedLabels, CancellationToken cancel, + CanonicalLabel canonicalLabel, ObservedExemplar observedExemplar, byte[]? suffix = null) + { + await _stream.Value.WriteAsync(name, 0, name.Length, cancel); + if (suffix != null && suffix.Length > 0) + { + await _stream.Value.WriteAsync(Underscore, 0, Underscore.Length, cancel); + await _stream.Value.WriteAsync(suffix, 0, suffix.Length, cancel); } - - - /// - /// Creates a metric identifier, with an optional name postfix and an optional extra label to append to the end. - /// familyname_postfix{labelkey1="labelvalue1",labelkey2="labelvalue2"} - /// Note: Terminates with a SPACE - /// - private async Task WriteIdentifierPartAsync(byte[] name, byte[] flattenedLabels, CancellationToken cancel, - CanonicalLabel canonicalLabel, ObservedExemplar observedExemplar, byte[]? suffix = null) + + if (flattenedLabels.Length > 0 || canonicalLabel.IsNotEmpty) { - await _stream.Value.WriteAsync(name, 0, name.Length, cancel); - if (suffix != null && suffix.Length > 0) + await _stream.Value.WriteAsync(LeftBrace, 0, LeftBrace.Length, cancel); + if (flattenedLabels.Length > 0) { - await _stream.Value.WriteAsync(Underscore, 0, Underscore.Length, cancel); - await _stream.Value.WriteAsync(suffix, 0, suffix.Length, cancel); + await _stream.Value.WriteAsync(flattenedLabels, 0, flattenedLabels.Length, cancel); } - if (flattenedLabels.Length > 0 || canonicalLabel.IsNotEmpty) + // Extra labels go to the end (i.e. they are deepest to inherit from). + if (canonicalLabel.IsNotEmpty) { - await _stream.Value.WriteAsync(LeftBrace, 0, LeftBrace.Length, cancel); if (flattenedLabels.Length > 0) { - await _stream.Value.WriteAsync(flattenedLabels, 0, flattenedLabels.Length, cancel); - } - - // Extra labels go to the end (i.e. they are deepest to inherit from). - if (canonicalLabel.IsNotEmpty) - { - if (flattenedLabels.Length > 0) - { - await _stream.Value.WriteAsync(Comma, 0, Comma.Length, cancel); - } - - await _stream.Value.WriteAsync(canonicalLabel.Name, 0, canonicalLabel.Name.Length, cancel); - await _stream.Value.WriteAsync(Equal, 0, Equal.Length, cancel); - await _stream.Value.WriteAsync(Quote, 0, Quote.Length, cancel); - if (_fmt == ExpositionFormat.OpenMetricsText) - await _stream.Value.WriteAsync( - canonicalLabel.OpenMetrics, 0, canonicalLabel.OpenMetrics.Length, cancel); - else - await _stream.Value.WriteAsync( - canonicalLabel.Prometheus, 0, canonicalLabel.Prometheus.Length, cancel); - await _stream.Value.WriteAsync(Quote, 0, Quote.Length, cancel); + await _stream.Value.WriteAsync(Comma, 0, Comma.Length, cancel); } - await _stream.Value.WriteAsync(RightBraceSpace, 0, RightBraceSpace.Length, cancel); - } - else - { - await _stream.Value.WriteAsync(Space, 0, Space.Length, cancel); + await _stream.Value.WriteAsync(canonicalLabel.Name, 0, canonicalLabel.Name.Length, cancel); + await _stream.Value.WriteAsync(Equal, 0, Equal.Length, cancel); + await _stream.Value.WriteAsync(Quote, 0, Quote.Length, cancel); + if (_fmt == ExpositionFormat.OpenMetricsText) + await _stream.Value.WriteAsync( + canonicalLabel.OpenMetrics, 0, canonicalLabel.OpenMetrics.Length, cancel); + else + await _stream.Value.WriteAsync( + canonicalLabel.Prometheus, 0, canonicalLabel.Prometheus.Length, cancel); + await _stream.Value.WriteAsync(Quote, 0, Quote.Length, cancel); } - } - /// - /// Encode the special variable in regular Prometheus form and also return a OpenMetrics variant, these can be - /// the same. - /// see: https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#considerations-canonical-numbers - /// - internal static CanonicalLabel EncodeValueAsCanonicalLabel(byte[] name, double value) + await _stream.Value.WriteAsync(RightBraceSpace, 0, RightBraceSpace.Length, cancel); + } + else { - if (double.IsPositiveInfinity(value)) - return new CanonicalLabel(name, PositiveInfinity, PositiveInfinity); + await _stream.Value.WriteAsync(Space, 0, Space.Length, cancel); + } + } - var valueAsString = value.ToString("g", CultureInfo.InvariantCulture); - var prom = PrometheusConstants.ExportEncoding.GetBytes(valueAsString); + /// + /// Encode the special variable in regular Prometheus form and also return a OpenMetrics variant, these can be + /// the same. + /// see: https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#considerations-canonical-numbers + /// + internal static CanonicalLabel EncodeValueAsCanonicalLabel(byte[] name, double value) + { + if (double.IsPositiveInfinity(value)) + return new CanonicalLabel(name, PositiveInfinity, PositiveInfinity); - return new CanonicalLabel(name, prom, valueAsString.IndexOfAny(DotEChar) != -1 // contained .|e - ? prom - : PrometheusConstants.ExportEncoding.GetBytes(valueAsString + ".0")); + var valueAsString = value.ToString("g", CultureInfo.InvariantCulture); + var prometheusBytes = PrometheusConstants.ExportEncoding.GetBytes(valueAsString); + + var openMetricsBytes = prometheusBytes; + + // Identify whether the original value is floating-point, by checking for presence of the 'e' or '.' characters. + if (valueAsString.IndexOfAny(DotEChar) == -1) + { + // OpenMetrics requires labels containing numeric values to be expressed in floating point format. + // If all we find is an integer, we add a ".0" to the end to make it a floating point value. + openMetricsBytes = PrometheusConstants.ExportEncoding.GetBytes(valueAsString + ".0"); } + + return new CanonicalLabel(name, prometheusBytes, openMetricsBytes); } } \ No newline at end of file From 5786e0049777914545ff499a8f913f5774d5cdf8 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 29 Dec 2022 15:23:02 +0200 Subject: [PATCH 026/230] History clarification --- History | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/History b/History index 6837b191..799a701a 100644 --- a/History +++ b/History @@ -1,6 +1,6 @@ * 8.0.0 - Added OpenMetrics exposition format support (#388). -- Added exemplar support (#388). +- Added exemplar support for Counter and Histogram (#388). - The ':' character is no longer allowed in metric or label names. For metric names, Prometheus standard practice is to use colon only in recording rules. * 7.1.0 - Added back .NET Standard 2.0 support as some customers had a hard dependency on .NET Standard 2.0 (despite not being a documented feature even earlier). From b691fa645abfe59710714ffd0d3f2e87c7065aa7 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 29 Dec 2022 15:29:22 +0200 Subject: [PATCH 027/230] Emit default exemplar with trace_id and span_id --- Prometheus/ChildBase.cs | 32 +++++++++++++++++++++++++++++ Prometheus/Counter.cs | 2 ++ Prometheus/Histogram.cs | 2 ++ README.md | 24 ++++++++++++++++++++++ Sample.Console.Exemplars/Program.cs | 17 +++++++++++++-- 5 files changed, 75 insertions(+), 2 deletions(-) diff --git a/Prometheus/ChildBase.cs b/Prometheus/ChildBase.cs index 7b1332e9..7fcf7b1f 100644 --- a/Prometheus/ChildBase.cs +++ b/Prometheus/ChildBase.cs @@ -1,3 +1,7 @@ +#if NET6_0_OR_GREATER +using System.Diagnostics; +#endif + namespace Prometheus; /// @@ -111,4 +115,32 @@ internal void ReturnBorrowedExemplar(ref ObservedExemplar storage, ObservedExemp ObservedExemplar.ReturnPooledIfNotEmpty(borrowed); } } + + // Based on https://opentelemetry.io/docs/reference/specification/compatibility/prometheus_and_openmetrics/ + private static readonly Exemplar.LabelKey TraceIdKey = Exemplar.Key("trace_id"); + private static readonly Exemplar.LabelKey SpanIdKey = Exemplar.Key("span_id"); + + /// + /// Returns either the provided exemplar (if any) or the default exemplar. + /// The default exemplar consists of "trace_id" and "span_id" from the current trace context (.NET Core only). + /// + protected internal Exemplar.LabelPair[] ExemplarOrDefault(Exemplar.LabelPair[] exemplar) + { + // A custom exemplar was provided - just use it. + if (exemplar is { Length: > 0 }) { return exemplar; } + +#if NET6_0_OR_GREATER + var activity = Activity.Current; + if (activity != null) + { + // Based on https://opentelemetry.io/docs/reference/specification/compatibility/prometheus_and_openmetrics/ + var traceIdLabel = TraceIdKey.WithValue(activity.TraceId.ToString()); + var spanIdLabel = SpanIdKey.WithValue(activity.SpanId.ToString()); + + return new[] { traceIdLabel, spanIdLabel }; + } +#endif + + return Array.Empty(); + } } \ No newline at end of file diff --git a/Prometheus/Counter.cs b/Prometheus/Counter.cs index 40ad4f51..7dda1268 100644 --- a/Prometheus/Counter.cs +++ b/Prometheus/Counter.cs @@ -37,6 +37,8 @@ public void Inc(double increment = 1.0, params Exemplar.LabelPair[] exemplarLabe if (increment < 0.0) throw new ArgumentOutOfRangeException(nameof(increment), "Counter value cannot decrease."); + exemplarLabels = ExemplarOrDefault(exemplarLabels); + if (exemplarLabels is { Length: > 0 }) { var exemplar = ObservedExemplar.CreatePooled(exemplarLabels, increment); diff --git a/Prometheus/Histogram.cs b/Prometheus/Histogram.cs index 0e2cf4e1..472e9782 100644 --- a/Prometheus/Histogram.cs +++ b/Prometheus/Histogram.cs @@ -136,6 +136,8 @@ private void ObserveInternal(double val, long count, params Exemplar.LabelPair[] return; } + exemplarLabels = ExemplarOrDefault(exemplarLabels); + for (int i = 0; i < _upperBounds.Length; i++) { if (val <= _upperBounds[i]) diff --git a/README.md b/README.md index cbb8f20a..9cf5e1bc 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ The library targets the following runtimes (and newer): * [Counting exceptions](#counting-exceptions) * [Labels](#labels) * [Static labels](#static-labels) +* [Exemplars](#exemplars) * [When are metrics published?](#when-are-metrics-published) * [Unpublishing metrics](#unpublishing-metrics) * [ASP.NET Core exporter middleware](#aspnet-core-exporter-middleware) @@ -319,6 +320,29 @@ requestsHandled.WithLabels("404").Inc(); requestsHandled.WithLabels("200").Inc(); ``` +# Exemplars + +Exemplars facilitate [distributed tracing](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/distributed-tracing-concepts), by attaching related trace IDs to metrics. This enables a metrics GUI to cross-references [traces](https://opentelemetry.io/docs/concepts/signals/traces/) that explain how the metric got the value it has. + +By default, prometheus-net will create an exemplar with the `trace_id` and `span_id` labels from the current .NET distributed tracing context (`Activity.Current`). To override this, provide your own exemplar when updating the value of the metric: + +```csharp +private static readonly Counter RecordsProcessed = Metrics.CreateCounter("sample_records_processed_total", "Total number of records processed."); + +... + +// The key from an exemplar key-value pair should be created once and reused to minimize memory allocations. +var recordIdKey = Exemplar.Key("record_id"); + +foreach (var record in recordsToProcess) +{ + var recordIdKeyValuePair = recordIdKey.WithValue(record.Id.ToString()); + RecordsProcessed.Inc(recordIdKeyValuePair); +} +``` + +See also, [Sample.Console.Exemplars](Sample.Console.Exemplars/Program.cs). + # When are metrics published? Metrics without labels are published immediately after the `Metrics.CreateX()` call. Metrics that use labels are published when you provide the label values for the first time. diff --git a/Sample.Console.Exemplars/Program.cs b/Sample.Console.Exemplars/Program.cs index 72049dc5..46fc2c59 100644 --- a/Sample.Console.Exemplars/Program.cs +++ b/Sample.Console.Exemplars/Program.cs @@ -1,4 +1,5 @@ using Prometheus; +using System.Diagnostics; // This sample demonstrates how to attach exemplars to metrics exposed by a .NET console app. // @@ -24,6 +25,8 @@ Buckets = Histogram.PowersOfTenDividedBuckets(0, 2, 10) }); +var totalSleepTime = Metrics.CreateCounter("sample_sleep_seconds_total", "Total amount of time spent sleeping."); + // The key from an exemplar key-value pair should be created once and reused to minimize memory allocations. var recordIdKey = Exemplar.Key("record_id"); @@ -42,12 +45,22 @@ recordsProcessed.Inc(recordIdKeyValuePair); recordSizeInPages.Observe(recordPageCount, recordIdKeyValuePair); - await Task.Delay(TimeSpan.FromSeconds(1)); + // The activity is often automatically inherited from incoming HTTP requests if using OpenTelemetry tracing in ASP.NET Core. + // Here, we manually create and start an activity for sample purposes, without relying on the platform managing the activity context. + // See https://learn.microsoft.com/en-us/dotnet/core/diagnostics/distributed-tracing-concepts + using (var activity = new Activity("Taking a break from record processing").Start()) + { + var sleepStopwatch = Stopwatch.StartNew(); + await Task.Delay(TimeSpan.FromSeconds(1)); + + // If you do not specify an exemplar yourself, the trace_id and span_id from the current Activity are automatically used. + totalSleepTime.Inc(sleepStopwatch.Elapsed.TotalSeconds); + } } }); // Metrics published in this sample: -// * the custom sample counter defined above, with exemplars +// * the custom sample metrics defined above, with exemplars // * internal debug metrics from prometheus-net, without exemplars Console.WriteLine("Open http://localhost:1234/metrics in a web browser."); Console.WriteLine("Press enter to exit."); From 6a9ab6dc4cdd7d6a1d3bf5c3c93c8ddd7988f0f5 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 29 Dec 2022 15:47:27 +0200 Subject: [PATCH 028/230] Allow "Accept" header to be overridden via query string, for ease of development --- .../MetricServerMiddleware.cs | 9 +++++-- Prometheus/PrometheusConstants.cs | 25 +++++++++---------- README.md | 20 ++++++++------- Sample.Console.Exemplars/Program.cs | 3 ++- 4 files changed, 32 insertions(+), 25 deletions(-) diff --git a/Prometheus.AspNetCore/MetricServerMiddleware.cs b/Prometheus.AspNetCore/MetricServerMiddleware.cs index bed4101b..fd210796 100644 --- a/Prometheus.AspNetCore/MetricServerMiddleware.cs +++ b/Prometheus.AspNetCore/MetricServerMiddleware.cs @@ -60,9 +60,14 @@ private IEnumerable ExtractAcceptableMediaTypes private ProtocolNegotiationResult NegotiateComminucationProtocol(HttpRequest request) { - var acceptHeader = request.Headers.Accept.ToString(); + var acceptHeaderValues = request.Headers.Accept.ToString(); - foreach (var candidate in ExtractAcceptableMediaTypes(acceptHeader) + // We allow the "Accept" HTTP header to be overridden by the "accept" query string parameter. + // This is mainly for development purposes (to make it easier to request OpenMetrics format via browser URL bar). + if (request.Query.TryGetValue("accept", out var acceptValuesFromQuery)) + acceptHeaderValues = string.Join(",", acceptValuesFromQuery); + + foreach (var candidate in ExtractAcceptableMediaTypes(acceptHeaderValues) .OrderByDescending(mt => mt.Quality.GetValueOrDefault(1))) { if (candidate.MediaType == PrometheusConstants.TextContentType) diff --git a/Prometheus/PrometheusConstants.cs b/Prometheus/PrometheusConstants.cs index 11f65a4e..6df93505 100644 --- a/Prometheus/PrometheusConstants.cs +++ b/Prometheus/PrometheusConstants.cs @@ -1,21 +1,20 @@ using System.Net.Http.Headers; using System.Text; -namespace Prometheus +namespace Prometheus; + +public static class PrometheusConstants { - public static class PrometheusConstants - { - public const string TextContentType = "text/plain"; - public const string OpenMetricsContentType = "application/openmetrics-text"; + public const string TextContentType = "text/plain"; + public const string OpenMetricsContentType = "application/openmetrics-text"; - public const string TextContentTypeWithVersionAndEncoding = TextContentType + "; version=0.0.4; charset=utf-8"; - public const string OpenMetricsContentTypeWithVersionAndEncoding = OpenMetricsContentType + "; version=1.0.0; charset=utf-8"; + public const string TextContentTypeWithVersionAndEncoding = TextContentType + "; version=0.0.4; charset=utf-8"; + public const string OpenMetricsContentTypeWithVersionAndEncoding = OpenMetricsContentType + "; version=1.0.0; charset=utf-8"; - // ASP.NET requires a MediaTypeHeaderValue object - public static readonly MediaTypeHeaderValue ExporterContentTypeValue = MediaTypeHeaderValue.Parse(TextContentTypeWithVersionAndEncoding); - public static readonly MediaTypeHeaderValue ExporterOpenMetricsContentTypeValue = MediaTypeHeaderValue.Parse(OpenMetricsContentTypeWithVersionAndEncoding); + // ASP.NET requires a MediaTypeHeaderValue object + public static readonly MediaTypeHeaderValue ExporterContentTypeValue = MediaTypeHeaderValue.Parse(TextContentTypeWithVersionAndEncoding); + public static readonly MediaTypeHeaderValue ExporterOpenMetricsContentTypeValue = MediaTypeHeaderValue.Parse(OpenMetricsContentTypeWithVersionAndEncoding); - // Use UTF-8 encoding, but provide the flag to ensure the Unicode Byte Order Mark is never prepended to the output stream. - public static readonly Encoding ExportEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); - } + // Use UTF-8 encoding, but provide the flag to ensure the Unicode Byte Order Mark is never prepended to the output stream. + public static readonly Encoding ExportEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); } \ No newline at end of file diff --git a/README.md b/README.md index 9cf5e1bc..fdadc148 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ The library targets the following runtimes (and newer): * [Static labels](#static-labels) * [Exemplars](#exemplars) * [When are metrics published?](#when-are-metrics-published) -* [Unpublishing metrics](#unpublishing-metrics) +* [Deleting metrics](#deleting-metrics) * [ASP.NET Core exporter middleware](#aspnet-core-exporter-middleware) * [ASP.NET Core HTTP request metrics](#aspnet-core-http-request-metrics) * [ASP.NET Core gRPC request metrics](#aspnet-core-grpc-request-metrics) @@ -93,7 +93,7 @@ Refer to the sample projects for quick start instructions: | [Sample.Grpc.Client](Sample.Grpc.Client/Program.cs) | Client app for the above | | [Sample.NetStandard](Sample.NetStandard/ImportantProcess.cs) | Demonstrates how to reference prometheus-net in a .NET Standard class library | | [Sample.Web.DifferentPort](Sample.Web.DifferentPort/Program.cs) | Demonstrates how to set up the metric exporter on a different port from the main web API (e.g. for security purposes) | -| [Sample.Web.MetricExpiration](Sample.Web.MetricExpiration/Program.cs) | Demonstrates how to use [automatic metric unpublishing](#unpublishing-metrics) | +| [Sample.Web.MetricExpiration](Sample.Web.MetricExpiration/Program.cs) | Demonstrates how to use [automatic metric deletion](#deleting-metrics) | | [Sample.Web.NetFramework](Sample.Web.NetFramework/Global.asax.cs) | .NET Framework web app that publishes custom metrics | The rest of this document describes how to use individual features of the library. @@ -341,6 +341,8 @@ foreach (var record in recordsToProcess) } ``` +Exemplars are only present if the metrics are being scraped by an OpenMetrics-capable client. For development purposes, you can force the library to use the OpenMetrics exposition format by adding `?accept=application/openmetrics-text` to the `/metrics` URL. The Prometheus database automatically negotiates OpenMetrics support when scraping metrics - you do not need to take any special action in production scenarios. + See also, [Sample.Console.Exemplars](Sample.Console.Exemplars/Program.cs). # When are metrics published? @@ -363,13 +365,13 @@ private static readonly Gauge UsersLoggedIn = Metrics UsersLoggedIn.Set(LoadSessions().Count); ``` -You can also use `.Publish()` on a metric to mark it as ready to be published without modifying the initial value (e.g. to publish a zero). +You can also use `.Publish()` on a metric to mark it as ready to be published without modifying the initial value (e.g. to publish a zero). Conversely, you can use `.Unpublish()` to hide a metric temporarily. Note that the metric remains in memory and retains its value. -# Unpublishing metrics +# Deleting metrics -You can use `.Dispose()` or `.RemoveLabelled()` methods on the metric classes to manually unpublish metrics at any time. +You can use `.Dispose()` or `.RemoveLabelled()` methods on the metric classes to manually delete metrics at any time. -In some situations, it can be hard to determine when a metric with a specific set of labels becomes irrelevant and needs to be unpublished. The library provides some assistance here by enabling automatic expiration of metrics when they are no longer used. +In some situations, it can be hard to determine when a metric with a specific set of labels becomes irrelevant and needs to be removed. The library provides some assistance here by enabling automatic expiration of metrics when they are no longer used. To enable automatic expiration, create the metrics via the metric factory returned by `Metrics.WithManagedLifetime()`. All such metrics will have a fixed expiration time, with the expiration restarting based on certain conditions that indicate the metric is in use. @@ -381,7 +383,7 @@ var factory = Metrics.WithManagedLifetime(expiresAfter: TimeSpan.FromMinutes(5)) // With expiring metrics, we get back handles to the metric, not the metric directly. var inProgressHandle = expiringMetricFactory .CreateGauge("documents_in_progress", "Number of documents currently being processed.", - // Automatic unpublishing only makes sense if we have a high/unknown cardinality label set, + // Automatic metric deletion only makes sense if we have a high/unknown cardinality label set, // so here is a sample label for each "document provider", whoever that may be. labelNames: new[] { "document_provider" }); @@ -389,7 +391,7 @@ var inProgressHandle = expiringMetricFactory public void ProcessDocument(string documentProvider) { - // Automatic unpublishing will not occur while this lease is held. + // Automatic metric deletion will not occur while this lease is held. // This will also reset any existing expiration timer for this document provider. inProgressHandle.WithLease(metric => { @@ -409,7 +411,7 @@ var factory = Metrics.WithManagedLifetime(expiresAfter: TimeSpan.FromMinutes(5)) // With expiring metrics, we get back handles to the metric, not the metric directly. var processingStartedHandle = expiringMetricFactory .CreateGauge("documents_started_processing_total", "Number of documents for which processing has started.", - // Automatic unpublishing only makes sense if we have a high/unknown cardinality label set, + // Automatic metric deletion only makes sense if we have a high/unknown cardinality label set, // so here is a sample label for each "document provider", whoever that may be. labelNames: new[] { "document_provider" }); diff --git a/Sample.Console.Exemplars/Program.cs b/Sample.Console.Exemplars/Program.cs index 46fc2c59..f544f922 100644 --- a/Sample.Console.Exemplars/Program.cs +++ b/Sample.Console.Exemplars/Program.cs @@ -62,6 +62,7 @@ // Metrics published in this sample: // * the custom sample metrics defined above, with exemplars // * internal debug metrics from prometheus-net, without exemplars -Console.WriteLine("Open http://localhost:1234/metrics in a web browser."); +// Note that the OpenMetrics exposition format must be selected via HTTP header or query string parameter to see exemplars. +Console.WriteLine("Open http://localhost:1234/metrics?accept=application/openmetrics-text in a web browser."); Console.WriteLine("Press enter to exit."); Console.ReadLine(); \ No newline at end of file From a0534a101b469b16e0b6f160d4fdbdab3e99ecea Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 29 Dec 2022 15:50:30 +0200 Subject: [PATCH 029/230] Clarify distinction between unpublish and remove/delete/dispose --- Prometheus/Collector.cs | 4 ++-- Prometheus/IManagedLifetimeMetricFactory.cs | 4 ++-- Prometheus/IManagedLifetimeMetricHandle.cs | 2 +- Prometheus/IMetricFactory.cs | 2 +- Prometheus/ManagedLifetimeMetricHandle.cs | 6 +++--- Prometheus/MeterAdapterOptions.cs | 2 +- Prometheus/Metrics.cs | 2 +- Sample.Web.MetricExpiration/Program.cs | 4 ++-- 8 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Prometheus/Collector.cs b/Prometheus/Collector.cs index 4df5ab98..45b4dc88 100644 --- a/Prometheus/Collector.cs +++ b/Prometheus/Collector.cs @@ -125,7 +125,7 @@ public abstract class Collector : Collector, ICollector private readonly ConcurrentDictionary _labelledMetrics = new(); // Lazy-initialized since not every collector will use a child with no labels. - // Lazy instance will be replaced if the unlabelled timeseries is unpublished. + // Lazy instance will be replaced if the unlabelled timeseries is removed. private Lazy _unlabelledLazy; /// @@ -167,7 +167,7 @@ internal override void RemoveLabelled(LabelSequence labels) if (labels.Length == 0) { - // If we remove the unlabeled instance (technically legitimate, to unpublish it) then + // If we remove the unlabeled instance (technically legitimate, if the caller really desires to do so) then // we need to also ensure that the special-casing used for it gets properly wired up the next time. _unlabelledLazy = GetUnlabelledLazyInitializer(); } diff --git a/Prometheus/IManagedLifetimeMetricFactory.cs b/Prometheus/IManagedLifetimeMetricFactory.cs index 2c856f17..7b554166 100644 --- a/Prometheus/IManagedLifetimeMetricFactory.cs +++ b/Prometheus/IManagedLifetimeMetricFactory.cs @@ -2,11 +2,11 @@ { /// /// A metric factory for creating metrics that use a managed lifetime, whereby the metric may - /// be unpublished based on logic other than disposal or similar explicit unpublishing. + /// be deleted based on logic other than disposal or similar explicit deletion. /// /// /// The lifetime management logic is associated with a metric handle. Calling CreateXyz() with equivalent identity parameters will return - /// the same handle. However, using multiple factories will create independent handles (which will unpublish the same metric independently). + /// the same handle. However, using multiple factories will create independent handles (which will delete the same metric independently). /// public interface IManagedLifetimeMetricFactory { diff --git a/Prometheus/IManagedLifetimeMetricHandle.cs b/Prometheus/IManagedLifetimeMetricHandle.cs index ddf254be..ebeccff7 100644 --- a/Prometheus/IManagedLifetimeMetricHandle.cs +++ b/Prometheus/IManagedLifetimeMetricHandle.cs @@ -2,7 +2,7 @@ /// /// Handle to a metric with a lease-extended lifetime, enabling the metric to be accessed and its lifetime to be controlled. -/// Each label combination is automatically unpublished N seconds after the last lease on that label combination expires. +/// Each label combination is automatically deleted N seconds after the last lease on that label combination expires. /// public interface IManagedLifetimeMetricHandle where TMetricInterface : ICollectorChild diff --git a/Prometheus/IMetricFactory.cs b/Prometheus/IMetricFactory.cs index 691e7d27..f2318318 100644 --- a/Prometheus/IMetricFactory.cs +++ b/Prometheus/IMetricFactory.cs @@ -35,7 +35,7 @@ public interface IMetricFactory /// Returns a factory that creates metrics with a managed lifetime. /// /// - /// Metrics created from this factory will expire after this time span elapses, enabling automatic unpublishing of unused metrics. + /// Metrics created from this factory will expire after this time span elapses, enabling automatic deletion of unused metrics. /// The expiration timer is reset to zero for the duration of any active lifetime-extension lease that is taken on a specific metric. /// IManagedLifetimeMetricFactory WithManagedLifetime(TimeSpan expiresAfter); diff --git a/Prometheus/ManagedLifetimeMetricHandle.cs b/Prometheus/ManagedLifetimeMetricHandle.cs index 53d71563..7b520c1f 100644 --- a/Prometheus/ManagedLifetimeMetricHandle.cs +++ b/Prometheus/ManagedLifetimeMetricHandle.cs @@ -246,7 +246,7 @@ public void Dispose() /// - ConcurrentDictionary will throw away an optimistically created duplicate. /// * Creating a new instance takes a reader lock to allow allocation to be blocked by removal logic. /// * Removal will take a writer lock to prevent concurrent allocataions (which also implies preventing concurrent new leases that might "renew" a lifetime). - /// - It can be that between "unpublishing needed" event and write lock being taken, the state of the lifetime manager changes because of + /// - It can be that between "deletion needed" event and write lock being taken, the state of the lifetime manager changes because of /// actions done by holders of the read lock (e.g. new lease added). For code simplicity, we accept this as a gap where we may lose data (such a lease fails to renew/start a lifetime). /// private readonly ConcurrentDictionary _lifetimeManagers = new(); @@ -300,13 +300,13 @@ private LifetimeManager GetOrAddLifetimeManagerCore(TChild child) private LifetimeManager CreateLifetimeManager(TChild child) { - return new LifetimeManager(child, _expiresAfter, Delayer, UnpublishOuter); + return new LifetimeManager(child, _expiresAfter, Delayer, DeleteMetricOuter); } /// /// Performs the locking necessary to ensure that a LifetimeManager that ends the lifetime does not get reused. /// - private void UnpublishOuter(TChild child) + private void DeleteMetricOuter(TChild child) { _lifetimeManagersLock.EnterWriteLock(); diff --git a/Prometheus/MeterAdapterOptions.cs b/Prometheus/MeterAdapterOptions.cs index e27da58f..372f097b 100644 --- a/Prometheus/MeterAdapterOptions.cs +++ b/Prometheus/MeterAdapterOptions.cs @@ -17,7 +17,7 @@ public sealed class MeterAdapterOptions /// /// The .NET Meters API does not tell us (or even know itself) when a metric with a certain label combination is no longer going to receive new data. - /// To avoid building an ever-increasing store of in-memory metrics states, we unpublish metrics once they have not been updated in a while. + /// To avoid building an ever-increasing store of in-memory metrics states, we delete metrics once they have not been updated in a while. /// The idea being that metrics are useful when they are changing regularly - if a value stays the same for N minutes, it probably is not a valuable data point anymore. /// public TimeSpan MetricsExpireAfter { get; set; } = TimeSpan.FromMinutes(5); diff --git a/Prometheus/Metrics.cs b/Prometheus/Metrics.cs index 09571688..438e6e01 100644 --- a/Prometheus/Metrics.cs +++ b/Prometheus/Metrics.cs @@ -37,7 +37,7 @@ public static IMetricFactory WithLabels(IDictionary labels) => /// Returns a factory that creates metrics with a managed lifetime. /// /// - /// Metrics created from this factory will expire after this time span elapses, enabling automatic unpublishing of unused metrics. + /// Metrics created from this factory will expire after this time span elapses, enabling automatic deletion of unused metrics. /// The expiration timer is reset to zero for the duration of any active lifetime-extension lease that is taken on a specific metric. /// public static IManagedLifetimeMetricFactory WithManagedLifetime(TimeSpan expiresAfter) => diff --git a/Sample.Web.MetricExpiration/Program.cs b/Sample.Web.MetricExpiration/Program.cs index fd31256f..c8459e53 100644 --- a/Sample.Web.MetricExpiration/Program.cs +++ b/Sample.Web.MetricExpiration/Program.cs @@ -24,7 +24,7 @@ app.UseRouting(); -// Use an auto-expiring variant for all the demo metrics here - they get automatically unpublished if not used in the last 60 seconds. +// Use an auto-expiring variant for all the demo metrics here - they get automatically deleted if not used in the last 60 seconds. var expiringMetricFactory = Metrics.WithManagedLifetime(expiresAfter: TimeSpan.FromSeconds(60)); // OPTION 1: metric lifetime can be managed by leases, to ensure they do not go away during potentially @@ -33,7 +33,7 @@ { var inProgress = expiringMetricFactory.CreateGauge("long_running_operations_in_progress", "Number of long running operations in progress.", labelNames: new[] { "operation_type" }); - // The metric will not be unpublished as long as this lease is kept. + // The metric will not be deleted as long as this lease is kept. await inProgress.WithLeaseAsync(async inProgressInstance => { // Long-running operation, which we track via the "in progress" gauge. From 14a300c1c0e2ea338ccca90ad23fdc3d3f3afb44 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 29 Dec 2022 15:59:33 +0200 Subject: [PATCH 030/230] 8.0.0 --- Resources/SolutionAssemblyInfo.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Resources/SolutionAssemblyInfo.cs b/Resources/SolutionAssemblyInfo.cs index e5a3f103..e6e5e379 100644 --- a/Resources/SolutionAssemblyInfo.cs +++ b/Resources/SolutionAssemblyInfo.cs @@ -2,10 +2,10 @@ using System.Runtime.CompilerServices; // This is the real version number, used in NuGet packages and for display purposes. -[assembly: AssemblyFileVersion("7.1.0")] +[assembly: AssemblyFileVersion("8.0.0")] // Only use major version here, with others kept at zero, for correct assembly binding logic. -[assembly: AssemblyVersion("7.0.0")] +[assembly: AssemblyVersion("8.0.0")] [assembly: InternalsVisibleTo("Tests.NetFramework, PublicKey=002400000480000014010000060200000024000052534131000800000100010049b30b6bccc8311c8d5f9c006a5968b0592eca8b5a228e9e0a2ac0292e2a162ea3314b0f9941ffad9fe40a4071de2a0b6e4f50b70292d26081054f96df6a05e5a89a71538d50decaf8322f0cdd008e8e14d5e227b46c8c10a6cc850a5d7febf9ad5e0ffb8371e840744d3dd0cb88012ee61490a09d007fab29fc13fb0b4c2fb4d72692232546712b3e9e25a201e309bec907a9a241059d26f1826a337faf6e7a16902fc35e8dafeceff35a48622a9716af86138a1a064c879b7239a9495b8416abf63f8763a613e5be2e6b13403eb952c36008a281502bc2c89ca3367624b0791712f50674760fcbab2e7795fb6c53b0675f940d152ef449ad10463bce59a7d5")] [assembly: InternalsVisibleTo("Tests.NetCore, PublicKey=002400000480000014010000060200000024000052534131000800000100010049b30b6bccc8311c8d5f9c006a5968b0592eca8b5a228e9e0a2ac0292e2a162ea3314b0f9941ffad9fe40a4071de2a0b6e4f50b70292d26081054f96df6a05e5a89a71538d50decaf8322f0cdd008e8e14d5e227b46c8c10a6cc850a5d7febf9ad5e0ffb8371e840744d3dd0cb88012ee61490a09d007fab29fc13fb0b4c2fb4d72692232546712b3e9e25a201e309bec907a9a241059d26f1826a337faf6e7a16902fc35e8dafeceff35a48622a9716af86138a1a064c879b7239a9495b8416abf63f8763a613e5be2e6b13403eb952c36008a281502bc2c89ca3367624b0791712f50674760fcbab2e7795fb6c53b0675f940d152ef449ad10463bce59a7d5")] From c2e8d4579f2e93f904fcf1007577b325cbbb713f Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 29 Dec 2022 16:06:10 +0200 Subject: [PATCH 031/230] Add warning about exemplar size limit --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index fdadc148..ed2529f2 100644 --- a/README.md +++ b/README.md @@ -343,6 +343,9 @@ foreach (var record in recordsToProcess) Exemplars are only present if the metrics are being scraped by an OpenMetrics-capable client. For development purposes, you can force the library to use the OpenMetrics exposition format by adding `?accept=application/openmetrics-text` to the `/metrics` URL. The Prometheus database automatically negotiates OpenMetrics support when scraping metrics - you do not need to take any special action in production scenarios. +> **Warning** +> Exemplars are limited to 128 bytes - they are meant to contain IDs for cross-referencing with trace databases, not as a replacement for trace databases. + See also, [Sample.Console.Exemplars](Sample.Console.Exemplars/Program.cs). # When are metrics published? From 4d6812e3ed25bf31f70b860aa344289225cfa4bb Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 29 Dec 2022 16:07:36 +0200 Subject: [PATCH 032/230] Better code in example --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ed2529f2..a1682e4d 100644 --- a/README.md +++ b/README.md @@ -327,16 +327,16 @@ Exemplars facilitate [distributed tracing](https://learn.microsoft.com/en-us/dot By default, prometheus-net will create an exemplar with the `trace_id` and `span_id` labels from the current .NET distributed tracing context (`Activity.Current`). To override this, provide your own exemplar when updating the value of the metric: ```csharp -private static readonly Counter RecordsProcessed = Metrics.CreateCounter("sample_records_processed_total", "Total number of records processed."); - -... +private static readonly Counter RecordsProcessed = Metrics + .CreateCounter("sample_records_processed_total", "Total number of records processed."); // The key from an exemplar key-value pair should be created once and reused to minimize memory allocations. -var recordIdKey = Exemplar.Key("record_id"); +private static readonly Exemplar.LabelKey RecordIdKey = Exemplar.Key("record_id"); +... foreach (var record in recordsToProcess) { - var recordIdKeyValuePair = recordIdKey.WithValue(record.Id.ToString()); + var recordIdKeyValuePair = RecordIdKey.WithValue(record.Id.ToString()); RecordsProcessed.Inc(recordIdKeyValuePair); } ``` From 8a0988e9dbdd7c918c795bb0f9d169757070cb07 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 29 Dec 2022 22:35:51 +0200 Subject: [PATCH 033/230] Docs fix --- Prometheus/EventCounterAdapter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Prometheus/EventCounterAdapter.cs b/Prometheus/EventCounterAdapter.cs index b734e16c..8636e383 100644 --- a/Prometheus/EventCounterAdapter.cs +++ b/Prometheus/EventCounterAdapter.cs @@ -44,7 +44,7 @@ public void Dispose() private readonly Listener _listener; - // We never decrease it in the current implementation but perhaps might in a future implementation, so might as well make it a counter. + // We never decrease it in the current implementation but perhaps might in a future implementation, so might as well make it a gauge. private readonly Gauge _eventSourcesConnected; private bool OnEventSourceCreated(EventSource source) From 0492766260cf08784dd6d6a381c8da7088fb8ccb Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 29 Dec 2022 22:36:41 +0200 Subject: [PATCH 034/230] Docs fix --- Prometheus/MeterAdapter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Prometheus/MeterAdapter.cs b/Prometheus/MeterAdapter.cs index 87dc960a..2e3e776d 100644 --- a/Prometheus/MeterAdapter.cs +++ b/Prometheus/MeterAdapter.cs @@ -162,7 +162,7 @@ or ObservableUpDownCounter { var handle = _factory.CreateHistogram(_instrumentPrometheusNames[instrument], _instrumentPrometheusHelp[instrument], labelNames, new HistogramConfiguration { - // We oursource the bucket definition to the callback in options, as it might need to be different for different instruments. + // We outsource the bucket definition to the callback in options, as it might need to be different for different instruments. Buckets = _options.ResolveHistogramBuckets(instrument) }); From 9526fbf0e55487341997918f7786a127950cb5c2 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Fri, 30 Dec 2022 11:06:09 +0200 Subject: [PATCH 035/230] Fix defect where metric instances with different static label values for the same static label keys were coalesced into the same instance. We now create separate instances for separate static label values, still serialized as a single family based on label keys. Fixes #389 --- Benchmark.NetCore/SummaryBenchmarks.cs | 2 +- Prometheus/Collector.cs | 8 ++- Prometheus/CollectorFamily.cs | 27 ++++++++ Prometheus/CollectorFamilyIdentity.cs | 70 +++++++++++++++++++++ Prometheus/CollectorIdentity.cs | 69 --------------------- Prometheus/CollectorRegistry.cs | 72 ++++++++++++++-------- Prometheus/ManagedLifetimeMetricFactory.cs | 24 ++++---- Tests.NetCore/FactoryLabelTests.cs | 32 ++++++++++ Tests.NetCore/HistogramTests.cs | 2 +- 9 files changed, 196 insertions(+), 110 deletions(-) create mode 100644 Prometheus/CollectorFamily.cs create mode 100644 Prometheus/CollectorFamilyIdentity.cs delete mode 100644 Prometheus/CollectorIdentity.cs diff --git a/Benchmark.NetCore/SummaryBenchmarks.cs b/Benchmark.NetCore/SummaryBenchmarks.cs index d4638ede..a4169737 100644 --- a/Benchmark.NetCore/SummaryBenchmarks.cs +++ b/Benchmark.NetCore/SummaryBenchmarks.cs @@ -70,7 +70,7 @@ public async Task Summary_NPerSecond_For10Minutes() { lastExport = t; - await summary.CollectAndSerializeAsync(new TextSerializer(Stream.Null), default); + await summary.CollectAndSerializeAsync(new TextSerializer(Stream.Null), true, default); } } } diff --git a/Prometheus/Collector.cs b/Prometheus/Collector.cs index 45b4dc88..97ab6d78 100644 --- a/Prometheus/Collector.cs +++ b/Prometheus/Collector.cs @@ -46,7 +46,7 @@ public abstract class Collector internal abstract int ChildCount { get; } internal abstract int TimeseriesCount { get; } - internal abstract Task CollectAndSerializeAsync(IMetricsSerializer serializer, CancellationToken cancel); + internal abstract Task CollectAndSerializeAsync(IMetricsSerializer serializer, bool writeFamilyDeclaration, CancellationToken cancel); // Used by ChildBase.Remove() internal abstract void RemoveLabelled(LabelSequence instanceLabels); @@ -249,11 +249,13 @@ internal Collector(string name, string help, StringSequence instanceLabelNames, private readonly byte[][] _familyHeaderLines; - internal override async Task CollectAndSerializeAsync(IMetricsSerializer serializer, CancellationToken cancel) + internal override async Task CollectAndSerializeAsync(IMetricsSerializer serializer, bool writeFamilyDeclaration, CancellationToken cancel) { EnsureUnlabelledMetricCreatedIfNoLabels(); - await serializer.WriteFamilyDeclarationAsync(Name, NameBytes, HelpBytes, Type, TypeBytes, cancel); + // There may be multiple Collectors emitting data for the same family. Only the first will write out the family declaration. + if (writeFamilyDeclaration) + await serializer.WriteFamilyDeclarationAsync(Name, NameBytes, HelpBytes, Type, TypeBytes, cancel); foreach (var child in _labelledMetrics.Values) await child.CollectAndSerializeAsync(serializer, cancel); diff --git a/Prometheus/CollectorFamily.cs b/Prometheus/CollectorFamily.cs new file mode 100644 index 00000000..1905d7a1 --- /dev/null +++ b/Prometheus/CollectorFamily.cs @@ -0,0 +1,27 @@ +using System.Collections.Concurrent; + +namespace Prometheus; + +internal sealed class CollectorFamily +{ + public Type CollectorType { get; } + + // Different collectors in the same family are differentiated by different sets of static labels. + public ConcurrentDictionary Collectors { get; } = new(); + + public CollectorFamily(Type collectorType) + { + CollectorType = collectorType; + } + + internal async Task CollectAndSerializeAsync(IMetricsSerializer serializer, CancellationToken cancel) + { + bool isFirst = true; + + foreach (var collector in Collectors.Values) + { + await collector.CollectAndSerializeAsync(serializer, isFirst, cancel); + isFirst = false; + } + } +} diff --git a/Prometheus/CollectorFamilyIdentity.cs b/Prometheus/CollectorFamilyIdentity.cs new file mode 100644 index 00000000..81007974 --- /dev/null +++ b/Prometheus/CollectorFamilyIdentity.cs @@ -0,0 +1,70 @@ +namespace Prometheus; + +/// +/// Represents the values that make up a single collector family's unique identity. +/// Used during collector registration to identify which family each collector belongs to. +/// +/// If these values match an existing collector family, we will reuse it (or throw if metadata mismatches). +/// If these values do not match any existing collector family, we will create a new collector family. +/// +internal struct CollectorFamilyIdentity : IEquatable +{ + public readonly string Name; + public readonly StringSequence InstanceLabelNames; + public readonly StringSequence StaticLabelNames; + + private readonly int _hashCode; + + public CollectorFamilyIdentity(string name, StringSequence instanceLabelNames, StringSequence staticLabelNames) + { + Name = name; + InstanceLabelNames = instanceLabelNames; + StaticLabelNames = staticLabelNames; + + _hashCode = CalculateHashCode(name, instanceLabelNames, staticLabelNames); + } + + public bool Equals(CollectorFamilyIdentity other) + { + if (!string.Equals(Name, other.Name, StringComparison.Ordinal)) + return false; + + if (_hashCode != other._hashCode) + return false; + + if (InstanceLabelNames.Length != other.InstanceLabelNames.Length) + return false; + + if (!InstanceLabelNames.Equals(other.InstanceLabelNames)) + return false; + + if (!StaticLabelNames.Equals(other.StaticLabelNames)) + return false; + + return true; + } + + public override int GetHashCode() + { + return _hashCode; + } + + private static int CalculateHashCode(string name, StringSequence instanceLabelNames, StringSequence staticLabelNames) + { + unchecked + { + int hashCode = 0; + + hashCode ^= name.GetHashCode() * 31; + hashCode ^= instanceLabelNames.GetHashCode() * 397; + hashCode ^= staticLabelNames.GetHashCode() * 397; + + return hashCode; + } + } + + public override string ToString() + { + return $"{Name}{{{InstanceLabelNames.Length + StaticLabelNames.Length}}}"; + } +} diff --git a/Prometheus/CollectorIdentity.cs b/Prometheus/CollectorIdentity.cs deleted file mode 100644 index 84a829f0..00000000 --- a/Prometheus/CollectorIdentity.cs +++ /dev/null @@ -1,69 +0,0 @@ -namespace Prometheus -{ - /// - /// Represents the values that make up a single collector's unique identity, used during collector registration. - /// If these values match an existing collector, we will reuse it (or throw if metadata mismatches). - /// If these values do not match any existing collector, we will create a new collector. - /// - internal struct CollectorIdentity : IEquatable - { - public readonly string Name; - public readonly StringSequence InstanceLabelNames; - public readonly StringSequence StaticLabelNames; - - private readonly int _hashCode; - - public CollectorIdentity(string name, StringSequence instanceLabelNames, StringSequence staticLabelNames) - { - Name = name; - InstanceLabelNames = instanceLabelNames; - StaticLabelNames = staticLabelNames; - - _hashCode = CalculateHashCode(name, instanceLabelNames, staticLabelNames); - } - - public bool Equals(CollectorIdentity other) - { - if (!string.Equals(Name, other.Name, StringComparison.Ordinal)) - return false; - - if (_hashCode != other._hashCode) - return false; - - if (InstanceLabelNames.Length != other.InstanceLabelNames.Length) - return false; - - if (!InstanceLabelNames.Equals(other.InstanceLabelNames)) - return false; - - if (!StaticLabelNames.Equals(other.StaticLabelNames)) - return false; - - return true; - } - - public override int GetHashCode() - { - return _hashCode; - } - - private static int CalculateHashCode(string name, StringSequence instanceLabelNames, StringSequence staticLabelNames) - { - unchecked - { - int hashCode = 0; - - hashCode ^= name.GetHashCode() * 31; - hashCode ^= instanceLabelNames.GetHashCode() * 397; - hashCode ^= staticLabelNames.GetHashCode() * 397; - - return hashCode; - } - } - - public override string ToString() - { - return $"{Name}{{{InstanceLabelNames.Length + StaticLabelNames.Length}}}"; - } - } -} diff --git a/Prometheus/CollectorRegistry.cs b/Prometheus/CollectorRegistry.cs index 0d8d3eec..e7b13488 100644 --- a/Prometheus/CollectorRegistry.cs +++ b/Prometheus/CollectorRegistry.cs @@ -1,4 +1,5 @@ -using System.Collections.Concurrent; +using Microsoft.Extensions.Configuration; +using System.Collections.Concurrent; using System.Diagnostics; namespace Prometheus; @@ -84,7 +85,7 @@ public void SetStaticLabels(IDictionary labels) if (_staticLabels.Length != 0) throw new InvalidOperationException("Static labels have already been defined - you can only do it once per registry."); - if (_collectors.Count != 0) + if (_families.Count != 0) throw new InvalidOperationException("Metrics have already been added to the registry - cannot define static labels anymore."); // Keep the lock for the duration of this method to make sure no publishing happens while we are setting labels. @@ -136,7 +137,7 @@ internal LabelSequence GetStaticLabels() /// /// This method is designed to be used with custom output mechanisms that do not use an IMetricServer. /// - public Task CollectAndExportAsTextAsync(Stream to, ExpositionFormat format= ExpositionFormat.Text, CancellationToken cancel = default) + public Task CollectAndExportAsTextAsync(Stream to, ExpositionFormat format = ExpositionFormat.Text, CancellationToken cancel = default) { if (to == null) throw new ArgumentNullException(nameof(to)); @@ -171,7 +172,7 @@ public CollectorInitializer(CreateInstanceDelegate createInstance, string name, _configuration = configuration; } - public TCollector CreateInstance(CollectorIdentity _) => _createInstance(_name, _help, _instanceLabelNames, _staticLabels, _configuration); + public TCollector CreateInstance(LabelSequence _) => _createInstance(_name, _help, _instanceLabelNames, _staticLabels, _configuration); public delegate TCollector CreateInstanceDelegate(string name, string help, StringSequence instanceLabelNames, LabelSequence staticLabels, TConfiguration configuration); } @@ -183,31 +184,46 @@ internal TCollector GetOrAdd(in CollectorInitializer where TCollector : Collector where TConfiguration : MetricConfiguration { - var identity = new CollectorIdentity(initializer.Name, initializer.InstanceLabelNames, initializer.StaticLabels.Names); + // Should we optimize for the case where the family/collector is already registered? It is unlikely to be very common. However, we still should because: + // 1) In scenarios where collector re-registration is common, it could be an expensive persistent cost in terms of allocations. + // 2) In scenarios where collector re-registration is not common, a tiny compute overhead is unlikely to be significant in the big picture, as it only happens once. + + var family = GetOrAddCollectorFamily(initializer); + + var collectorIdentity = initializer.StaticLabels; + + if (family.Collectors.TryGetValue(collectorIdentity, out var existing)) + return (TCollector)existing; + + return (TCollector)family.Collectors.GetOrAdd(collectorIdentity, initializer.CreateInstance); + } + + private CollectorFamily GetOrAddCollectorFamily(in CollectorInitializer initializer) + where TCollector : Collector + where TConfiguration : MetricConfiguration + { + var familyIdentity = new CollectorFamilyIdentity(initializer.Name, initializer.InstanceLabelNames, initializer.StaticLabels.Names); - static TCollector Validate(Collector candidate) + static CollectorFamily ValidateFamily(CollectorFamily candidate) { - // We either created a new collector or found one with a matching identity. + // We either created a new collector family or found one with a matching identity. // We do some basic validation here to avoid silly API usage mistakes. - if (!(candidate is TCollector)) + if (candidate.CollectorType != typeof(TCollector)) throw new InvalidOperationException("Collector of a different type with the same identity is already registered."); - return (TCollector)candidate; + return candidate; } - // Should we optimize for the case where the collector is already registered? It is unlikely to be very common. However, we still should because: - // 1) In scenarios where collector re-registration is common, it could be an expensive persistent cost in terms of allocations. - // 2) In scenarios where collector re-registration is not common, a tiny compute overhead is unlikely to be significant in the big picture, as it only happens once. - if (_collectors.TryGetValue(identity, out var existing)) - return Validate(existing); - - var collector = _collectors.GetOrAdd(identity, initializer.CreateInstance); + if (_families.TryGetValue(familyIdentity, out var existing)) + return ValidateFamily(existing); - return Validate(collector); + var collector = _families.GetOrAdd(familyIdentity, new CollectorFamily(typeof(TCollector))); + return ValidateFamily(collector); } - private readonly ConcurrentDictionary _collectors = new ConcurrentDictionary(); + // Each collector family has an identity and any number of collectors within. + private readonly ConcurrentDictionary _families = new(); internal void SetBeforeFirstCollectCallback(Action a) { @@ -246,7 +262,7 @@ internal async Task CollectAndSerializeAsync(IMetricsSerializer serializer, Canc UpdateRegistryMetrics(); - foreach (var collector in _collectors.Values) + foreach (var collector in _families.Values) await collector.CollectAndSerializeAsync(serializer, cancel); await serializer.WriteEnd(cancel); await serializer.FlushAsync(cancel); @@ -315,7 +331,7 @@ internal void StartCollectingRegistryMetrics() private void UpdateRegistryMetrics() { - if (_metricFamiliesPerType == null ||_metricInstancesPerType == null || _metricTimeseriesPerType == null) + if (_metricFamiliesPerType == null || _metricInstancesPerType == null || _metricTimeseriesPerType == null) return; // Debug metrics are not enabled. foreach (MetricType type in Enum.GetValues(typeof(MetricType))) @@ -324,11 +340,19 @@ private void UpdateRegistryMetrics() long instances = 0; long timeseries = 0; - foreach (var collector in _collectors.Values.Where(c => c.Type == type)) + foreach (var family in _families.Values) { - families++; - instances += collector.ChildCount; - timeseries += collector.TimeseriesCount; + bool hadMatchingType = false; + + foreach (var collector in family.Collectors.Values.Where(c => c.Type == type)) + { + hadMatchingType = true; + instances += collector.ChildCount; + timeseries += collector.TimeseriesCount; + } + + if (hadMatchingType) + families++; } _metricFamiliesPerType[type].Set(families); diff --git a/Prometheus/ManagedLifetimeMetricFactory.cs b/Prometheus/ManagedLifetimeMetricFactory.cs index 647841fa..570a6774 100644 --- a/Prometheus/ManagedLifetimeMetricFactory.cs +++ b/Prometheus/ManagedLifetimeMetricFactory.cs @@ -20,7 +20,7 @@ public ManagedLifetimeMetricFactory(MetricFactory inner, TimeSpan expiresAfter) public IManagedLifetimeMetricHandle CreateCounter(string name, string help, string[] instanceLabelNames, CounterConfiguration? configuration = null) { - var identity = new CollectorIdentity(name, StringSequence.From(instanceLabelNames), StringSequence.Empty); + var identity = new CollectorFamilyIdentity(name, StringSequence.From(instanceLabelNames), StringSequence.Empty); // Let's be optimistic and assume that in the typical case, the metric will already exist. if (_counters.TryGetValue(identity, out var existing)) @@ -32,7 +32,7 @@ public IManagedLifetimeMetricHandle CreateCounter(string name, string public IManagedLifetimeMetricHandle CreateGauge(string name, string help, string[] instanceLabelNames, GaugeConfiguration? configuration = null) { - var identity = new CollectorIdentity(name, StringSequence.From(instanceLabelNames), StringSequence.Empty); + var identity = new CollectorFamilyIdentity(name, StringSequence.From(instanceLabelNames), StringSequence.Empty); // Let's be optimistic and assume that in the typical case, the metric will already exist. if (_gauges.TryGetValue(identity, out var existing)) @@ -44,7 +44,7 @@ public IManagedLifetimeMetricHandle CreateGauge(string name, string help public IManagedLifetimeMetricHandle CreateHistogram(string name, string help, string[] instanceLabelNames, HistogramConfiguration? configuration = null) { - var identity = new CollectorIdentity(name, StringSequence.From(instanceLabelNames), StringSequence.Empty); + var identity = new CollectorFamilyIdentity(name, StringSequence.From(instanceLabelNames), StringSequence.Empty); // Let's be optimistic and assume that in the typical case, the metric will already exist. if (_histograms.TryGetValue(identity, out var existing)) @@ -56,7 +56,7 @@ public IManagedLifetimeMetricHandle CreateHistogram(string name, str public IManagedLifetimeMetricHandle CreateSummary(string name, string help, string[] instanceLabelNames, SummaryConfiguration? configuration = null) { - var identity = new CollectorIdentity(name, StringSequence.From(instanceLabelNames), StringSequence.Empty); + var identity = new CollectorFamilyIdentity(name, StringSequence.From(instanceLabelNames), StringSequence.Empty); // Let's be optimistic and assume that in the typical case, the metric will already exist. if (_summaries.TryGetValue(identity, out var existing)) @@ -74,10 +74,10 @@ public IManagedLifetimeMetricHandle CreateSummary(string name, string // We need to reuse existing instances of lifetime-managed metrics because the user might not want to cache it. // This somewhat duplicates the metric identity tracking logic in CollectorRegistry but this is intentional, as we really do need to do this work on two layers. // We never remove collectors from here as long as the factory is alive. The expectation is that there is not an unbounded set of label names, so this set is non-gigantic. - private readonly ConcurrentDictionary _counters = new(); - private readonly ConcurrentDictionary _gauges = new(); - private readonly ConcurrentDictionary _histograms = new(); - private readonly ConcurrentDictionary _summaries = new(); + private readonly ConcurrentDictionary _counters = new(); + private readonly ConcurrentDictionary _gauges = new(); + private readonly ConcurrentDictionary _histograms = new(); + private readonly ConcurrentDictionary _summaries = new(); private readonly struct CounterInitializer { @@ -94,7 +94,7 @@ public CounterInitializer(MetricFactory inner, TimeSpan expiresAfter, string hel Configuration = configuration; } - public ManagedLifetimeCounter CreateInstance(CollectorIdentity identity) + public ManagedLifetimeCounter CreateInstance(CollectorFamilyIdentity identity) { var metric = Inner.CreateCounter(identity.Name, Help, identity.InstanceLabelNames, Configuration); return new ManagedLifetimeCounter(metric, ExpiresAfter); @@ -116,7 +116,7 @@ public GaugeInitializer(MetricFactory inner, TimeSpan expiresAfter, string help, Configuration = configuration; } - public ManagedLifetimeGauge CreateInstance(CollectorIdentity identity) + public ManagedLifetimeGauge CreateInstance(CollectorFamilyIdentity identity) { var metric = Inner.CreateGauge(identity.Name, Help, identity.InstanceLabelNames, Configuration); return new ManagedLifetimeGauge(metric, ExpiresAfter); @@ -138,7 +138,7 @@ public HistogramInitializer(MetricFactory inner, TimeSpan expiresAfter, string h Configuration = configuration; } - public ManagedLifetimeHistogram CreateInstance(CollectorIdentity identity) + public ManagedLifetimeHistogram CreateInstance(CollectorFamilyIdentity identity) { var metric = Inner.CreateHistogram(identity.Name, Help, identity.InstanceLabelNames, Configuration); return new ManagedLifetimeHistogram(metric, ExpiresAfter); @@ -160,7 +160,7 @@ public SummaryInitializer(MetricFactory inner, TimeSpan expiresAfter, string hel Configuration = configuration; } - public ManagedLifetimeSummary CreateInstance(CollectorIdentity identity) + public ManagedLifetimeSummary CreateInstance(CollectorFamilyIdentity identity) { var metric = Inner.CreateSummary(identity.Name, Help, identity.InstanceLabelNames, Configuration); return new ManagedLifetimeSummary(metric, ExpiresAfter); diff --git a/Tests.NetCore/FactoryLabelTests.cs b/Tests.NetCore/FactoryLabelTests.cs index 94baf170..bf6da23a 100644 --- a/Tests.NetCore/FactoryLabelTests.cs +++ b/Tests.NetCore/FactoryLabelTests.cs @@ -1,6 +1,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; namespace Prometheus.Tests @@ -91,5 +92,36 @@ public void WithLabels_WithEmptyLabelSet_IsNoop() Assert.AreEqual(2, counter1.Value); } + + // https://github.com/prometheus-net/prometheus-net/issues/389 + [TestMethod] + public async Task Issue389() + { + _registry.SetStaticLabels(new Dictionary { { "registry", "registry-label-value" } }); + + var factory1 = _metrics.WithLabels(new Dictionary { { "factory", "factory1" } }); + var factory2 = _metrics.WithLabels(new Dictionary { { "factory", "factory2" } }); + var factory3 = _metrics.WithLabels(new Dictionary { { "factory", "factory3" } }); + + var metric1 = factory1.CreateCounter("counter", ""); + var metric2 = factory2.CreateCounter("counter", ""); + var metric3 = factory3.CreateCounter("counter", ""); + + metric1.Inc(); + metric2.Inc(); + metric3.Inc(); + + Assert.AreEqual(1, metric1.Value); + Assert.AreEqual(1, metric2.Value); + Assert.AreEqual(1, metric3.Value); + + var serialized = await _registry.CollectAndSerializeToStringAsync(); + + // It should serialize them all as a single family, not multiple families. + var lines = serialized.Split('\n'); + var familyDeclarationLineCount = lines.Count(x => x.StartsWith("# TYPE ")); + + Assert.AreEqual(1, familyDeclarationLineCount); + } } } diff --git a/Tests.NetCore/HistogramTests.cs b/Tests.NetCore/HistogramTests.cs index 702724fb..e5fb2869 100644 --- a/Tests.NetCore/HistogramTests.cs +++ b/Tests.NetCore/HistogramTests.cs @@ -52,7 +52,7 @@ public async Task Observe_IncrementsCorrectBucketsAndCountAndSum() histogram.Observe(3.0); var serializer = Substitute.For(); - await histogram.CollectAndSerializeAsync(serializer, default); + await histogram.CollectAndSerializeAsync(serializer, true, default); // Sum // Count From e83cc873111c68df2a0992a31ca4dbc33d9c25d3 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Fri, 30 Dec 2022 14:30:38 +0200 Subject: [PATCH 036/230] Deterministic builds --- Prometheus.AspNetCore.Grpc/Prometheus.AspNetCore.Grpc.csproj | 5 +++++ .../Prometheus.AspNetCore.HealthChecks.csproj | 5 +++++ Prometheus.AspNetCore/Prometheus.AspNetCore.csproj | 5 +++++ Prometheus/Prometheus.csproj | 5 +++++ 4 files changed, 20 insertions(+) diff --git a/Prometheus.AspNetCore.Grpc/Prometheus.AspNetCore.Grpc.csproj b/Prometheus.AspNetCore.Grpc/Prometheus.AspNetCore.Grpc.csproj index 62447b54..cbbdbcc7 100644 --- a/Prometheus.AspNetCore.Grpc/Prometheus.AspNetCore.Grpc.csproj +++ b/Prometheus.AspNetCore.Grpc/Prometheus.AspNetCore.Grpc.csproj @@ -21,6 +21,11 @@ True + + + true + + diff --git a/Prometheus.AspNetCore.HealthChecks/Prometheus.AspNetCore.HealthChecks.csproj b/Prometheus.AspNetCore.HealthChecks/Prometheus.AspNetCore.HealthChecks.csproj index 95f49e9d..d5af36ff 100644 --- a/Prometheus.AspNetCore.HealthChecks/Prometheus.AspNetCore.HealthChecks.csproj +++ b/Prometheus.AspNetCore.HealthChecks/Prometheus.AspNetCore.HealthChecks.csproj @@ -21,6 +21,11 @@ True + + + true + + diff --git a/Prometheus.AspNetCore/Prometheus.AspNetCore.csproj b/Prometheus.AspNetCore/Prometheus.AspNetCore.csproj index c0ca6a85..888fecd4 100644 --- a/Prometheus.AspNetCore/Prometheus.AspNetCore.csproj +++ b/Prometheus.AspNetCore/Prometheus.AspNetCore.csproj @@ -21,6 +21,11 @@ True + + + true + + diff --git a/Prometheus/Prometheus.csproj b/Prometheus/Prometheus.csproj index 91f1afbe..22e58728 100644 --- a/Prometheus/Prometheus.csproj +++ b/Prometheus/Prometheus.csproj @@ -34,6 +34,11 @@ True + + + true + + From 6db6a9cbccccb2e71b63df1ef31f87e5968b018c Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Sat, 31 Dec 2022 08:40:20 +0200 Subject: [PATCH 037/230] Remove misleading _total from MeterAdapter debug metric --- Prometheus/MeterAdapter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Prometheus/MeterAdapter.cs b/Prometheus/MeterAdapter.cs index 2e3e776d..0cedd6aa 100644 --- a/Prometheus/MeterAdapter.cs +++ b/Prometheus/MeterAdapter.cs @@ -37,7 +37,7 @@ private MeterAdapter(MeterAdapterOptions options) _listener.SetMeasurementEventCallback(OnMeasurementRecorded); var regularFactory = Metrics.WithCustomRegistry(_registry); - _instrumentsConnected = regularFactory.CreateGauge("prometheus_net_meteradapter_instruments_connected_total", "Number of instruments that are currently connected to the adapter."); + _instrumentsConnected = regularFactory.CreateGauge("prometheus_net_meteradapter_instruments_connected", "Number of instruments that are currently connected to the adapter."); _listener.Start(); From fb39dcb52e2539acb83547929d3691fabdced726 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Sat, 31 Dec 2022 08:47:28 +0200 Subject: [PATCH 038/230] Merge nuspec files into csproj --- .../Prometheus.AspNetCore.Grpc.csproj | 26 ++++ .../Prometheus.AspNetCore.HealthChecks.csproj | 26 ++++ .../Prometheus.AspNetCore.csproj | 26 ++++ .../AspNetMetricServer.cs | 111 +++++++------- .../Prometheus.NetFramework.AspNet.csproj | 140 +++++++++--------- .../packages.config | 5 - Prometheus/Prometheus.csproj | 26 ++++ Resources/Nuspec/Merge-ReleaseNotes.ps1 | 14 -- .../prometheus-net.AspNetCore.Grpc.nuspec | 30 ---- ...metheus-net.AspNetCore.HealthChecks.nuspec | 29 ---- .../Nuspec/prometheus-net.AspNetCore.nuspec | 36 ----- .../prometheus-net.NetFramework.AspNet.nuspec | 32 ---- Resources/Nuspec/prometheus-net.nuspec | 45 ------ .../{Nuspec => }/prometheus-net-logo.png | Bin 14 files changed, 225 insertions(+), 321 deletions(-) delete mode 100644 Prometheus.NetFramework.AspNet/packages.config delete mode 100644 Resources/Nuspec/Merge-ReleaseNotes.ps1 delete mode 100644 Resources/Nuspec/prometheus-net.AspNetCore.Grpc.nuspec delete mode 100644 Resources/Nuspec/prometheus-net.AspNetCore.HealthChecks.nuspec delete mode 100644 Resources/Nuspec/prometheus-net.AspNetCore.nuspec delete mode 100644 Resources/Nuspec/prometheus-net.NetFramework.AspNet.nuspec delete mode 100644 Resources/Nuspec/prometheus-net.nuspec rename Resources/{Nuspec => }/prometheus-net-logo.png (100%) diff --git a/Prometheus.AspNetCore.Grpc/Prometheus.AspNetCore.Grpc.csproj b/Prometheus.AspNetCore.Grpc/Prometheus.AspNetCore.Grpc.csproj index cbbdbcc7..9a68b9dd 100644 --- a/Prometheus.AspNetCore.Grpc/Prometheus.AspNetCore.Grpc.csproj +++ b/Prometheus.AspNetCore.Grpc/Prometheus.AspNetCore.Grpc.csproj @@ -19,6 +19,21 @@ 9999 True + + prometheus-net.AspNetCore.Grpc + sandersaares + prometheus-net + prometheus-net + ASP.NET Core gRPC integration with Prometheus + Copyright © prometheus-net developers + https://github.com/prometheus-net/prometheus-net + prometheus-net-logo.png + README.md + https://github.com/prometheus-net/prometheus-net.git + metrics prometheus aspnetcore + MIT + True + snupkg @@ -26,6 +41,17 @@ true + + + True + \ + + + True + \ + + + diff --git a/Prometheus.AspNetCore.HealthChecks/Prometheus.AspNetCore.HealthChecks.csproj b/Prometheus.AspNetCore.HealthChecks/Prometheus.AspNetCore.HealthChecks.csproj index d5af36ff..fde02f79 100644 --- a/Prometheus.AspNetCore.HealthChecks/Prometheus.AspNetCore.HealthChecks.csproj +++ b/Prometheus.AspNetCore.HealthChecks/Prometheus.AspNetCore.HealthChecks.csproj @@ -19,6 +19,21 @@ 9999 True + + prometheus-net.AspNetCore.HealthChecks + sandersaares + prometheus-net + prometheus-net + ASP.NET Core Health Checks integration with Prometheus + Copyright © prometheus-net developers + https://github.com/prometheus-net/prometheus-net + prometheus-net-logo.png + README.md + https://github.com/prometheus-net/prometheus-net.git + metrics prometheus aspnetcore + MIT + True + snupkg @@ -26,6 +41,17 @@ true + + + True + \ + + + True + \ + + + diff --git a/Prometheus.AspNetCore/Prometheus.AspNetCore.csproj b/Prometheus.AspNetCore/Prometheus.AspNetCore.csproj index 888fecd4..2012824f 100644 --- a/Prometheus.AspNetCore/Prometheus.AspNetCore.csproj +++ b/Prometheus.AspNetCore/Prometheus.AspNetCore.csproj @@ -19,6 +19,21 @@ 9999 True + + prometheus-net.AspNetCore + andrasm,qed-,lakario,sandersaares + prometheus-net + prometheus-net + ASP.NET Core middleware and stand-alone Kestrel server for exporting metrics to Prometheus + Copyright © prometheus-net developers + https://github.com/prometheus-net/prometheus-net + prometheus-net-logo.png + README.md + https://github.com/prometheus-net/prometheus-net.git + metrics prometheus aspnetcore + MIT + True + snupkg @@ -26,6 +41,17 @@ true + + + True + \ + + + True + \ + + + diff --git a/Prometheus.NetFramework.AspNet/AspNetMetricServer.cs b/Prometheus.NetFramework.AspNet/AspNetMetricServer.cs index 46abf391..256cd232 100644 --- a/Prometheus.NetFramework.AspNet/AspNetMetricServer.cs +++ b/Prometheus.NetFramework.AspNet/AspNetMetricServer.cs @@ -1,78 +1,73 @@ -using System; -using System.Net; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; +using System.Net; using System.Web.Http; -namespace Prometheus +namespace Prometheus; + +public static class AspNetMetricServer { - public static class AspNetMetricServer + private const string RouteNamePrefix = "Prometheus_"; + + public sealed class Settings { - private const string RouteNamePrefix = "Prometheus_"; + public CollectorRegistry? Registry { get; set; } + } - public sealed class Settings - { - public CollectorRegistry Registry { get; set; } - } + /// + /// Registers an anonymous instance of the controller to be published on the /metrics URL. + /// + public static void RegisterRoutes(HttpConfiguration configuration, Settings? settings = null) => + MapRoute(configuration, "Default", "metrics", settings); - /// - /// Registers an anonymous instance of the controller to be published on the /metrics URL. - /// - public static void RegisterRoutes(HttpConfiguration configuration, Settings settings = null) => - MapRoute(configuration, "Default", "metrics", settings); + /// + /// Registers an anonymous instance of the controller to be published on a given URL path (e.g. "custom/metrics"). + /// + public static void RegisterRoutes(HttpConfiguration configuration, string path, Settings? settings = null) => + MapRoute(configuration, path, path, settings); - /// - /// Registers an anonymous instance of the controller to be published on a given URL path (e.g. "custom/metrics"). - /// - public static void RegisterRoutes(HttpConfiguration configuration, string path, Settings settings = null) => - MapRoute(configuration, path, path, settings); + private static void MapRoute(HttpConfiguration configuration, string routeName, string routeTemplate, Settings? settings) + { + if (configuration == null) + throw new ArgumentNullException(nameof(configuration)); - private static void MapRoute(HttpConfiguration configuration, string routeName, string routeTemplate, Settings settings) - { - if (configuration == null) - throw new ArgumentNullException(nameof(configuration)); + configuration.Routes.MapHttpRoute( + name: $"{RouteNamePrefix}{routeName}", + routeTemplate: routeTemplate, + defaults: null, + constraints: null, + handler: new Handler(settings?.Registry ?? Metrics.DefaultRegistry)); + } - configuration.Routes.MapHttpRoute( - name: $"{RouteNamePrefix}{routeName}", - routeTemplate: routeTemplate, - defaults: null, - constraints: null, - handler: new Handler(settings?.Registry ?? Metrics.DefaultRegistry)); + private sealed class Handler : HttpMessageHandler + { + public Handler(CollectorRegistry registry) + { + _registry = registry; } - private sealed class Handler : HttpMessageHandler - { - public Handler(CollectorRegistry registry) - { - _registry = registry; - } + private readonly CollectorRegistry _registry; - private readonly CollectorRegistry _registry; + protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + // The ASP.NET PushStreamContent does not have a way to easily handle exceptions that + // occur before we write to the stream (when we can still return nice error headers). + // Maybe in a future version this could be improved, as right now exception == aborted connection. - protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + var response = new HttpResponseMessage(HttpStatusCode.OK) { - // The ASP.NET PushStreamContent does not have a way to easily handle exceptions that - // occur before we write to the stream (when we can still return nice error headers). - // Maybe in a future version this could be improved, as right now exception == aborted connection. - - var response = new HttpResponseMessage(HttpStatusCode.OK) + Content = new PushStreamContent(async (stream, content, context) => { - Content = new PushStreamContent(async (stream, content, context) => + try + { + await _registry.CollectAndExportAsTextAsync(stream, ExpositionFormat.Text, cancellationToken); + } + finally { - try - { - await _registry.CollectAndExportAsTextAsync(stream, ExpositionFormat.Text, cancellationToken); - } - finally - { - stream.Close(); - } - }, PrometheusConstants.ExporterContentTypeValue) - }; + stream.Close(); + } + }, PrometheusConstants.ExporterContentTypeValue) + }; - return Task.FromResult(response); - } + return Task.FromResult(response); } } } diff --git a/Prometheus.NetFramework.AspNet/Prometheus.NetFramework.AspNet.csproj b/Prometheus.NetFramework.AspNet/Prometheus.NetFramework.AspNet.csproj index e486de41..0dafe186 100644 --- a/Prometheus.NetFramework.AspNet/Prometheus.NetFramework.AspNet.csproj +++ b/Prometheus.NetFramework.AspNet/Prometheus.NetFramework.AspNet.csproj @@ -1,72 +1,68 @@ - - - - - Debug - AnyCPU - {25C05E15-A8F8-4F36-A35C-4004C0948305} - Library - Properties - Prometheus - Prometheus.NetFramework.AspNet - v4.6.2 - 512 - true - true - ..\Resources\prometheus-net.snk - - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - true - true - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - true - true - - - - - - ..\packages\Microsoft.AspNet.WebApi.Client.5.2.9\lib\net45\System.Net.Http.Formatting.dll - - - ..\packages\Microsoft.AspNet.WebApi.Core.5.2.9\lib\net45\System.Web.Http.dll - - - - - - - - - - - Properties\SolutionAssemblyInfo.cs - - - - - - - - - {e585417c-f7dd-4d8c-a0c5-d4b79594634a} - Prometheus - - - - \ No newline at end of file + + + + netframework462 + Prometheus + + false + + true + ..\Resources\prometheus-net.snk + + enable + enable + True + True + 1591 + + latest + 9999 + + True + + prometheus-net.NetFramework.AspNet + sandersaares + prometheus-net + prometheus-net + ASP.NET Web API exporter for Prometheus + Copyright © prometheus-net developers + https://github.com/prometheus-net/prometheus-net + prometheus-net-logo.png + README.md + https://github.com/prometheus-net/prometheus-net.git + metrics prometheus aspnetcore + MIT + True + snupkg + + + + + true + + + + + True + \ + + + True + \ + + + + + + + + + + + + + + + + + diff --git a/Prometheus.NetFramework.AspNet/packages.config b/Prometheus.NetFramework.AspNet/packages.config deleted file mode 100644 index c77a9bd1..00000000 --- a/Prometheus.NetFramework.AspNet/packages.config +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/Prometheus/Prometheus.csproj b/Prometheus/Prometheus.csproj index 22e58728..04f5b054 100644 --- a/Prometheus/Prometheus.csproj +++ b/Prometheus/Prometheus.csproj @@ -32,6 +32,21 @@ 9999 True + + prometheus-net + andrasm,qed-,lakario,sandersaares + prometheus-net + prometheus-net + .NET client library for the Prometheus monitoring and alerting system + Copyright © prometheus-net developers + https://github.com/prometheus-net/prometheus-net + prometheus-net-logo.png + README.md + https://github.com/prometheus-net/prometheus-net.git + metrics prometheus + MIT + True + snupkg @@ -48,6 +63,17 @@ + + + True + \ + + + True + \ + + + diff --git a/Resources/Nuspec/Merge-ReleaseNotes.ps1 b/Resources/Nuspec/Merge-ReleaseNotes.ps1 deleted file mode 100644 index 53de7c84..00000000 --- a/Resources/Nuspec/Merge-ReleaseNotes.ps1 +++ /dev/null @@ -1,14 +0,0 @@ -$ErrorActionPreference = "Stop" - -# This script merges the History file into the NuSpec (using release agent paths). -# It just exists to enable maintaining the history as a plain text file, not hidden away somewhere. - -# Path as it exists on the release agent. -$historyPath = Join-Path $PSScriptRoot "..\History\History" -$releaseNotes = [IO.File]::ReadAllText($historyPath) - -foreach ($nuspec in Get-ChildItem -Path $PSScriptRoot -Filter *.nuspec) { - $content = [IO.File]::ReadAllText($nuspec.FullName) - $content = $content.Replace("", "$releaseNotes") - [IO.File]::WriteAllText($nuspec.FullName, $content) -} \ No newline at end of file diff --git a/Resources/Nuspec/prometheus-net.AspNetCore.Grpc.nuspec b/Resources/Nuspec/prometheus-net.AspNetCore.Grpc.nuspec deleted file mode 100644 index bb60c364..00000000 --- a/Resources/Nuspec/prometheus-net.AspNetCore.Grpc.nuspec +++ /dev/null @@ -1,30 +0,0 @@ - - - - prometheus-net.AspNetCore.Grpc - sandersaares - ASP.NET Core gRPC integration with Prometheus - https://github.com/prometheus-net/prometheus-net - MIT - metrics prometheus aspnetcore grpc - Copyright © prometheus-net developers - images/prometheus-net-logo.png - - - __NUGETPACKAGEVERSION__ - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Resources/Nuspec/prometheus-net.AspNetCore.HealthChecks.nuspec b/Resources/Nuspec/prometheus-net.AspNetCore.HealthChecks.nuspec deleted file mode 100644 index 0b3b94ba..00000000 --- a/Resources/Nuspec/prometheus-net.AspNetCore.HealthChecks.nuspec +++ /dev/null @@ -1,29 +0,0 @@ - - - - prometheus-net.AspNetCore.HealthChecks - sandersaares - ASP.NET Core Health Checks integration with Prometheus - https://github.com/prometheus-net/prometheus-net - MIT - metrics prometheus aspnetcore healthchecks - Copyright © prometheus-net developers - images/prometheus-net-logo.png - - - __NUGETPACKAGEVERSION__ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Resources/Nuspec/prometheus-net.AspNetCore.nuspec b/Resources/Nuspec/prometheus-net.AspNetCore.nuspec deleted file mode 100644 index 5180625f..00000000 --- a/Resources/Nuspec/prometheus-net.AspNetCore.nuspec +++ /dev/null @@ -1,36 +0,0 @@ - - - - prometheus-net.AspNetCore - andrasm,qed-,lakario,sandersaares - ASP.NET Core middleware and stand-alone Kestrel server for exporting metrics to Prometheus - https://github.com/prometheus-net/prometheus-net - MIT - metrics prometheus aspnetcore - Copyright © prometheus-net developers - images/prometheus-net-logo.png - - - __NUGETPACKAGEVERSION__ - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Resources/Nuspec/prometheus-net.NetFramework.AspNet.nuspec b/Resources/Nuspec/prometheus-net.NetFramework.AspNet.nuspec deleted file mode 100644 index cab98a17..00000000 --- a/Resources/Nuspec/prometheus-net.NetFramework.AspNet.nuspec +++ /dev/null @@ -1,32 +0,0 @@ - - - - prometheus-net.NetFramework.AspNet - sandersaares - ASP.NET Web API exporter for Prometheus - https://github.com/prometheus-net/prometheus-net - MIT - metrics prometheus aspnet webapi - Copyright © prometheus-net developers - images/prometheus-net-logo.png - - - __NUGETPACKAGEVERSION__ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Resources/Nuspec/prometheus-net.nuspec b/Resources/Nuspec/prometheus-net.nuspec deleted file mode 100644 index 74bf9f5a..00000000 --- a/Resources/Nuspec/prometheus-net.nuspec +++ /dev/null @@ -1,45 +0,0 @@ - - - - prometheus-net - andrasm,qed-,lakario,sandersaares - .NET client library for the Prometheus monitoring and alerting system - https://github.com/prometheus-net/prometheus-net - MIT - metrics prometheus - Copyright © prometheus-net developers - images/prometheus-net-logo.png - - - __NUGETPACKAGEVERSION__ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Resources/Nuspec/prometheus-net-logo.png b/Resources/prometheus-net-logo.png similarity index 100% rename from Resources/Nuspec/prometheus-net-logo.png rename to Resources/prometheus-net-logo.png From 1f1e34e2cdfd561f08cde344cc8bfddae020e0c3 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Sat, 31 Dec 2022 12:18:05 +0200 Subject: [PATCH 039/230] Source Link --- .../Prometheus.AspNetCore.Grpc.csproj | 10 +++++++++- .../Prometheus.AspNetCore.HealthChecks.csproj | 9 ++++++++- Prometheus.AspNetCore/Prometheus.AspNetCore.csproj | 10 +++++++++- .../Prometheus.NetFramework.AspNet.csproj | 13 +++++++++---- Prometheus/Prometheus.csproj | 7 ++++++- 5 files changed, 41 insertions(+), 8 deletions(-) diff --git a/Prometheus.AspNetCore.Grpc/Prometheus.AspNetCore.Grpc.csproj b/Prometheus.AspNetCore.Grpc/Prometheus.AspNetCore.Grpc.csproj index 9a68b9dd..7d1556fa 100644 --- a/Prometheus.AspNetCore.Grpc/Prometheus.AspNetCore.Grpc.csproj +++ b/Prometheus.AspNetCore.Grpc/Prometheus.AspNetCore.Grpc.csproj @@ -20,6 +20,11 @@ True + + true + true + + prometheus-net.AspNetCore.Grpc sandersaares prometheus-net @@ -29,7 +34,6 @@ https://github.com/prometheus-net/prometheus-net prometheus-net-logo.png README.md - https://github.com/prometheus-net/prometheus-net.git metrics prometheus aspnetcore MIT True @@ -61,4 +65,8 @@ + + + + diff --git a/Prometheus.AspNetCore.HealthChecks/Prometheus.AspNetCore.HealthChecks.csproj b/Prometheus.AspNetCore.HealthChecks/Prometheus.AspNetCore.HealthChecks.csproj index fde02f79..68ade9f2 100644 --- a/Prometheus.AspNetCore.HealthChecks/Prometheus.AspNetCore.HealthChecks.csproj +++ b/Prometheus.AspNetCore.HealthChecks/Prometheus.AspNetCore.HealthChecks.csproj @@ -20,6 +20,11 @@ True + + true + true + + prometheus-net.AspNetCore.HealthChecks sandersaares prometheus-net @@ -29,7 +34,6 @@ https://github.com/prometheus-net/prometheus-net prometheus-net-logo.png README.md - https://github.com/prometheus-net/prometheus-net.git metrics prometheus aspnetcore MIT True @@ -58,10 +62,13 @@ + + + diff --git a/Prometheus.AspNetCore/Prometheus.AspNetCore.csproj b/Prometheus.AspNetCore/Prometheus.AspNetCore.csproj index 2012824f..8a84ff63 100644 --- a/Prometheus.AspNetCore/Prometheus.AspNetCore.csproj +++ b/Prometheus.AspNetCore/Prometheus.AspNetCore.csproj @@ -20,6 +20,11 @@ True + + true + true + + prometheus-net.AspNetCore andrasm,qed-,lakario,sandersaares prometheus-net @@ -29,7 +34,6 @@ https://github.com/prometheus-net/prometheus-net prometheus-net-logo.png README.md - https://github.com/prometheus-net/prometheus-net.git metrics prometheus aspnetcore MIT True @@ -64,4 +68,8 @@ + + + + diff --git a/Prometheus.NetFramework.AspNet/Prometheus.NetFramework.AspNet.csproj b/Prometheus.NetFramework.AspNet/Prometheus.NetFramework.AspNet.csproj index 0dafe186..cdd99c2e 100644 --- a/Prometheus.NetFramework.AspNet/Prometheus.NetFramework.AspNet.csproj +++ b/Prometheus.NetFramework.AspNet/Prometheus.NetFramework.AspNet.csproj @@ -20,6 +20,11 @@ True + + true + true + + prometheus-net.NetFramework.AspNet sandersaares prometheus-net @@ -29,7 +34,6 @@ https://github.com/prometheus-net/prometheus-net prometheus-net-logo.png README.md - https://github.com/prometheus-net/prometheus-net.git metrics prometheus aspnetcore MIT True @@ -57,12 +61,13 @@ - - + + + - + diff --git a/Prometheus/Prometheus.csproj b/Prometheus/Prometheus.csproj index 04f5b054..78ff6711 100644 --- a/Prometheus/Prometheus.csproj +++ b/Prometheus/Prometheus.csproj @@ -33,6 +33,11 @@ True + + true + true + + prometheus-net andrasm,qed-,lakario,sandersaares prometheus-net @@ -42,7 +47,6 @@ https://github.com/prometheus-net/prometheus-net prometheus-net-logo.png README.md - https://github.com/prometheus-net/prometheus-net.git metrics prometheus MIT True @@ -77,6 +81,7 @@ + From 13e7ac6c3186aebd2da1470ff9b85f398a7e8225 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Sat, 31 Dec 2022 12:19:08 +0200 Subject: [PATCH 040/230] History --- History | 2 ++ 1 file changed, 2 insertions(+) diff --git a/History b/History index 799a701a..06b7f4e3 100644 --- a/History +++ b/History @@ -2,6 +2,8 @@ - Added OpenMetrics exposition format support (#388). - Added exemplar support for Counter and Histogram (#388). - The ':' character is no longer allowed in metric or label names. For metric names, Prometheus standard practice is to use colon only in recording rules. +- Publish symbol packages and add Source Link support for easier debugging experience. +- Fix defect where metrics with different labels could overwrite each other in specific circumstances with multiple metric factories in use (#389). * 7.1.0 - Added back .NET Standard 2.0 support as some customers had a hard dependency on .NET Standard 2.0 (despite not being a documented feature even earlier). - Added (Observable)UpDownCounter support to MeterAdapter (.NET 7 specific feature). From fada9d700a6a180cbc662057535b25caa7095a0b Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Sun, 1 Jan 2023 09:13:54 +0200 Subject: [PATCH 041/230] Add test to make sure histogram exemplar is on the right bucket --- .../MetricServerMiddleware.cs | 2 +- .../AspNetMetricServer.cs | 2 +- Prometheus/CollectorRegistry.cs | 2 +- Prometheus/ExpositionFormats.cs | 2 +- Prometheus/ICollectorRegistry.cs | 2 +- Prometheus/TextSerializer.cs | 4 +- Tests.NetCore/HistogramTests.cs | 52 +++++++++++++++---- Tests.NetCore/TestExtensions.cs | 15 +++--- Tests.NetCore/TextSerializerTests.cs | 2 +- 9 files changed, 58 insertions(+), 25 deletions(-) diff --git a/Prometheus.AspNetCore/MetricServerMiddleware.cs b/Prometheus.AspNetCore/MetricServerMiddleware.cs index fd210796..620d7856 100644 --- a/Prometheus.AspNetCore/MetricServerMiddleware.cs +++ b/Prometheus.AspNetCore/MetricServerMiddleware.cs @@ -81,7 +81,7 @@ private ProtocolNegotiationResult NegotiateComminucationProtocol(HttpRequest req } } - return new ProtocolNegotiationResult(ExpositionFormat.Text, PrometheusConstants.TextContentTypeWithVersionAndEncoding); + return new ProtocolNegotiationResult(ExpositionFormat.PrometheusText, PrometheusConstants.TextContentTypeWithVersionAndEncoding); } public async Task Invoke(HttpContext context) diff --git a/Prometheus.NetFramework.AspNet/AspNetMetricServer.cs b/Prometheus.NetFramework.AspNet/AspNetMetricServer.cs index 256cd232..a9d28c9d 100644 --- a/Prometheus.NetFramework.AspNet/AspNetMetricServer.cs +++ b/Prometheus.NetFramework.AspNet/AspNetMetricServer.cs @@ -58,7 +58,7 @@ protected override Task SendAsync(HttpRequestMessage reques { try { - await _registry.CollectAndExportAsTextAsync(stream, ExpositionFormat.Text, cancellationToken); + await _registry.CollectAndExportAsTextAsync(stream, ExpositionFormat.PrometheusText, cancellationToken); } finally { diff --git a/Prometheus/CollectorRegistry.cs b/Prometheus/CollectorRegistry.cs index e7b13488..45423e29 100644 --- a/Prometheus/CollectorRegistry.cs +++ b/Prometheus/CollectorRegistry.cs @@ -137,7 +137,7 @@ internal LabelSequence GetStaticLabels() /// /// This method is designed to be used with custom output mechanisms that do not use an IMetricServer. /// - public Task CollectAndExportAsTextAsync(Stream to, ExpositionFormat format = ExpositionFormat.Text, CancellationToken cancel = default) + public Task CollectAndExportAsTextAsync(Stream to, ExpositionFormat format = ExpositionFormat.PrometheusText, CancellationToken cancel = default) { if (to == null) throw new ArgumentNullException(nameof(to)); diff --git a/Prometheus/ExpositionFormats.cs b/Prometheus/ExpositionFormats.cs index b3e0b78a..02d811a5 100644 --- a/Prometheus/ExpositionFormats.cs +++ b/Prometheus/ExpositionFormats.cs @@ -5,7 +5,7 @@ public enum ExpositionFormat /// /// The traditional prometheus exposition format. /// - Text, + PrometheusText, /// /// The OpenMetrics text exposition format /// diff --git a/Prometheus/ICollectorRegistry.cs b/Prometheus/ICollectorRegistry.cs index 0ab8d779..24163f4c 100644 --- a/Prometheus/ICollectorRegistry.cs +++ b/Prometheus/ICollectorRegistry.cs @@ -12,6 +12,6 @@ public interface ICollectorRegistry IEnumerable> StaticLabels { get; } void SetStaticLabels(IDictionary labels); - Task CollectAndExportAsTextAsync(Stream to, ExpositionFormat format = ExpositionFormat.Text, CancellationToken cancel = default); + Task CollectAndExportAsTextAsync(Stream to, ExpositionFormat format = ExpositionFormat.PrometheusText, CancellationToken cancel = default); } } diff --git a/Prometheus/TextSerializer.cs b/Prometheus/TextSerializer.cs index 6304ed6e..8866b478 100644 --- a/Prometheus/TextSerializer.cs +++ b/Prometheus/TextSerializer.cs @@ -29,7 +29,7 @@ internal sealed class TextSerializer : IMetricsSerializer private static readonly char[] DotEChar = { '.', 'e' }; - public TextSerializer(Stream stream, ExpositionFormat fmt = ExpositionFormat.Text) + public TextSerializer(Stream stream, ExpositionFormat fmt = ExpositionFormat.PrometheusText) { _fmt = fmt; _stream = new Lazy(() => stream); @@ -37,7 +37,7 @@ public TextSerializer(Stream stream, ExpositionFormat fmt = ExpositionFormat.Tex // Enables delay-loading of the stream, because touching stream in HTTP handler triggers some behavior. public TextSerializer(Func streamFactory, - ExpositionFormat fmt = ExpositionFormat.Text) + ExpositionFormat fmt = ExpositionFormat.PrometheusText) { _fmt = fmt; _stream = new Lazy(streamFactory); diff --git a/Tests.NetCore/HistogramTests.cs b/Tests.NetCore/HistogramTests.cs index e5fb2869..4fd454a6 100644 --- a/Tests.NetCore/HistogramTests.cs +++ b/Tests.NetCore/HistogramTests.cs @@ -19,7 +19,7 @@ public void ObserveExemplarDuplicateKeys() var histogram = factory.CreateHistogram("xxx", ""); histogram.Observe(1, Exemplar.Pair("traceID", "123"), Exemplar.Pair("traceID", "1")); } - + [TestMethod] [ExpectedException(typeof(ArgumentException))] public void ObserveExemplarTooManyRunes() @@ -31,12 +31,46 @@ public void ObserveExemplarTooManyRunes() var key2 = "0123456789" + "0123456789" + "0123456789" + "0123456789" + "0123456780"; // 50 var val1 = "01234567890123"; // 14 var val2 = "012345678901234"; // 15 (= 129) - + var histogram = factory.CreateHistogram("xxx", ""); histogram.Observe(1, Exemplar.Pair(key1, val1), Exemplar.Pair(key2, val2)); } - - + + [TestMethod] + public async Task ObserveExemplar_OnlyAddsExemplarToSingleBucket() + { + var registry = Metrics.NewCustomRegistry(); + var factory = Metrics.WithCustomRegistry(registry); + + var histogram = factory.CreateHistogram("xxx", "", new HistogramConfiguration + { + Buckets = new[] { 1.0, 2.0, 3.0 } + }); + + var canary = "my_value_354867398"; + var exemplar = Exemplar.Pair("my_key", canary); + + // We expect the exemplar to be added to the specific bucket that the value falls into, not every bucket that gets incremented. + // In this case, it would be the 2.0 bucket that the exemplar belongs to (the lowest-valued bucket that gets incremented). + // OpenMetrics says "Exemplars SHOULD be put into the bucket with the highest value." but that seems backwards - it would mean + // that every exemplar goes into the +Inf bucket, as that is always the highest value of an incremented bucket. + histogram.Observe(1.9, exemplar); + + var serialized = await registry.CollectAndSerializeToStringAsync(ExpositionFormat.OpenMetricsText); + + // We expect to see it there. + StringAssert.Contains(serialized, canary); + + // And we expect to see it only once. + var firstIndex = serialized.IndexOf(canary); + var lastIndex = serialized.LastIndexOf(canary); + Assert.AreEqual(firstIndex, lastIndex); + + // And we expect to see it on the correct line. + var expectedLine = $@"xxx_bucket{{le=""2.0""}} 1.0 # {{my_key=""{canary}""}}"; + StringAssert.Contains(serialized, expectedLine); + } + [TestMethod] public async Task Observe_IncrementsCorrectBucketsAndCountAndSum() { @@ -60,11 +94,11 @@ public async Task Observe_IncrementsCorrectBucketsAndCountAndSum() // 2.0 // 3.0 // +inf - await serializer.Received().WriteMetricPointAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(),2.0,Arg.Any(), Arg.Any()); - await serializer.Received().WriteMetricPointAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(),0,Arg.Any(), Arg.Any()); - await serializer.Received().WriteMetricPointAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(),1,Arg.Any(), Arg.Any()); - await serializer.Received().WriteMetricPointAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(),2,Arg.Any(), Arg.Any()); - await serializer.Received().WriteMetricPointAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(),2,Arg.Any(), Arg.Any()); + await serializer.Received().WriteMetricPointAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), 2.0, Arg.Any(), Arg.Any()); + await serializer.Received().WriteMetricPointAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), 0, Arg.Any(), Arg.Any()); + await serializer.Received().WriteMetricPointAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), 1, Arg.Any(), Arg.Any()); + await serializer.Received().WriteMetricPointAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), 2, Arg.Any(), Arg.Any()); + await serializer.Received().WriteMetricPointAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), 2, Arg.Any(), Arg.Any()); } [TestMethod] diff --git a/Tests.NetCore/TestExtensions.cs b/Tests.NetCore/TestExtensions.cs index 2442c8e6..34ab4c04 100644 --- a/Tests.NetCore/TestExtensions.cs +++ b/Tests.NetCore/TestExtensions.cs @@ -2,15 +2,14 @@ using System.Text; using System.Threading.Tasks; -namespace Prometheus.Tests +namespace Prometheus.Tests; + +internal static class TestExtensions { - internal static class TestExtensions + public static async Task CollectAndSerializeToStringAsync(this CollectorRegistry registry, ExpositionFormat expositionFormat = ExpositionFormat.PrometheusText) { - public static async Task CollectAndSerializeToStringAsync(this CollectorRegistry registry) - { - var buffer = new MemoryStream(); - await registry.CollectAndExportAsTextAsync(buffer); - return Encoding.UTF8.GetString(buffer.ToArray()); - } + var buffer = new MemoryStream(); + await registry.CollectAndExportAsTextAsync(buffer, expositionFormat); + return Encoding.UTF8.GetString(buffer.ToArray()); } } diff --git a/Tests.NetCore/TextSerializerTests.cs b/Tests.NetCore/TextSerializerTests.cs index d130c8a3..6b009f6c 100644 --- a/Tests.NetCore/TextSerializerTests.cs +++ b/Tests.NetCore/TextSerializerTests.cs @@ -291,7 +291,7 @@ public static async Task RunOpenMetrics(Action register return await Run(register, ExpositionFormat.OpenMetricsText); } - public static async Task Run(Action register, ExpositionFormat format = ExpositionFormat.Text) + public static async Task Run(Action register, ExpositionFormat format = ExpositionFormat.PrometheusText) { var registry = Metrics.NewCustomRegistry(); var factory = Metrics.WithCustomRegistry(registry); From b1621198b4ec4c06b3be875dcf650381caa272d4 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Sun, 1 Jan 2023 09:46:51 +0200 Subject: [PATCH 042/230] Ensure that metrics from the same family are serialized as part of the same family, even if they differ by instance label names --- Prometheus/CollectorFamily.cs | 3 +- Prometheus/CollectorFamilyIdentity.cs | 70 ------ Prometheus/CollectorIdentity.cs | 62 +++++ Prometheus/CollectorRegistry.cs | 20 +- Prometheus/ManagedLifetimeMetricFactory.cs | 265 ++++++++++---------- Prometheus/ManagedLifetimeMetricIdentity.cs | 61 +++++ Tests.NetCore/MetricsTests.cs | 22 +- 7 files changed, 280 insertions(+), 223 deletions(-) delete mode 100644 Prometheus/CollectorFamilyIdentity.cs create mode 100644 Prometheus/CollectorIdentity.cs create mode 100644 Prometheus/ManagedLifetimeMetricIdentity.cs diff --git a/Prometheus/CollectorFamily.cs b/Prometheus/CollectorFamily.cs index 1905d7a1..49c87f83 100644 --- a/Prometheus/CollectorFamily.cs +++ b/Prometheus/CollectorFamily.cs @@ -6,8 +6,7 @@ internal sealed class CollectorFamily { public Type CollectorType { get; } - // Different collectors in the same family are differentiated by different sets of static labels. - public ConcurrentDictionary Collectors { get; } = new(); + public ConcurrentDictionary Collectors { get; } = new(); public CollectorFamily(Type collectorType) { diff --git a/Prometheus/CollectorFamilyIdentity.cs b/Prometheus/CollectorFamilyIdentity.cs deleted file mode 100644 index 81007974..00000000 --- a/Prometheus/CollectorFamilyIdentity.cs +++ /dev/null @@ -1,70 +0,0 @@ -namespace Prometheus; - -/// -/// Represents the values that make up a single collector family's unique identity. -/// Used during collector registration to identify which family each collector belongs to. -/// -/// If these values match an existing collector family, we will reuse it (or throw if metadata mismatches). -/// If these values do not match any existing collector family, we will create a new collector family. -/// -internal struct CollectorFamilyIdentity : IEquatable -{ - public readonly string Name; - public readonly StringSequence InstanceLabelNames; - public readonly StringSequence StaticLabelNames; - - private readonly int _hashCode; - - public CollectorFamilyIdentity(string name, StringSequence instanceLabelNames, StringSequence staticLabelNames) - { - Name = name; - InstanceLabelNames = instanceLabelNames; - StaticLabelNames = staticLabelNames; - - _hashCode = CalculateHashCode(name, instanceLabelNames, staticLabelNames); - } - - public bool Equals(CollectorFamilyIdentity other) - { - if (!string.Equals(Name, other.Name, StringComparison.Ordinal)) - return false; - - if (_hashCode != other._hashCode) - return false; - - if (InstanceLabelNames.Length != other.InstanceLabelNames.Length) - return false; - - if (!InstanceLabelNames.Equals(other.InstanceLabelNames)) - return false; - - if (!StaticLabelNames.Equals(other.StaticLabelNames)) - return false; - - return true; - } - - public override int GetHashCode() - { - return _hashCode; - } - - private static int CalculateHashCode(string name, StringSequence instanceLabelNames, StringSequence staticLabelNames) - { - unchecked - { - int hashCode = 0; - - hashCode ^= name.GetHashCode() * 31; - hashCode ^= instanceLabelNames.GetHashCode() * 397; - hashCode ^= staticLabelNames.GetHashCode() * 397; - - return hashCode; - } - } - - public override string ToString() - { - return $"{Name}{{{InstanceLabelNames.Length + StaticLabelNames.Length}}}"; - } -} diff --git a/Prometheus/CollectorIdentity.cs b/Prometheus/CollectorIdentity.cs new file mode 100644 index 00000000..c6cd75c1 --- /dev/null +++ b/Prometheus/CollectorIdentity.cs @@ -0,0 +1,62 @@ +namespace Prometheus; + +/// +/// Uniquely identifies a specific collector within a family. Different collectors are used for different label combinations. +/// * Any difference in static labels (keys or values) means it is a different collector. +/// * Any difference in the names of instance labels means it is a different collector. +/// +internal struct CollectorIdentity : IEquatable +{ + public readonly StringSequence InstanceLabelNames; + public readonly LabelSequence StaticLabels; + + private readonly int _hashCode; + + public CollectorIdentity(StringSequence instanceLabelNames, LabelSequence staticLabels) + { + InstanceLabelNames = instanceLabelNames; + StaticLabels = staticLabels; + + _hashCode = CalculateHashCode(instanceLabelNames, staticLabels); + } + + public bool Equals(CollectorIdentity other) + { + if (_hashCode != other._hashCode) + return false; + + if (InstanceLabelNames.Length != other.InstanceLabelNames.Length) + return false; + + if (!InstanceLabelNames.Equals(other.InstanceLabelNames)) + return false; + + if (!StaticLabels.Equals(other.StaticLabels)) + return false; + + return true; + } + + public override int GetHashCode() + { + return _hashCode; + } + + private static int CalculateHashCode(StringSequence instanceLabelNames, LabelSequence staticLabels) + { + unchecked + { + int hashCode = 0; + + hashCode ^= instanceLabelNames.GetHashCode() * 397; + hashCode ^= staticLabels.GetHashCode() * 397; + + return hashCode; + } + } + + public override string ToString() + { + return $"{_hashCode}{{{InstanceLabelNames.Length} + {StaticLabels.Length}}}"; + } +} \ No newline at end of file diff --git a/Prometheus/CollectorRegistry.cs b/Prometheus/CollectorRegistry.cs index 45423e29..fac7ac32 100644 --- a/Prometheus/CollectorRegistry.cs +++ b/Prometheus/CollectorRegistry.cs @@ -1,5 +1,4 @@ -using Microsoft.Extensions.Configuration; -using System.Collections.Concurrent; +using System.Collections.Concurrent; using System.Diagnostics; namespace Prometheus; @@ -172,7 +171,7 @@ public CollectorInitializer(CreateInstanceDelegate createInstance, string name, _configuration = configuration; } - public TCollector CreateInstance(LabelSequence _) => _createInstance(_name, _help, _instanceLabelNames, _staticLabels, _configuration); + public TCollector CreateInstance(CollectorIdentity _) => _createInstance(_name, _help, _instanceLabelNames, _staticLabels, _configuration); public delegate TCollector CreateInstanceDelegate(string name, string help, StringSequence instanceLabelNames, LabelSequence staticLabels, TConfiguration configuration); } @@ -190,7 +189,7 @@ internal TCollector GetOrAdd(in CollectorInitializer var family = GetOrAddCollectorFamily(initializer); - var collectorIdentity = initializer.StaticLabels; + var collectorIdentity = new CollectorIdentity(initializer.InstanceLabelNames, initializer.StaticLabels); if (family.Collectors.TryGetValue(collectorIdentity, out var existing)) return (TCollector)existing; @@ -202,28 +201,27 @@ private CollectorFamily GetOrAddCollectorFamily(in C where TCollector : Collector where TConfiguration : MetricConfiguration { - var familyIdentity = new CollectorFamilyIdentity(initializer.Name, initializer.InstanceLabelNames, initializer.StaticLabels.Names); - static CollectorFamily ValidateFamily(CollectorFamily candidate) { // We either created a new collector family or found one with a matching identity. // We do some basic validation here to avoid silly API usage mistakes. if (candidate.CollectorType != typeof(TCollector)) - throw new InvalidOperationException("Collector of a different type with the same identity is already registered."); + throw new InvalidOperationException("Collector of a different type with the same name is already registered."); return candidate; } - if (_families.TryGetValue(familyIdentity, out var existing)) + if (_families.TryGetValue(initializer.Name, out var existing)) return ValidateFamily(existing); - var collector = _families.GetOrAdd(familyIdentity, new CollectorFamily(typeof(TCollector))); + var collector = _families.GetOrAdd(initializer.Name, new CollectorFamily(typeof(TCollector))); return ValidateFamily(collector); } - // Each collector family has an identity and any number of collectors within. - private readonly ConcurrentDictionary _families = new(); + // Each collector family has an identity (the base name of the metric, in Prometheus format) and any number of collectors within. + // Different collectors in the same family may have different sets of labels (static and instance) depending on how they were created. + private readonly ConcurrentDictionary _families = new(); internal void SetBeforeFirstCollectCallback(Action a) { diff --git a/Prometheus/ManagedLifetimeMetricFactory.cs b/Prometheus/ManagedLifetimeMetricFactory.cs index 570a6774..80f2e1a1 100644 --- a/Prometheus/ManagedLifetimeMetricFactory.cs +++ b/Prometheus/ManagedLifetimeMetricFactory.cs @@ -1,170 +1,169 @@ using System.Collections.Concurrent; -namespace Prometheus +namespace Prometheus; + +internal sealed class ManagedLifetimeMetricFactory : IManagedLifetimeMetricFactory { - internal sealed class ManagedLifetimeMetricFactory : IManagedLifetimeMetricFactory + public ManagedLifetimeMetricFactory(MetricFactory inner, TimeSpan expiresAfter) { - public ManagedLifetimeMetricFactory(MetricFactory inner, TimeSpan expiresAfter) - { - // .NET Framework requires the timer to fit in int.MaxValue and we will have hidden failures to expire if it does not. - // For simplicity, let's just limit it to 1 day, which should be enough for anyone. - if (expiresAfter > TimeSpan.FromDays(1)) - throw new ArgumentOutOfRangeException(nameof(expiresAfter), "Automatic metric expiration time must be no greater than 1 day."); + // .NET Framework requires the timer to fit in int.MaxValue and we will have hidden failures to expire if it does not. + // For simplicity, let's just limit it to 1 day, which should be enough for anyone. + if (expiresAfter > TimeSpan.FromDays(1)) + throw new ArgumentOutOfRangeException(nameof(expiresAfter), "Automatic metric expiration time must be no greater than 1 day."); - _inner = inner; - _expiresAfter = expiresAfter; - } + _inner = inner; + _expiresAfter = expiresAfter; + } - private readonly MetricFactory _inner; - private readonly TimeSpan _expiresAfter; + private readonly MetricFactory _inner; + private readonly TimeSpan _expiresAfter; - public IManagedLifetimeMetricHandle CreateCounter(string name, string help, string[] instanceLabelNames, CounterConfiguration? configuration = null) - { - var identity = new CollectorFamilyIdentity(name, StringSequence.From(instanceLabelNames), StringSequence.Empty); + public IManagedLifetimeMetricHandle CreateCounter(string name, string help, string[] instanceLabelNames, CounterConfiguration? configuration = null) + { + var identity = new ManagedLifetimeMetricIdentity(name, StringSequence.From(instanceLabelNames)); - // Let's be optimistic and assume that in the typical case, the metric will already exist. - if (_counters.TryGetValue(identity, out var existing)) - return existing; + // Let's be optimistic and assume that in the typical case, the metric will already exist. + if (_counters.TryGetValue(identity, out var existing)) + return existing; - var initializer = new CounterInitializer(_inner, _expiresAfter, help, configuration); - return _counters.GetOrAdd(identity, initializer.CreateInstance); - } + var initializer = new CounterInitializer(_inner, _expiresAfter, help, configuration); + return _counters.GetOrAdd(identity, initializer.CreateInstance); + } - public IManagedLifetimeMetricHandle CreateGauge(string name, string help, string[] instanceLabelNames, GaugeConfiguration? configuration = null) - { - var identity = new CollectorFamilyIdentity(name, StringSequence.From(instanceLabelNames), StringSequence.Empty); + public IManagedLifetimeMetricHandle CreateGauge(string name, string help, string[] instanceLabelNames, GaugeConfiguration? configuration = null) + { + var identity = new ManagedLifetimeMetricIdentity(name, StringSequence.From(instanceLabelNames)); - // Let's be optimistic and assume that in the typical case, the metric will already exist. - if (_gauges.TryGetValue(identity, out var existing)) - return existing; + // Let's be optimistic and assume that in the typical case, the metric will already exist. + if (_gauges.TryGetValue(identity, out var existing)) + return existing; - var initializer = new GaugeInitializer(_inner, _expiresAfter, help, configuration); - return _gauges.GetOrAdd(identity, initializer.CreateInstance); - } + var initializer = new GaugeInitializer(_inner, _expiresAfter, help, configuration); + return _gauges.GetOrAdd(identity, initializer.CreateInstance); + } - public IManagedLifetimeMetricHandle CreateHistogram(string name, string help, string[] instanceLabelNames, HistogramConfiguration? configuration = null) - { - var identity = new CollectorFamilyIdentity(name, StringSequence.From(instanceLabelNames), StringSequence.Empty); + public IManagedLifetimeMetricHandle CreateHistogram(string name, string help, string[] instanceLabelNames, HistogramConfiguration? configuration = null) + { + var identity = new ManagedLifetimeMetricIdentity(name, StringSequence.From(instanceLabelNames)); + + // Let's be optimistic and assume that in the typical case, the metric will already exist. + if (_histograms.TryGetValue(identity, out var existing)) + return existing; + + var initializer = new HistogramInitializer(_inner, _expiresAfter, help, configuration); + return _histograms.GetOrAdd(identity, initializer.CreateInstance); + } + + public IManagedLifetimeMetricHandle CreateSummary(string name, string help, string[] instanceLabelNames, SummaryConfiguration? configuration = null) + { + var identity = new ManagedLifetimeMetricIdentity(name, StringSequence.From(instanceLabelNames)); - // Let's be optimistic and assume that in the typical case, the metric will already exist. - if (_histograms.TryGetValue(identity, out var existing)) - return existing; + // Let's be optimistic and assume that in the typical case, the metric will already exist. + if (_summaries.TryGetValue(identity, out var existing)) + return existing; - var initializer = new HistogramInitializer(_inner, _expiresAfter, help, configuration); - return _histograms.GetOrAdd(identity, initializer.CreateInstance); + var initializer = new SummaryInitializer(_inner, _expiresAfter, help, configuration); + return _summaries.GetOrAdd(identity, initializer.CreateInstance); + } + + /// + /// Gets all the existing label names predefined either in the factory or in the registry. + /// + internal StringSequence GetAllStaticLabelNames() => _inner.GetAllStaticLabelNames(); + + // We need to reuse existing instances of lifetime-managed metrics because the user might not want to cache it. + // This somewhat duplicates the metric identity tracking logic in CollectorRegistry but this is intentional, as we really do need to do this work on two layers. + // We never remove collectors from here as long as the factory is alive. The expectation is that there is not an unbounded set of label names, so this set is non-gigantic. + private readonly ConcurrentDictionary _counters = new(); + private readonly ConcurrentDictionary _gauges = new(); + private readonly ConcurrentDictionary _histograms = new(); + private readonly ConcurrentDictionary _summaries = new(); + + private readonly struct CounterInitializer + { + public readonly MetricFactory Inner; + public readonly TimeSpan ExpiresAfter; + public readonly string Help; + public readonly CounterConfiguration? Configuration; + + public CounterInitializer(MetricFactory inner, TimeSpan expiresAfter, string help, CounterConfiguration? configuration) + { + Inner = inner; + ExpiresAfter = expiresAfter; + Help = help; + Configuration = configuration; } - public IManagedLifetimeMetricHandle CreateSummary(string name, string help, string[] instanceLabelNames, SummaryConfiguration? configuration = null) + public ManagedLifetimeCounter CreateInstance(ManagedLifetimeMetricIdentity identity) { - var identity = new CollectorFamilyIdentity(name, StringSequence.From(instanceLabelNames), StringSequence.Empty); + var metric = Inner.CreateCounter(identity.MetricFamilyName, Help, identity.InstanceLabelNames, Configuration); + return new ManagedLifetimeCounter(metric, ExpiresAfter); + } + } - // Let's be optimistic and assume that in the typical case, the metric will already exist. - if (_summaries.TryGetValue(identity, out var existing)) - return existing; + private readonly struct GaugeInitializer + { + public readonly MetricFactory Inner; + public readonly TimeSpan ExpiresAfter; + public readonly string Help; + public readonly GaugeConfiguration? Configuration; - var initializer = new SummaryInitializer(_inner, _expiresAfter, help, configuration); - return _summaries.GetOrAdd(identity, initializer.CreateInstance); + public GaugeInitializer(MetricFactory inner, TimeSpan expiresAfter, string help, GaugeConfiguration? configuration) + { + Inner = inner; + ExpiresAfter = expiresAfter; + Help = help; + Configuration = configuration; } - /// - /// Gets all the existing label names predefined either in the factory or in the registry. - /// - internal StringSequence GetAllStaticLabelNames() => _inner.GetAllStaticLabelNames(); + public ManagedLifetimeGauge CreateInstance(ManagedLifetimeMetricIdentity identity) + { + var metric = Inner.CreateGauge(identity.MetricFamilyName, Help, identity.InstanceLabelNames, Configuration); + return new ManagedLifetimeGauge(metric, ExpiresAfter); + } + } - // We need to reuse existing instances of lifetime-managed metrics because the user might not want to cache it. - // This somewhat duplicates the metric identity tracking logic in CollectorRegistry but this is intentional, as we really do need to do this work on two layers. - // We never remove collectors from here as long as the factory is alive. The expectation is that there is not an unbounded set of label names, so this set is non-gigantic. - private readonly ConcurrentDictionary _counters = new(); - private readonly ConcurrentDictionary _gauges = new(); - private readonly ConcurrentDictionary _histograms = new(); - private readonly ConcurrentDictionary _summaries = new(); + private readonly struct HistogramInitializer + { + public readonly MetricFactory Inner; + public readonly TimeSpan ExpiresAfter; + public readonly string Help; + public readonly HistogramConfiguration? Configuration; - private readonly struct CounterInitializer + public HistogramInitializer(MetricFactory inner, TimeSpan expiresAfter, string help, HistogramConfiguration? configuration) { - public readonly MetricFactory Inner; - public readonly TimeSpan ExpiresAfter; - public readonly string Help; - public readonly CounterConfiguration? Configuration; - - public CounterInitializer(MetricFactory inner, TimeSpan expiresAfter, string help, CounterConfiguration? configuration) - { - Inner = inner; - ExpiresAfter = expiresAfter; - Help = help; - Configuration = configuration; - } - - public ManagedLifetimeCounter CreateInstance(CollectorFamilyIdentity identity) - { - var metric = Inner.CreateCounter(identity.Name, Help, identity.InstanceLabelNames, Configuration); - return new ManagedLifetimeCounter(metric, ExpiresAfter); - } + Inner = inner; + ExpiresAfter = expiresAfter; + Help = help; + Configuration = configuration; } - private readonly struct GaugeInitializer + public ManagedLifetimeHistogram CreateInstance(ManagedLifetimeMetricIdentity identity) { - public readonly MetricFactory Inner; - public readonly TimeSpan ExpiresAfter; - public readonly string Help; - public readonly GaugeConfiguration? Configuration; - - public GaugeInitializer(MetricFactory inner, TimeSpan expiresAfter, string help, GaugeConfiguration? configuration) - { - Inner = inner; - ExpiresAfter = expiresAfter; - Help = help; - Configuration = configuration; - } - - public ManagedLifetimeGauge CreateInstance(CollectorFamilyIdentity identity) - { - var metric = Inner.CreateGauge(identity.Name, Help, identity.InstanceLabelNames, Configuration); - return new ManagedLifetimeGauge(metric, ExpiresAfter); - } + var metric = Inner.CreateHistogram(identity.MetricFamilyName, Help, identity.InstanceLabelNames, Configuration); + return new ManagedLifetimeHistogram(metric, ExpiresAfter); } + } + + private readonly struct SummaryInitializer + { + public readonly MetricFactory Inner; + public readonly TimeSpan ExpiresAfter; + public readonly string Help; + public readonly SummaryConfiguration? Configuration; - private readonly struct HistogramInitializer + public SummaryInitializer(MetricFactory inner, TimeSpan expiresAfter, string help, SummaryConfiguration? configuration) { - public readonly MetricFactory Inner; - public readonly TimeSpan ExpiresAfter; - public readonly string Help; - public readonly HistogramConfiguration? Configuration; - - public HistogramInitializer(MetricFactory inner, TimeSpan expiresAfter, string help, HistogramConfiguration? configuration) - { - Inner = inner; - ExpiresAfter = expiresAfter; - Help = help; - Configuration = configuration; - } - - public ManagedLifetimeHistogram CreateInstance(CollectorFamilyIdentity identity) - { - var metric = Inner.CreateHistogram(identity.Name, Help, identity.InstanceLabelNames, Configuration); - return new ManagedLifetimeHistogram(metric, ExpiresAfter); - } + Inner = inner; + ExpiresAfter = expiresAfter; + Help = help; + Configuration = configuration; } - private readonly struct SummaryInitializer + public ManagedLifetimeSummary CreateInstance(ManagedLifetimeMetricIdentity identity) { - public readonly MetricFactory Inner; - public readonly TimeSpan ExpiresAfter; - public readonly string Help; - public readonly SummaryConfiguration? Configuration; - - public SummaryInitializer(MetricFactory inner, TimeSpan expiresAfter, string help, SummaryConfiguration? configuration) - { - Inner = inner; - ExpiresAfter = expiresAfter; - Help = help; - Configuration = configuration; - } - - public ManagedLifetimeSummary CreateInstance(CollectorFamilyIdentity identity) - { - var metric = Inner.CreateSummary(identity.Name, Help, identity.InstanceLabelNames, Configuration); - return new ManagedLifetimeSummary(metric, ExpiresAfter); - } + var metric = Inner.CreateSummary(identity.MetricFamilyName, Help, identity.InstanceLabelNames, Configuration); + return new ManagedLifetimeSummary(metric, ExpiresAfter); } } } diff --git a/Prometheus/ManagedLifetimeMetricIdentity.cs b/Prometheus/ManagedLifetimeMetricIdentity.cs new file mode 100644 index 00000000..3e9b1017 --- /dev/null +++ b/Prometheus/ManagedLifetimeMetricIdentity.cs @@ -0,0 +1,61 @@ +namespace Prometheus; + +/// +/// For managed lifetime metrics, we just want to uniquely identify metric instances so we can cache them. +/// We differentiate by the family name + the set of unique instance label names applied to the instance. +/// +/// Managed lifetime metrics are not differentiated by static labels because the static labels are applied +/// in a lower layer (the underlying MetricFactory) and cannot differ within a single ManagedLifetimeMetricFactory. +/// +internal struct ManagedLifetimeMetricIdentity : IEquatable +{ + public readonly string MetricFamilyName; + public readonly StringSequence InstanceLabelNames; + + private readonly int _hashCode; + + public ManagedLifetimeMetricIdentity(string metricFamilyName, StringSequence instanceLabelNames) + { + MetricFamilyName = metricFamilyName; + InstanceLabelNames = instanceLabelNames; + + _hashCode = CalculateHashCode(metricFamilyName, instanceLabelNames); + } + + public bool Equals(ManagedLifetimeMetricIdentity other) + { + if (_hashCode != other._hashCode) + return false; + + if (!string.Equals(MetricFamilyName, other.MetricFamilyName, StringComparison.Ordinal)) + return false; + + if (!InstanceLabelNames.Equals(other.InstanceLabelNames)) + return false; + + return true; + } + + public override int GetHashCode() + { + return _hashCode; + } + + private static int CalculateHashCode(string metricFamilyName, StringSequence instanceLabelNames) + { + unchecked + { + int hashCode = 0; + + hashCode ^= metricFamilyName.GetHashCode() * 997; + hashCode ^= instanceLabelNames.GetHashCode() * 397; + + return hashCode; + } + } + + public override string ToString() + { + return $"{MetricFamilyName}{InstanceLabelNames}"; + } +} diff --git a/Tests.NetCore/MetricsTests.cs b/Tests.NetCore/MetricsTests.cs index 53df00e6..a8f8a7f2 100644 --- a/Tests.NetCore/MetricsTests.cs +++ b/Tests.NetCore/MetricsTests.cs @@ -323,6 +323,14 @@ public async Task CreateMetric_WithSameMetadataButDifferentLabels_CreatesMetric( StringAssert.Contains(serialized, canary2.ToString()); StringAssert.Contains(serialized, canary3.ToString()); StringAssert.Contains(serialized, canary4.ToString()); + + // We expect them all to be serialized as different metric instances in the same metric family. + var familyDeclaration = "# TYPE name1 gauge"; + StringAssert.Contains(serialized, familyDeclaration); + var firstIndex = serialized.IndexOf(familyDeclaration); + var lastIndex = serialized.LastIndexOf(familyDeclaration); + + Assert.AreEqual(firstIndex, lastIndex); } [TestMethod] @@ -336,7 +344,7 @@ public void cannot_create_metrics_with_the_same_name_and_labels_but_different_ty } catch (InvalidOperationException e) { - Assert.AreEqual("Collector of a different type with the same identity is already registered.", e.Message); + Assert.AreEqual("Collector of a different type with the same name is already registered.", e.Message); } } @@ -357,13 +365,13 @@ public void metric_names() public void label_names() { Assert.ThrowsException(() => _metrics.CreateGauge("a", "help1", "my-label")); - Assert.ThrowsException(() => _metrics.CreateGauge("a", "help1", "my!label")); - Assert.ThrowsException(() => _metrics.CreateGauge("a", "help1", "my%label")); - Assert.ThrowsException(() => _metrics.CreateHistogram("a", "help1", "le")); - Assert.ThrowsException(() => _metrics.CreateHistogram("a", "help1", "my:label")); - _metrics.CreateGauge("b", "help1", "good_name"); + Assert.ThrowsException(() => _metrics.CreateGauge("b", "help1", "my!label")); + Assert.ThrowsException(() => _metrics.CreateGauge("c", "help1", "my%label")); + Assert.ThrowsException(() => _metrics.CreateHistogram("d", "help1", "le")); + Assert.ThrowsException(() => _metrics.CreateHistogram("e", "help1", "my:label")); + _metrics.CreateGauge("f", "help1", "good_name"); - Assert.ThrowsException(() => _metrics.CreateGauge("c", "help1", "__reserved")); + Assert.ThrowsException(() => _metrics.CreateGauge("g", "help1", "__reserved")); } [TestMethod] From 630935fd5ee9f525a0f499e8e8989ff176ed9aa0 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Sun, 1 Jan 2023 10:43:58 +0200 Subject: [PATCH 043/230] Export integer-valued metric points as integers --- Prometheus/IMetricsSerializer.cs | 8 +- Prometheus/Summary.cs | 4 +- Prometheus/TextSerializer.cs | 99 ++++++++++++++------ Tests.NetCore/HistogramTests.cs | 20 ++-- Tests.NetCore/MetricInitializationTests.cs | 52 +++++++--- Tests.NetCore/MetricsTests.cs | 25 +++-- Tests.NetCore/Tests.NetCore.csproj | 6 +- Tests.NetCore/TextSerializerTests.cs | 20 ++-- Tests.NetFramework/packages.config | 4 +- Tests.NetFramework/tests.netframework.csproj | 12 +-- 10 files changed, 163 insertions(+), 87 deletions(-) diff --git a/Prometheus/IMetricsSerializer.cs b/Prometheus/IMetricsSerializer.cs index 64da8480..eef010b1 100644 --- a/Prometheus/IMetricsSerializer.cs +++ b/Prometheus/IMetricsSerializer.cs @@ -14,11 +14,17 @@ Task WriteFamilyDeclarationAsync(string name, byte[] nameBytes, byte[] helpBytes byte[] typeBytes, CancellationToken cancel); /// - /// Writes out a single metric point + /// Writes out a single metric point with a floating point value. /// Task WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, CanonicalLabel canonicalLabel, CancellationToken cancel, double value, ObservedExemplar exemplar, byte[]? suffix = null); + /// + /// Writes out a single metric point with an integer value. + /// + Task WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, CanonicalLabel canonicalLabel, + CancellationToken cancel, long value, ObservedExemplar exemplar, byte[]? suffix = null); + /// /// Writes out terminal lines /// diff --git a/Prometheus/Summary.cs b/Prometheus/Summary.cs index ecd653f8..f017498f 100644 --- a/Prometheus/Summary.cs +++ b/Prometheus/Summary.cs @@ -117,7 +117,7 @@ private protected override async Task CollectAndSerializeImplAsync(IMetricsSeria var now = DateTime.UtcNow; - double count; + long count; double sum; var values = new List<(double quantile, double value)>(_objectives.Count); @@ -178,7 +178,7 @@ await serializer.WriteMetricPointAsync( private IReadOnlyList _objectives = new List(); private double[] _sortedObjectives; private double _sum; - private ulong _count; + private long _count; private SampleBuffer _hotBuf; private SampleBuffer _coldBuf; private QuantileStream[] _streams; diff --git a/Prometheus/TextSerializer.cs b/Prometheus/TextSerializer.cs index 8866b478..230f3ba1 100644 --- a/Prometheus/TextSerializer.cs +++ b/Prometheus/TextSerializer.cs @@ -19,9 +19,13 @@ internal sealed class TextSerializer : IMetricsSerializer private static readonly byte[] PositiveInfinity = PrometheusConstants.ExportEncoding.GetBytes("+Inf"); private static readonly byte[] NegativeInfinity = PrometheusConstants.ExportEncoding.GetBytes("-Inf"); private static readonly byte[] NotANumber = PrometheusConstants.ExportEncoding.GetBytes("NaN"); - private static readonly byte[] PositiveOne = PrometheusConstants.ExportEncoding.GetBytes("1.0"); - private static readonly byte[] Zero = PrometheusConstants.ExportEncoding.GetBytes("0.0"); - private static readonly byte[] NegativeOne = PrometheusConstants.ExportEncoding.GetBytes("-1.0"); + private static readonly byte[] DotZero = PrometheusConstants.ExportEncoding.GetBytes(".0"); + private static readonly byte[] FloatPositiveOne = PrometheusConstants.ExportEncoding.GetBytes("1.0"); + private static readonly byte[] FloatZero = PrometheusConstants.ExportEncoding.GetBytes("0.0"); + private static readonly byte[] FloatNegativeOne = PrometheusConstants.ExportEncoding.GetBytes("-1.0"); + private static readonly byte[] IntPositiveOne = PrometheusConstants.ExportEncoding.GetBytes("1"); + private static readonly byte[] IntZero = PrometheusConstants.ExportEncoding.GetBytes("0"); + private static readonly byte[] IntNegativeOne = PrometheusConstants.ExportEncoding.GetBytes("-1"); private static readonly byte[] EofNewLine = PrometheusConstants.ExportEncoding.GetBytes("# EOF\n"); private static readonly byte[] HashHelpSpace = PrometheusConstants.ExportEncoding.GetBytes("# HELP "); private static readonly byte[] NewlineHashTypeSpace = PrometheusConstants.ExportEncoding.GetBytes("\n# TYPE "); @@ -31,7 +35,7 @@ internal sealed class TextSerializer : IMetricsSerializer public TextSerializer(Stream stream, ExpositionFormat fmt = ExpositionFormat.PrometheusText) { - _fmt = fmt; + _expositionFormat = fmt; _stream = new Lazy(() => stream); } @@ -39,7 +43,7 @@ public TextSerializer(Stream stream, ExpositionFormat fmt = ExpositionFormat.Pro public TextSerializer(Func streamFactory, ExpositionFormat fmt = ExpositionFormat.PrometheusText) { - _fmt = fmt; + _expositionFormat = fmt; _stream = new Lazy(streamFactory); } @@ -58,7 +62,7 @@ public async Task WriteFamilyDeclarationAsync(string name, byte[] nameBytes, byt byte[] typeBytes, CancellationToken cancel) { var nameLen = nameBytes.Length; - if (_fmt == ExpositionFormat.OpenMetricsText && type == MetricType.Counter) + if (_expositionFormat == ExpositionFormat.OpenMetricsText && type == MetricType.Counter) { if (name.EndsWith("_total")) { @@ -86,7 +90,7 @@ public async Task WriteFamilyDeclarationAsync(string name, byte[] nameBytes, byt public async Task WriteEnd(CancellationToken cancel) { - if (_fmt == ExpositionFormat.OpenMetricsText) + if (_expositionFormat == ExpositionFormat.OpenMetricsText) await _stream.Value.WriteAsync(EofNewLine, 0, EofNewLine.Length, cancel); } @@ -94,7 +98,28 @@ public async Task WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, Can CancellationToken cancel, double value, ObservedExemplar exemplar, byte[]? suffix = null) { await WriteIdentifierPartAsync(name, flattenedLabels, cancel, canonicalLabel, exemplar, suffix); - await WriteValuePartAsync(value, exemplar, cancel); + + await WriteValue(value, cancel); + if (_expositionFormat == ExpositionFormat.OpenMetricsText && exemplar.IsValid) + { + await WriteExemplarAsync(cancel, exemplar); + } + + await _stream.Value.WriteAsync(NewLine, 0, NewLine.Length, cancel); + } + + public async Task WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, CanonicalLabel canonicalLabel, + CancellationToken cancel, long value, ObservedExemplar exemplar, byte[]? suffix = null) + { + await WriteIdentifierPartAsync(name, flattenedLabels, cancel, canonicalLabel, exemplar, suffix); + + await WriteValue(value, cancel); + if (_expositionFormat == ExpositionFormat.OpenMetricsText && exemplar.IsValid) + { + await WriteExemplarAsync(cancel, exemplar); + } + + await _stream.Value.WriteAsync(NewLine, 0, NewLine.Length, cancel); } private async Task WriteExemplarAsync(CancellationToken cancel, ObservedExemplar exemplar) @@ -124,18 +149,18 @@ private async Task WriteLabel(byte[] label, byte[] value, CancellationToken canc private async Task WriteValue(double value, CancellationToken cancel) { - if (_fmt == ExpositionFormat.OpenMetricsText) + if (_expositionFormat == ExpositionFormat.OpenMetricsText) { switch (value) { case 0: - await _stream.Value.WriteAsync(Zero, 0, Zero.Length, cancel); + await _stream.Value.WriteAsync(FloatZero, 0, FloatZero.Length, cancel); return; case 1: - await _stream.Value.WriteAsync(PositiveOne, 0, PositiveOne.Length, cancel); + await _stream.Value.WriteAsync(FloatPositiveOne, 0, FloatPositiveOne.Length, cancel); return; case -1: - await _stream.Value.WriteAsync(NegativeOne, 0, NegativeOne.Length, cancel); + await _stream.Value.WriteAsync(FloatNegativeOne, 0, FloatNegativeOne.Length, cancel); return; case double.PositiveInfinity: await _stream.Value.WriteAsync(PositiveInfinity, 0, PositiveInfinity.Length, cancel); @@ -150,33 +175,45 @@ private async Task WriteValue(double value, CancellationToken cancel) } var valueAsString = value.ToString("g", CultureInfo.InvariantCulture); - if (_fmt == ExpositionFormat.OpenMetricsText && valueAsString.IndexOfAny(DotEChar) == -1 /* did not contain .|e */) - valueAsString += ".0"; - var numBytes = PrometheusConstants.ExportEncoding - .GetBytes(valueAsString, 0, valueAsString.Length, _stringBytesBuffer, 0); + + var numBytes = PrometheusConstants.ExportEncoding.GetBytes(valueAsString, 0, valueAsString.Length, _stringBytesBuffer, 0); await _stream.Value.WriteAsync(_stringBytesBuffer, 0, numBytes, cancel); - } - // Reuse a buffer to do the UTF-8 encoding. - // Maybe one day also ValueStringBuilder but that would be .NET Core only. - // https://github.com/dotnet/corefx/issues/28379 - // Size limit guided by https://stackoverflow.com/questions/21146544/what-is-the-maximum-length-of-double-tostringd - private readonly byte[] _stringBytesBuffer = new byte[32]; - private readonly ExpositionFormat _fmt; + // In certain places (e.g. "le" label) we need floating point values to actually have the decimal point in them for OpenMetrics. + if (_expositionFormat == ExpositionFormat.OpenMetricsText && valueAsString.IndexOfAny(DotEChar) == -1 /* did not contain .|e */) + await _stream.Value.WriteAsync(DotZero, 0, DotZero.Length, cancel); + } - // 123.456 - // Note: Terminates with a NEWLINE - private async Task WriteValuePartAsync(double value, ObservedExemplar exemplar, CancellationToken cancel) + private async Task WriteValue(long value, CancellationToken cancel) { - await WriteValue(value, cancel); - if (_fmt == ExpositionFormat.OpenMetricsText && exemplar.IsValid) + if (_expositionFormat == ExpositionFormat.OpenMetricsText) { - await WriteExemplarAsync(cancel, exemplar); + switch (value) + { + case 0: + await _stream.Value.WriteAsync(IntZero, 0, IntZero.Length, cancel); + return; + case 1: + await _stream.Value.WriteAsync(IntPositiveOne, 0, IntPositiveOne.Length, cancel); + return; + case -1: + await _stream.Value.WriteAsync(IntNegativeOne, 0, IntNegativeOne.Length, cancel); + return; + } } - await _stream.Value.WriteAsync(NewLine, 0, NewLine.Length, cancel); + var valueAsString = value.ToString("D", CultureInfo.InvariantCulture); + + var numBytes = PrometheusConstants.ExportEncoding.GetBytes(valueAsString, 0, valueAsString.Length, _stringBytesBuffer, 0); + await _stream.Value.WriteAsync(_stringBytesBuffer, 0, numBytes, cancel); } + // Reuse a buffer to do the UTF-8 encoding. + // Maybe one day also ValueStringBuilder but that would be .NET Core only. + // https://github.com/dotnet/corefx/issues/28379 + // Size limit guided by https://stackoverflow.com/questions/21146544/what-is-the-maximum-length-of-double-tostringd + private readonly byte[] _stringBytesBuffer = new byte[32]; + private readonly ExpositionFormat _expositionFormat; /// /// Creates a metric identifier, with an optional name postfix and an optional extra label to append to the end. @@ -212,7 +249,7 @@ private async Task WriteIdentifierPartAsync(byte[] name, byte[] flattenedLabels, await _stream.Value.WriteAsync(canonicalLabel.Name, 0, canonicalLabel.Name.Length, cancel); await _stream.Value.WriteAsync(Equal, 0, Equal.Length, cancel); await _stream.Value.WriteAsync(Quote, 0, Quote.Length, cancel); - if (_fmt == ExpositionFormat.OpenMetricsText) + if (_expositionFormat == ExpositionFormat.OpenMetricsText) await _stream.Value.WriteAsync( canonicalLabel.OpenMetrics, 0, canonicalLabel.OpenMetrics.Length, cancel); else diff --git a/Tests.NetCore/HistogramTests.cs b/Tests.NetCore/HistogramTests.cs index 4fd454a6..466da915 100644 --- a/Tests.NetCore/HistogramTests.cs +++ b/Tests.NetCore/HistogramTests.cs @@ -67,7 +67,7 @@ public async Task ObserveExemplar_OnlyAddsExemplarToSingleBucket() Assert.AreEqual(firstIndex, lastIndex); // And we expect to see it on the correct line. - var expectedLine = $@"xxx_bucket{{le=""2.0""}} 1.0 # {{my_key=""{canary}""}}"; + var expectedLine = $@"xxx_bucket{{le=""2.0""}} 1 # {{my_key=""{canary}""}}"; StringAssert.Contains(serialized, expectedLine); } @@ -89,16 +89,18 @@ public async Task Observe_IncrementsCorrectBucketsAndCountAndSum() await histogram.CollectAndSerializeAsync(serializer, true, default); // Sum - // Count - // 1.0 - // 2.0 - // 3.0 - // +inf - await serializer.Received().WriteMetricPointAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), 2.0, Arg.Any(), Arg.Any()); + await serializer.Received().WriteMetricPointAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), 5.0, Arg.Any(), Arg.Any()); + + // 1.0 bucket await serializer.Received().WriteMetricPointAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), 0, Arg.Any(), Arg.Any()); + + // 2.0 bucket await serializer.Received().WriteMetricPointAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), 1, Arg.Any(), Arg.Any()); - await serializer.Received().WriteMetricPointAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), 2, Arg.Any(), Arg.Any()); - await serializer.Received().WriteMetricPointAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), 2, Arg.Any(), Arg.Any()); + + // Count + // 3.0 bucket + // +inf bucket + await serializer.Received(requiredNumberOfCalls: 3).WriteMetricPointAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), 2, Arg.Any(), Arg.Any()); } [TestMethod] diff --git a/Tests.NetCore/MetricInitializationTests.cs b/Tests.NetCore/MetricInitializationTests.cs index 492bb55d..8d42a1d8 100644 --- a/Tests.NetCore/MetricInitializationTests.cs +++ b/Tests.NetCore/MetricInitializationTests.cs @@ -48,7 +48,10 @@ public async Task CreatingUnlabelledMetric_WithoutObservingAnyData_ExportsImmedi // Without touching any metrics, there should be output for all because default config publishes immediately. await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default, default, default, default, default); - await serializer.ReceivedWithAnyArgs(9).WriteMetricPointAsync(default, default, default, default, default, default); + // gauge + counter + summary.sum + histogram.sum + summary quantile + await serializer.ReceivedWithAnyArgs(5).WriteMetricPointAsync(default, default, default, default, default(double), default); + // summary.count + 2x histogram bucket + histogram count + await serializer.ReceivedWithAnyArgs(4).WriteMetricPointAsync(default, default, default, default, default(long), default); } [TestMethod] @@ -80,7 +83,8 @@ public async Task CreatingUnlabelledMetric_WithInitialValueSuppression_ExportsNo // There is a family for each of the above, in each family we expect to see 0 metrics. await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default, default, default, default, default); - await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default,default, default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default(double), default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default(long), default); } [TestMethod] @@ -117,7 +121,10 @@ public async Task CreatingUnlabelledMetric_WithInitialValueSuppression_ExportsAf // Even though suppressed, they all now have values so should all be published. await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default, default, default, default, default); - await serializer.ReceivedWithAnyArgs(9).WriteMetricPointAsync(default, default, default, default, default, default); + // gauge + counter + summary.sum + histogram.sum + summary quantile + await serializer.ReceivedWithAnyArgs(5).WriteMetricPointAsync(default, default, default, default, default(double), default); + // summary.count + 2x histogram bucket + histogram count + await serializer.ReceivedWithAnyArgs(4).WriteMetricPointAsync(default, default, default, default, default(long), default); } [TestMethod] @@ -154,7 +161,11 @@ public async Task CreatingUnlabelledMetric_WithInitialValueSuppression_ExportsAf // Even though suppressed, they were all explicitly published. await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default, default, default, default, default); - await serializer.ReceivedWithAnyArgs(9).WriteMetricPointAsync(default, default, default, default, default, default); + + // gauge + counter + summary.sum + histogram.sum + summary quantile + await serializer.ReceivedWithAnyArgs(5).WriteMetricPointAsync(default, default, default, default, default(double), default); + // summary.count + 2x histogram bucket + histogram count + await serializer.ReceivedWithAnyArgs(4).WriteMetricPointAsync(default, default, default, default, default(long), default); } #endregion @@ -180,7 +191,10 @@ public async Task CreatingLabelledMetric_WithoutObservingAnyData_ExportsImmediat // Metrics are published as soon as label values are defined. await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default, default, default, default, default); - await serializer.ReceivedWithAnyArgs(9).WriteMetricPointAsync(default, default, default, default, default, default);; + // gauge + counter + summary.sum + histogram.sum + summary quantile + await serializer.ReceivedWithAnyArgs(5).WriteMetricPointAsync(default, default, default, default, default(double), default); + // summary.count + 2x histogram bucket + histogram count + await serializer.ReceivedWithAnyArgs(4).WriteMetricPointAsync(default, default, default, default, default(long), default); } [TestMethod] @@ -212,7 +226,8 @@ public async Task CreatingLabelledMetric_WithInitialValueSuppression_ExportsNoth // Publishing was suppressed. await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default, default, default, default, default); - await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default,default, default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default(double), default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default(long), default); } [TestMethod] @@ -249,7 +264,10 @@ public async Task CreatingLabelledMetric_WithInitialValueSuppression_ExportsAfte // Metrics are published because value was set. await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default, default, default, default, default); - await serializer.ReceivedWithAnyArgs(9).WriteMetricPointAsync(default, default, default, default, default, default); + // gauge + counter + summary.sum + histogram.sum + summary quantile + await serializer.ReceivedWithAnyArgs(5).WriteMetricPointAsync(default, default, default, default, default(double), default); + // summary.count + 2x histogram bucket + histogram count + await serializer.ReceivedWithAnyArgs(4).WriteMetricPointAsync(default, default, default, default, default(long), default); } [TestMethod] @@ -286,7 +304,11 @@ public async Task CreatingLabelledMetric_WithInitialValueSuppression_ExportsAfte // Metrics are published because of explicit publish. await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default, default, default, default, default); - await serializer.ReceivedWithAnyArgs(9).WriteMetricPointAsync(default, default, default, default, default, default); + + // gauge + counter + summary.sum + histogram.sum + summary quantile + await serializer.ReceivedWithAnyArgs(5).WriteMetricPointAsync(default, default, default, default, default(double), default); + // summary.count + 2x histogram bucket + histogram count + await serializer.ReceivedWithAnyArgs(4).WriteMetricPointAsync(default, default, default, default, default(long), default); } [TestMethod] @@ -304,7 +326,7 @@ public async Task CreatingLabelledMetric_AndUnpublishingAfterObservingData_DoesN await registry.CollectAndSerializeAsync(serializer, default); await serializer.ReceivedWithAnyArgs(1).WriteFamilyDeclarationAsync(default, default, default, default, default, default); - await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default,default, default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default, default); } #endregion @@ -329,7 +351,7 @@ public async Task CreatingLabelledMetric_WithoutObservingAnyData_DoesNotExportUn // Family for each of the above, in each is 0 metrics. await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default, default, default, default, default); - await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default,default, default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default, default); } [TestMethod] @@ -358,7 +380,10 @@ public async Task CreatingLabelledMetric_AfterObservingLabelledData_DoesNotExpor // Family for each of the above, in each is 4 metrics (labelled only). await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default, default, default, default, default); - await serializer.ReceivedWithAnyArgs(9).WriteMetricPointAsync(default, default, default, default, default, default); + // gauge + counter + summary.sum + histogram.sum + summary quantile + await serializer.ReceivedWithAnyArgs(5).WriteMetricPointAsync(default, default, default, default, default(double), default); + // summary.count + 2x histogram bucket + histogram count + await serializer.ReceivedWithAnyArgs(4).WriteMetricPointAsync(default, default, default, default, default(long), default); // Only after touching unlabelled do they get published. gauge.Inc(); @@ -369,9 +394,10 @@ public async Task CreatingLabelledMetric_AfterObservingLabelledData_DoesNotExpor serializer.ClearReceivedCalls(); await registry.CollectAndSerializeAsync(serializer, default); - // Family for each of the above, in each is 8 metrics (unlabelled+labelled). + // Family for each of the above, in each family the instance count now doubled as unlabelled instances are published. await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default, default, default, default, default); - await serializer.ReceivedWithAnyArgs(18).WriteMetricPointAsync(default, default, default, default, default, default); + await serializer.ReceivedWithAnyArgs(10).WriteMetricPointAsync(default, default, default, default, default(double), default); + await serializer.ReceivedWithAnyArgs(8).WriteMetricPointAsync(default, default, default, default, default(long), default); } #endregion diff --git a/Tests.NetCore/MetricsTests.cs b/Tests.NetCore/MetricsTests.cs index a8f8a7f2..a40584ee 100644 --- a/Tests.NetCore/MetricsTests.cs +++ b/Tests.NetCore/MetricsTests.cs @@ -71,10 +71,10 @@ public async Task CreateCounter_WithDifferentRegistry_CreatesIndependentCounters await registry2.CollectAndSerializeAsync(serializer2, default); await serializer1.ReceivedWithAnyArgs().WriteFamilyDeclarationAsync(default, default, default, default, default, default); - await serializer1.ReceivedWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default, default); + await serializer1.ReceivedWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default(double), default); await serializer2.ReceivedWithAnyArgs().WriteFamilyDeclarationAsync(default, default, default, default, default, default); - await serializer2.ReceivedWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default, default); + await serializer2.ReceivedWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default(double), default); } [TestMethod] @@ -90,7 +90,8 @@ public async Task Export_FamilyWithOnlyNonpublishedUnlabeledMetrics_ExportsFamil await _registry.CollectAndSerializeAsync(serializer, default); await serializer.ReceivedWithAnyArgs(1).WriteFamilyDeclarationAsync(default, default, default, default, default, default); - await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default,default, default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default(double), default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default(long), default); serializer.ClearReceivedCalls(); metric.Inc(); @@ -99,7 +100,8 @@ public async Task Export_FamilyWithOnlyNonpublishedUnlabeledMetrics_ExportsFamil await _registry.CollectAndSerializeAsync(serializer, default); await serializer.ReceivedWithAnyArgs(1).WriteFamilyDeclarationAsync(default, default, default, default, default, default); - await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default,default, default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default(double), default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default(long), default); } [TestMethod] @@ -117,7 +119,8 @@ public async Task Export_FamilyWithOnlyNonpublishedLabeledMetrics_ExportsFamilyD await _registry.CollectAndSerializeAsync(serializer, default); await serializer.ReceivedWithAnyArgs(1).WriteFamilyDeclarationAsync(default, default, default, default, default, default); - await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default,default, default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default(double), default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default(long), default); serializer.ClearReceivedCalls(); instance.Inc(); @@ -126,7 +129,8 @@ public async Task Export_FamilyWithOnlyNonpublishedLabeledMetrics_ExportsFamilyD await _registry.CollectAndSerializeAsync(serializer, default); await serializer.ReceivedWithAnyArgs(1).WriteFamilyDeclarationAsync(default, default, default, default, default, default); - await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default,default, default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default(double), default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default(long), default); } [TestMethod] @@ -144,15 +148,16 @@ public async Task DisposeChild_RemovesMetric() await _registry.CollectAndSerializeAsync(serializer, default); await serializer.ReceivedWithAnyArgs(1).WriteFamilyDeclarationAsync(default, default, default, default, default, default); - await serializer.ReceivedWithAnyArgs(1).WriteMetricPointAsync(default, default, default, default, default, default); + await serializer.ReceivedWithAnyArgs(1).WriteMetricPointAsync(default, default, default, default, default(double), default); serializer.ClearReceivedCalls(); instance.Dispose(); - + await _registry.CollectAndSerializeAsync(serializer, default); - + await serializer.ReceivedWithAnyArgs(1).WriteFamilyDeclarationAsync(default, default, default, default, default, default); - await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default, default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default(double), default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default(long), default); } [TestMethod] diff --git a/Tests.NetCore/Tests.NetCore.csproj b/Tests.NetCore/Tests.NetCore.csproj index 9b6cf69c..adbfcd4f 100644 --- a/Tests.NetCore/Tests.NetCore.csproj +++ b/Tests.NetCore/Tests.NetCore.csproj @@ -12,9 +12,9 @@ - - - + + + diff --git a/Tests.NetCore/TextSerializerTests.cs b/Tests.NetCore/TextSerializerTests.cs index 6b009f6c..32f33fed 100644 --- a/Tests.NetCore/TextSerializerTests.cs +++ b/Tests.NetCore/TextSerializerTests.cs @@ -185,10 +185,10 @@ public async Task ValidateOpenMetricsFmtHistogram_Basic() result.ShouldBe(@"# HELP boom_bam something # TYPE boom_bam histogram boom_bam_sum 2.5 -boom_bam_count 2.0 -boom_bam_bucket{le=""1.0""} 1.0 -boom_bam_bucket{le=""2.5""} 2.0 -boom_bam_bucket{le=""+Inf""} 2.0 +boom_bam_count 2 +boom_bam_bucket{le=""1.0""} 1 +boom_bam_bucket{le=""2.5""} 2 +boom_bam_bucket{le=""+Inf""} 2 # EOF "); } @@ -214,12 +214,12 @@ public async Task ValidateOpenMetricsFmtHistogram_WithExemplar() result.ShouldBe(@"# HELP boom_bam something # TYPE boom_bam histogram boom_bam_sum 1e+44 -boom_bam_count 4.0 -boom_bam_bucket{le=""1.0""} 1.0 # {traceID=""1""} 1.0 1668779954.714 -boom_bam_bucket{le=""2.5""} 2.0 # {traceID=""2""} 1.5 1668779954.714 -boom_bam_bucket{le=""3.0""} 2.0 -boom_bam_bucket{le=""1e+45""} 4.0 # {traceID=""4""} 1e+44 1668779954.714 -boom_bam_bucket{le=""+Inf""} 4.0 +boom_bam_count 4 +boom_bam_bucket{le=""1.0""} 1 # {traceID=""1""} 1.0 1668779954.714 +boom_bam_bucket{le=""2.5""} 2 # {traceID=""2""} 1.5 1668779954.714 +boom_bam_bucket{le=""3.0""} 2 +boom_bam_bucket{le=""1e+45""} 4 # {traceID=""4""} 1e+44 1668779954.714 +boom_bam_bucket{le=""+Inf""} 4 # EOF "); } diff --git a/Tests.NetFramework/packages.config b/Tests.NetFramework/packages.config index 2f70dd54..500902a2 100644 --- a/Tests.NetFramework/packages.config +++ b/Tests.NetFramework/packages.config @@ -1,8 +1,8 @@  - - + + diff --git a/Tests.NetFramework/tests.netframework.csproj b/Tests.NetFramework/tests.netframework.csproj index a5d59337..516278b1 100644 --- a/Tests.NetFramework/tests.netframework.csproj +++ b/Tests.NetFramework/tests.netframework.csproj @@ -1,6 +1,6 @@  - + Debug AnyCPU @@ -56,10 +56,10 @@ - ..\packages\MSTest.TestFramework.2.2.10\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll + ..\packages\MSTest.TestFramework.3.0.2\lib\net462\Microsoft.VisualStudio.TestPlatform.TestFramework.dll - ..\packages\MSTest.TestFramework.2.2.10\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll + ..\packages\MSTest.TestFramework.3.0.2\lib\net462\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll ..\packages\NSubstitute.4.4.0\lib\net46\NSubstitute.dll @@ -178,8 +178,8 @@ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - + + - + \ No newline at end of file From 9f238890a87b0192af9c905b0a8fc5c30ca3e7e7 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Sun, 1 Jan 2023 21:49:42 +0200 Subject: [PATCH 044/230] Ensure even harder that MetricPusher flushes the final state before stopping --- History | 1 + Prometheus/MetricPusher.cs | 22 +++++++++++++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/History b/History index 06b7f4e3..b7febae4 100644 --- a/History +++ b/History @@ -4,6 +4,7 @@ - The ':' character is no longer allowed in metric or label names. For metric names, Prometheus standard practice is to use colon only in recording rules. - Publish symbol packages and add Source Link support for easier debugging experience. - Fix defect where metrics with different labels could overwrite each other in specific circumstances with multiple metric factories in use (#389). +- Ensure even harder that MetricPusher flushes the final state before stopping (#383 and #384) * 7.1.0 - Added back .NET Standard 2.0 support as some customers had a hard dependency on .NET Standard 2.0 (despite not being a documented feature even earlier). - Added (Observable)UpDownCounter support to MeterAdapter (.NET 7 specific feature). diff --git a/Prometheus/MetricPusher.cs b/Prometheus/MetricPusher.cs index fad648ff..601b926c 100644 --- a/Prometheus/MetricPusher.cs +++ b/Prometheus/MetricPusher.cs @@ -80,6 +80,9 @@ protected override Task StartServer(CancellationToken cancel) // Start the server processing loop asynchronously in the background. return Task.Run(async delegate { + // We do 1 final push after we get cancelled, to ensure that we publish the final state. + var pushingFinalState = false; + while (true) { // We schedule approximately at the configured interval. There may be some small accumulation for the @@ -124,9 +127,21 @@ protected override Task StartServer(CancellationToken cancel) HandleFailedPush(ex); } - // We stop only after pushing metrics, to ensure that the latest state is flushed when told to stop. if (cancel.IsCancellationRequested) - break; + { + if (!pushingFinalState) + { + // Continue for one more loop to push the final state. + // We do this because it might be that we were stopped while in the middle of a push. + pushingFinalState = true; + continue; + } + else + { + // Final push completed, time to pack up our things and go home. + break; + } + } var sleepTime = _pushInterval - duration.GetElapsedTime(); @@ -137,10 +152,11 @@ protected override Task StartServer(CancellationToken cancel) { await Task.Delay(sleepTime, cancel); } - catch (OperationCanceledException) + catch (OperationCanceledException) when (cancel.IsCancellationRequested) { // The task was cancelled. // We continue the loop here to ensure final state gets pushed. + pushingFinalState = true; continue; } } From e334a54f79e41d20d05697baf0a36a6c0c488f1a Mon Sep 17 00:00:00 2001 From: medved91 Date: Mon, 2 Jan 2023 09:25:44 +0500 Subject: [PATCH 045/230] Update SampleService.cs (#386) Fix winner determination condition in Web sample --- Sample.Web/SampleService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sample.Web/SampleService.cs b/Sample.Web/SampleService.cs index 31add63a..3d197ef5 100644 --- a/Sample.Web/SampleService.cs +++ b/Sample.Web/SampleService.cs @@ -76,7 +76,7 @@ private async Task ReadySetGoAsync(CancellationToken cancel) WinsByEndpoint.WithLabels(googleUrl).Inc(exemplar); LossesByEndpoint.WithLabels(microsoftUrl).Inc(exemplar); } - else if (googleStopwatch.Elapsed < microsoftStopwatch.Elapsed) + else if (googleStopwatch.Elapsed > microsoftStopwatch.Elapsed) { WinsByEndpoint.WithLabels(microsoftUrl).Inc(exemplar); LossesByEndpoint.WithLabels(googleUrl).Inc(exemplar); From 0b46fe5ffbe023c4ca5bee7640bd692f0a66ce8d Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Mon, 2 Jan 2023 06:30:18 +0200 Subject: [PATCH 046/230] Use IncTo for more readable DotNetStats code --- Prometheus/DotNetStats.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Prometheus/DotNetStats.cs b/Prometheus/DotNetStats.cs index 8fd34167..1bd49f88 100644 --- a/Prometheus/DotNetStats.cs +++ b/Prometheus/DotNetStats.cs @@ -82,7 +82,7 @@ private void UpdateMetrics() _virtualMemorySize.Set(_process.VirtualMemorySize64); _workingSet.Set(_process.WorkingSet64); _privateMemorySize.Set(_process.PrivateMemorySize64); - _cpuTotal.Inc(Math.Max(0, _process.TotalProcessorTime.TotalSeconds - _cpuTotal.Value)); + _cpuTotal.IncTo(_process.TotalProcessorTime.TotalSeconds); _openHandles.Set(_process.HandleCount); _numThreads.Set(_process.Threads.Count); } From ad7145f965c79c88de0b7a3ce6869af58aa82de7 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Mon, 2 Jan 2023 06:31:15 +0200 Subject: [PATCH 047/230] More IncTo in DotNetStats --- Prometheus/DotNetStats.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Prometheus/DotNetStats.cs b/Prometheus/DotNetStats.cs index 1bd49f88..196ea384 100644 --- a/Prometheus/DotNetStats.cs +++ b/Prometheus/DotNetStats.cs @@ -73,10 +73,7 @@ private void UpdateMetrics() _process.Refresh(); for (var gen = 0; gen <= GC.MaxGeneration; gen++) - { - var collectionCount = _collectionCounts[gen]; - collectionCount.Inc(GC.CollectionCount(gen) - collectionCount.Value); - } + _collectionCounts[gen].IncTo(GC.CollectionCount(gen)); _totalMemory.Set(GC.GetTotalMemory(false)); _virtualMemorySize.Set(_process.VirtualMemorySize64); From f181e119cd086b821ac473b037165277f7f40760 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Mon, 2 Jan 2023 06:32:37 +0200 Subject: [PATCH 048/230] History --- History | 1 + 1 file changed, 1 insertion(+) diff --git a/History b/History index b7febae4..2728b63e 100644 --- a/History +++ b/History @@ -5,6 +5,7 @@ - Publish symbol packages and add Source Link support for easier debugging experience. - Fix defect where metrics with different labels could overwrite each other in specific circumstances with multiple metric factories in use (#389). - Ensure even harder that MetricPusher flushes the final state before stopping (#383 and #384) +- Simplify DotNetStats built-in collector code for ease of readability and more best practices (#365) * 7.1.0 - Added back .NET Standard 2.0 support as some customers had a hard dependency on .NET Standard 2.0 (despite not being a documented feature even earlier). - Added (Observable)UpDownCounter support to MeterAdapter (.NET 7 specific feature). From 3b94744a437e5b9d0c36c4074505fb2a696fa4e3 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Mon, 2 Jan 2023 06:34:44 +0200 Subject: [PATCH 049/230] Simplify process start time update --- Prometheus/DotNetStats.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Prometheus/DotNetStats.cs b/Prometheus/DotNetStats.cs index 196ea384..dd61f0dd 100644 --- a/Prometheus/DotNetStats.cs +++ b/Prometheus/DotNetStats.cs @@ -57,8 +57,7 @@ private DotNetStats(CollectorRegistry registry) // .net specific metrics _totalMemory = metrics.CreateGauge("dotnet_total_memory_bytes", "Total known allocated memory"); - var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); - _startTime.Set((_process.StartTime.ToUniversalTime() - epoch).TotalSeconds); + _startTime.SetToTimeUtc(_process.StartTime); } // The Process class is not thread-safe so let's synchronize the updates to avoid data tearing. From 51771de74de032fcc2a3dbafb6a670d2def530de Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Mon, 2 Jan 2023 06:34:57 +0200 Subject: [PATCH 050/230] History --- History | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/History b/History index 2728b63e..2d30f766 100644 --- a/History +++ b/History @@ -5,7 +5,7 @@ - Publish symbol packages and add Source Link support for easier debugging experience. - Fix defect where metrics with different labels could overwrite each other in specific circumstances with multiple metric factories in use (#389). - Ensure even harder that MetricPusher flushes the final state before stopping (#383 and #384) -- Simplify DotNetStats built-in collector code for ease of readability and more best practices (#365) +- Simplify DotNetStats built-in collector code for ease of readability and more best practices (#365, #364) * 7.1.0 - Added back .NET Standard 2.0 support as some customers had a hard dependency on .NET Standard 2.0 (despite not being a documented feature even earlier). - Added (Observable)UpDownCounter support to MeterAdapter (.NET 7 specific feature). From c4174ffff4b96414ce4ec0350f4200a95eda5739 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Mon, 2 Jan 2023 06:49:31 +0200 Subject: [PATCH 051/230] Exemplar docs and sample --- README.md | 31 ++++++++++++++++++++++++++--- Sample.Console.Exemplars/Program.cs | 29 ++++++++++++++------------- 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index a1682e4d..07f16d1d 100644 --- a/README.md +++ b/README.md @@ -324,7 +324,32 @@ requestsHandled.WithLabels("200").Inc(); Exemplars facilitate [distributed tracing](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/distributed-tracing-concepts), by attaching related trace IDs to metrics. This enables a metrics GUI to cross-references [traces](https://opentelemetry.io/docs/concepts/signals/traces/) that explain how the metric got the value it has. -By default, prometheus-net will create an exemplar with the `trace_id` and `span_id` labels from the current .NET distributed tracing context (`Activity.Current`). To override this, provide your own exemplar when updating the value of the metric: +By default, prometheus-net will create an exemplar with the `trace_id` and `span_id` labels from the current .NET distributed tracing context (`Activity.Current`). If using OpenTelemetry tracing with ASP.NET Core, the trace context from the `traceparent` HTTP request header will be used to automatically assign `Activity.Current`. + +```csharp +private static readonly Counter TotalSleepTime = Metrics + .CreateCounter("sample_sleep_seconds_total", "Total amount of time spent sleeping."); + +... + +// You only need to create the Activity if one is not automatically assigned (e.g. by ASP.NET Core). +using (var activity = new Activity("Pausing before record processing").Start()) +{ + var sleepStopwatch = Stopwatch.StartNew(); + await Task.Delay(TimeSpan.FromSeconds(1)); + + // The trace_id and span_id from the current Activity are exposed as the exemplar. + TotalSleepTime.Inc(sleepStopwatch.Elapsed.TotalSeconds); +} +``` + +This will be published as the following metric point: + +``` +sample_sleep_seconds_total 251.03833569999986 # {trace_id="08ad1c8cec52bf5284538abae7e6d26a",span_id="4761a4918922879b"} 1.0010688 1672634812.125 +``` + +You can override the default exemplar by providing your own when updating the value of the metric: ```csharp private static readonly Counter RecordsProcessed = Metrics @@ -341,11 +366,11 @@ foreach (var record in recordsToProcess) } ``` -Exemplars are only present if the metrics are being scraped by an OpenMetrics-capable client. For development purposes, you can force the library to use the OpenMetrics exposition format by adding `?accept=application/openmetrics-text` to the `/metrics` URL. The Prometheus database automatically negotiates OpenMetrics support when scraping metrics - you do not need to take any special action in production scenarios. - > **Warning** > Exemplars are limited to 128 bytes - they are meant to contain IDs for cross-referencing with trace databases, not as a replacement for trace databases. +Exemplars are only published if the metrics are being scraped by an OpenMetrics-capable client. For development purposes, you can force the library to use the OpenMetrics exposition format by adding `?accept=application/openmetrics-text` to the `/metrics` URL. The Prometheus database automatically negotiates OpenMetrics support when scraping metrics - you do not need to apply any special scraping configuration in production scenarios (you may need to enable exemplar storage, though, as it is still an experimental Prometheus feature as of time of writing this sentence). + See also, [Sample.Console.Exemplars](Sample.Console.Exemplars/Program.cs). # When are metrics published? diff --git a/Sample.Console.Exemplars/Program.cs b/Sample.Console.Exemplars/Program.cs index f544f922..a8de82bd 100644 --- a/Sample.Console.Exemplars/Program.cs +++ b/Sample.Console.Exemplars/Program.cs @@ -27,35 +27,36 @@ var totalSleepTime = Metrics.CreateCounter("sample_sleep_seconds_total", "Total amount of time spent sleeping."); -// The key from an exemplar key-value pair should be created once and reused to minimize memory allocations. +// CUSTOM EXEMPLAR: The key from an exemplar key-value pair should be created once and reused to minimize memory allocations. var recordIdKey = Exemplar.Key("record_id"); _ = Task.Run(async delegate { while (true) { + // DEFAULT EXEMPLAR: We expose the trace_id and span_id for distributed tracing, based on Activity.Current. + // Activity.Current is often automatically inherited from incoming HTTP requests if using OpenTelemetry tracing with ASP.NET Core. + // Here, we manually create and start an activity for sample purposes, without relying on the platform managing the activity context. + // See https://learn.microsoft.com/en-us/dotnet/core/diagnostics/distributed-tracing-concepts + using (var activity = new Activity("Pausing before record processing").Start()) + { + var sleepStopwatch = Stopwatch.StartNew(); + await Task.Delay(TimeSpan.FromSeconds(1)); + + // The trace_id and span_id from the current Activity are exposed as the exemplar by default. + totalSleepTime.Inc(sleepStopwatch.Elapsed.TotalSeconds); + } + // Pretend to process a record approximately every second, just for changing sample data. var recordId = Guid.NewGuid(); var recordPageCount = Random.Shared.Next(minValue: 5, maxValue: 100); - // We pass the record ID key-value pair when we increment the metric. + // CUSTOM EXEMPLAR: We pass the record ID key-value pair when we increment the metric. // When the metric data is published to Prometheus, the most recent record ID will be attached to it. var recordIdKeyValuePair = recordIdKey.WithValue(recordId.ToString()); recordsProcessed.Inc(recordIdKeyValuePair); recordSizeInPages.Observe(recordPageCount, recordIdKeyValuePair); - - // The activity is often automatically inherited from incoming HTTP requests if using OpenTelemetry tracing in ASP.NET Core. - // Here, we manually create and start an activity for sample purposes, without relying on the platform managing the activity context. - // See https://learn.microsoft.com/en-us/dotnet/core/diagnostics/distributed-tracing-concepts - using (var activity = new Activity("Taking a break from record processing").Start()) - { - var sleepStopwatch = Stopwatch.StartNew(); - await Task.Delay(TimeSpan.FromSeconds(1)); - - // If you do not specify an exemplar yourself, the trace_id and span_id from the current Activity are automatically used. - totalSleepTime.Inc(sleepStopwatch.Elapsed.TotalSeconds); - } } }); From 98e6bb9cbec0970a2e79ae96b3e2b0d225a6ffbe Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Mon, 2 Jan 2023 06:50:45 +0200 Subject: [PATCH 052/230] MetricHandler.Dispose() is now public (#355) --- Prometheus/MetricHandler.cs | 105 ++++++++++++++++++------------------ 1 file changed, 52 insertions(+), 53 deletions(-) diff --git a/Prometheus/MetricHandler.cs b/Prometheus/MetricHandler.cs index e57503a2..8286bb2c 100644 --- a/Prometheus/MetricHandler.cs +++ b/Prometheus/MetricHandler.cs @@ -1,70 +1,69 @@ -namespace Prometheus +namespace Prometheus; + +/// +/// Base class for various metric server implementations that start an independent exporter in the background. +/// The expoters may either be pull-based (exposing the Prometheus API) or push-based (actively pushing to PushGateway). +/// +public abstract class MetricHandler : IMetricServer, IDisposable { - /// - /// Base class for various metric server implementations that start an independent exporter in the background. - /// The expoters may either be pull-based (exposing the Prometheus API) or push-based (actively pushing to PushGateway). - /// - public abstract class MetricHandler : IMetricServer, IDisposable - { - // The token is cancelled when the handler is instructed to stop. - private CancellationTokenSource? _cts = new CancellationTokenSource(); + // The token is cancelled when the handler is instructed to stop. + private CancellationTokenSource? _cts = new CancellationTokenSource(); - // This is the task started for the purpose of exporting metrics. - private Task? _task; + // This is the task started for the purpose of exporting metrics. + private Task? _task; - protected MetricHandler() - { - } + protected MetricHandler() + { + } - public IMetricServer Start() - { - if (_task != null) - throw new InvalidOperationException("The metric server has already been started."); + public IMetricServer Start() + { + if (_task != null) + throw new InvalidOperationException("The metric server has already been started."); - if (_cts == null) - throw new InvalidOperationException("The metric server has already been started and stopped. Create a new server if you want to start it again."); + if (_cts == null) + throw new InvalidOperationException("The metric server has already been started and stopped. Create a new server if you want to start it again."); - _task = StartServer(_cts.Token); - return this; - } + _task = StartServer(_cts.Token); + return this; + } - public async Task StopAsync() - { - // Signal the CTS to give a hint to the server thread that it is time to close up shop. - _cts?.Cancel(); + public async Task StopAsync() + { + // Signal the CTS to give a hint to the server thread that it is time to close up shop. + _cts?.Cancel(); - try - { - if (_task == null) - return; // Never started. + try + { + if (_task == null) + return; // Never started. - // This will re-throw any exception that was caught on the StartServerAsync thread. - // Perhaps not ideal behavior but hey, if the implementation does not want this to happen - // it should have caught it itself in the background processing thread. - await _task.ConfigureAwait(false); // Issue #308 - } - catch (OperationCanceledException) - { - // We'll eat this one, though, since it can easily get thrown by whatever checks the CancellationToken. - } - finally - { - _cts?.Dispose(); - _cts = null; - } + // This will re-throw any exception that was caught on the StartServerAsync thread. + // Perhaps not ideal behavior but hey, if the implementation does not want this to happen + // it should have caught it itself in the background processing thread. + await _task.ConfigureAwait(false); // Issue #308 } - - public void Stop() + catch (OperationCanceledException) { - // This method mainly exists for API compatiblity with prometheus-net v1. But it works, so that's fine. - StopAsync().GetAwaiter().GetResult(); + // We'll eat this one, though, since it can easily get thrown by whatever checks the CancellationToken. } - - void IDisposable.Dispose() + finally { - Stop(); + _cts?.Dispose(); + _cts = null; } + } - protected abstract Task StartServer(CancellationToken cancel); + public void Stop() + { + // This method mainly exists for API compatiblity with prometheus-net v1. But it works, so that's fine. + StopAsync().GetAwaiter().GetResult(); } + + public void Dispose() + { + Stop(); + } + + protected abstract Task StartServer(CancellationToken cancel); } From 910aad74769d7714623a7103cb1a2cb3e130e5aa Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Mon, 2 Jan 2023 07:11:42 +0200 Subject: [PATCH 053/230] Docs --- Exemplars.png | Bin 0 -> 34140 bytes README.md | 16 +++++++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) create mode 100644 Exemplars.png diff --git a/Exemplars.png b/Exemplars.png new file mode 100644 index 0000000000000000000000000000000000000000..d199d65577ab13eeac2b7465855d48299155bca4 GIT binary patch literal 34140 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

KBVh{AcRqXj;g2tcFIFl8DpQdtz7ymo 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 literal 0 HcmV?d00001 diff --git a/README.md b/README.md index 07f16d1d..0d0e6be4 100644 --- a/README.md +++ b/README.md @@ -322,14 +322,17 @@ requestsHandled.WithLabels("200").Inc(); # Exemplars -Exemplars facilitate [distributed tracing](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/distributed-tracing-concepts), by attaching related trace IDs to metrics. This enables a metrics GUI to cross-references [traces](https://opentelemetry.io/docs/concepts/signals/traces/) that explain how the metric got the value it has. +Exemplars facilitate [distributed tracing](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/distributed-tracing-concepts), by attaching related trace IDs to metrics. This enables a metrics visualization app to cross-reference [traces](https://opentelemetry.io/docs/concepts/signals/traces/) that explain how the metric got the value it has. -By default, prometheus-net will create an exemplar with the `trace_id` and `span_id` labels from the current .NET distributed tracing context (`Activity.Current`). If using OpenTelemetry tracing with ASP.NET Core, the trace context from the `traceparent` HTTP request header will be used to automatically assign `Activity.Current`. +![](Exemplars.png) + +See also, [Grafana fundamentals - introduction to exemplars](https://grafana.com/docs/grafana/latest/fundamentals/exemplars/). + +By default, prometheus-net will create an exemplar with the `trace_id` and `span_id` labels based on the current distributed tracing context (`Activity.Current`). If using OpenTelemetry tracing with ASP.NET Core, the `traceparent` HTTP request header will be used to automatically assign `Activity.Current`. ```csharp private static readonly Counter TotalSleepTime = Metrics .CreateCounter("sample_sleep_seconds_total", "Total amount of time spent sleeping."); - ... // You only need to create the Activity if one is not automatically assigned (e.g. by ASP.NET Core). @@ -367,9 +370,12 @@ foreach (var record in recordsToProcess) ``` > **Warning** -> Exemplars are limited to 128 bytes - they are meant to contain IDs for cross-referencing with trace databases, not as a replacement for trace databases. +> Exemplars are limited to 128 ASCII characters (counting both keys and values) - they are meant to contain IDs for cross-referencing with trace databases, not as a replacement for trace databases. -Exemplars are only published if the metrics are being scraped by an OpenMetrics-capable client. For development purposes, you can force the library to use the OpenMetrics exposition format by adding `?accept=application/openmetrics-text` to the `/metrics` URL. The Prometheus database automatically negotiates OpenMetrics support when scraping metrics - you do not need to apply any special scraping configuration in production scenarios (you may need to enable exemplar storage, though, as it is still an experimental Prometheus feature as of time of writing this sentence). +Exemplars are only published if the metrics are being scraped by an OpenMetrics-capable client. For development purposes, you can force the library to use the OpenMetrics exposition format by adding `?accept=application/openmetrics-text` to the `/metrics` URL. + +> **Note** +> The Prometheus database automatically negotiates OpenMetrics support when scraping metrics - you do not need to apply any special scraping configuration in production scenarios. You may need to enable exemplar storage, though, as it is still an experimental Prometheus feature as of time of writing this. See also, [Sample.Console.Exemplars](Sample.Console.Exemplars/Program.cs). From e42513e97346e3bb61f98a0f815d99de5d4a8f74 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Mon, 2 Jan 2023 07:14:17 +0200 Subject: [PATCH 054/230] readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0d0e6be4..c8a0f2fd 100644 --- a/README.md +++ b/README.md @@ -375,7 +375,7 @@ foreach (var record in recordsToProcess) Exemplars are only published if the metrics are being scraped by an OpenMetrics-capable client. For development purposes, you can force the library to use the OpenMetrics exposition format by adding `?accept=application/openmetrics-text` to the `/metrics` URL. > **Note** -> The Prometheus database automatically negotiates OpenMetrics support when scraping metrics - you do not need to apply any special scraping configuration in production scenarios. You may need to enable exemplar storage, though, as it is still an experimental Prometheus feature as of time of writing this. +> The Prometheus database automatically negotiates OpenMetrics support when scraping metrics - you do not need to apply any special scraping configuration in production scenarios. You may need to [enable exemplar storage](https://prometheus.io/docs/prometheus/latest/feature_flags/#exemplars-storage), though.. See also, [Sample.Console.Exemplars](Sample.Console.Exemplars/Program.cs). From 42309794a5a5f48a0bb963fb108c20cf61143d54 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Mon, 2 Jan 2023 10:55:56 +0200 Subject: [PATCH 055/230] Benchmark config fine tuning --- Benchmark.NetCore/MeasurementBenchmarks.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Benchmark.NetCore/MeasurementBenchmarks.cs b/Benchmark.NetCore/MeasurementBenchmarks.cs index 1bbc3250..bf2e8554 100644 --- a/Benchmark.NetCore/MeasurementBenchmarks.cs +++ b/Benchmark.NetCore/MeasurementBenchmarks.cs @@ -11,6 +11,8 @@ namespace Benchmark.NetCore; /// [MemoryDiagnoser] [ThreadingDiagnoser] +[InvocationCount(1)] // The implementation does not support multiple invocations. +[MinIterationCount(50), MaxIterationCount(100)] // Help stabilize measurements. public class MeasurementBenchmarks { public enum MetricType @@ -41,7 +43,7 @@ public enum MetricType private readonly Summary.Child _summary; private readonly Histogram.Child _histogram; - private Exemplar.LabelPair[] _exemplars = Array.Empty(); + private Exemplar.LabelPair[] _exemplar = Array.Empty(); public MeasurementBenchmarks() { @@ -77,7 +79,7 @@ public MeasurementBenchmarks() public void GlobalSetup() { if (WithExemplars) - _exemplars = new[] { Exemplar.Key("traceID").WithValue("bar"), Exemplar.Key("traceID2").WithValue("foo") }; + _exemplar = new[] { Exemplar.Key("traceID").WithValue("bar"), Exemplar.Key("traceID2").WithValue("foo") }; } [IterationSetup] @@ -133,7 +135,7 @@ private void MeasurementThreadCounter(object state) for (var i = 0; i < MeasurementCount; i++) { - _counter.Inc(_exemplars); + _counter.Inc(_exemplar); } } @@ -159,7 +161,7 @@ private void MeasurementThreadHistogram(object state) for (var i = 0; i < MeasurementCount; i++) { - _histogram.Observe(i, _exemplars); + _histogram.Observe(i, _exemplar); } } From 23512669f319095531a2a7371837c1e8233d7b94 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Mon, 2 Jan 2023 11:24:16 +0200 Subject: [PATCH 056/230] Slightly improve Counter performance under high concurrency --- Benchmark.NetCore/MeasurementBenchmarks.cs | 4 +- Prometheus/ThreadSafeDouble.cs | 127 ++++++++++----------- Prometheus/ThreadSafeLong.cs | 65 ++++++----- 3 files changed, 97 insertions(+), 99 deletions(-) diff --git a/Benchmark.NetCore/MeasurementBenchmarks.cs b/Benchmark.NetCore/MeasurementBenchmarks.cs index bf2e8554..a30f3c32 100644 --- a/Benchmark.NetCore/MeasurementBenchmarks.cs +++ b/Benchmark.NetCore/MeasurementBenchmarks.cs @@ -12,7 +12,7 @@ namespace Benchmark.NetCore; [MemoryDiagnoser] [ThreadingDiagnoser] [InvocationCount(1)] // The implementation does not support multiple invocations. -[MinIterationCount(50), MaxIterationCount(100)] // Help stabilize measurements. +[MinIterationCount(50), MaxIterationCount(200)] // Help stabilize measurements. public class MeasurementBenchmarks { public enum MetricType @@ -23,7 +23,7 @@ public enum MetricType Summary } - [Params(100_000)] + [Params(200_000)] public int MeasurementCount { get; set; } [Params(1, 16)] diff --git a/Prometheus/ThreadSafeDouble.cs b/Prometheus/ThreadSafeDouble.cs index fb3679a8..d78b9590 100644 --- a/Prometheus/ThreadSafeDouble.cs +++ b/Prometheus/ThreadSafeDouble.cs @@ -1,92 +1,91 @@ using System.Globalization; -namespace Prometheus +namespace Prometheus; + +internal struct ThreadSafeDouble { - internal struct ThreadSafeDouble + private long _value; + + public ThreadSafeDouble(double value) { - private long _value; + _value = BitConverter.DoubleToInt64Bits(value); + } - public ThreadSafeDouble(double value) + public double Value + { + get { - _value = BitConverter.DoubleToInt64Bits(value); + return BitConverter.Int64BitsToDouble(Interlocked.Read(ref _value)); } - - public double Value + set { - get - { - return BitConverter.Int64BitsToDouble(Interlocked.Read(ref _value)); - } - set - { - Interlocked.Exchange(ref _value, BitConverter.DoubleToInt64Bits(value)); - } + Interlocked.Exchange(ref _value, BitConverter.DoubleToInt64Bits(value)); } + } - public void Add(double increment) + public void Add(double increment) + { + while (true) { - while (true) - { - long initialValue = _value; - double computedValue = BitConverter.Int64BitsToDouble(initialValue) + increment; - - if (initialValue == Interlocked.CompareExchange(ref _value, BitConverter.DoubleToInt64Bits(computedValue), initialValue)) - return; - } + long initialValue = Volatile.Read(ref _value); + double computedValue = BitConverter.Int64BitsToDouble(initialValue) + increment; + + if (initialValue == Interlocked.CompareExchange(ref _value, BitConverter.DoubleToInt64Bits(computedValue), initialValue)) + return; } + } - ///

- /// Sets the value to this, unless the existing value is already greater. - /// - public void IncrementTo(double to) + /// + /// Sets the value to this, unless the existing value is already greater. + /// + public void IncrementTo(double to) + { + while (true) { - while (true) - { - long initialRaw = _value; - double initialValue = BitConverter.Int64BitsToDouble(initialRaw); + long initialRaw = Volatile.Read(ref _value); + double initialValue = BitConverter.Int64BitsToDouble(initialRaw); - if (initialValue >= to) - return; // Already greater. + if (initialValue >= to) + return; // Already greater. - if (initialRaw == Interlocked.CompareExchange(ref _value, BitConverter.DoubleToInt64Bits(to), initialRaw)) - return; - } + if (initialRaw == Interlocked.CompareExchange(ref _value, BitConverter.DoubleToInt64Bits(to), initialRaw)) + return; } + } - /// - /// Sets the value to this, unless the existing value is already smaller. - /// - public void DecrementTo(double to) + /// + /// Sets the value to this, unless the existing value is already smaller. + /// + public void DecrementTo(double to) + { + while (true) { - while (true) - { - long initialRaw = _value; - double initialValue = BitConverter.Int64BitsToDouble(initialRaw); + long initialRaw = Volatile.Read(ref _value); + double initialValue = BitConverter.Int64BitsToDouble(initialRaw); - if (initialValue <= to) - return; // Already greater. + if (initialValue <= to) + return; // Already smaller. - if (initialRaw == Interlocked.CompareExchange(ref _value, BitConverter.DoubleToInt64Bits(to), initialRaw)) - return; - } + if (initialRaw == Interlocked.CompareExchange(ref _value, BitConverter.DoubleToInt64Bits(to), initialRaw)) + return; } + } - public override string ToString() - { - return Value.ToString(CultureInfo.InvariantCulture); - } + public override string ToString() + { + return Value.ToString(CultureInfo.InvariantCulture); + } - public override bool Equals(object? obj) - { - if (obj is ThreadSafeDouble) - return Value.Equals(((ThreadSafeDouble)obj).Value); + public override bool Equals(object? obj) + { + if (obj is ThreadSafeDouble) + return Value.Equals(((ThreadSafeDouble)obj).Value); - return Value.Equals(obj); - } + return Value.Equals(obj); + } - public override int GetHashCode() - { - return Value.GetHashCode(); - } + public override int GetHashCode() + { + return Value.GetHashCode(); } } diff --git a/Prometheus/ThreadSafeLong.cs b/Prometheus/ThreadSafeLong.cs index dabbad3d..222a262d 100644 --- a/Prometheus/ThreadSafeLong.cs +++ b/Prometheus/ThreadSafeLong.cs @@ -1,49 +1,48 @@ using System.Globalization; -namespace Prometheus +namespace Prometheus; + +internal struct ThreadSafeLong { - internal struct ThreadSafeLong + private long _value; + + public ThreadSafeLong(long value) { - private long _value; + _value = value; + } - public ThreadSafeLong(long value) + public long Value + { + get { - _value = value; + return Interlocked.Read(ref _value); } - - public long Value + set { - get - { - return Interlocked.Read(ref _value); - } - set - { - Interlocked.Exchange(ref _value, value); - } + Interlocked.Exchange(ref _value, value); } + } - public void Add(long increment) - { - Interlocked.Add(ref _value, increment); - } + public void Add(long increment) + { + Interlocked.Add(ref _value, increment); + } - public override string ToString() - { - return Value.ToString(CultureInfo.InvariantCulture); - } + public override string ToString() + { + return Value.ToString(CultureInfo.InvariantCulture); + } - public override bool Equals(object? obj) - { - if (obj is ThreadSafeLong) - return Value.Equals(((ThreadSafeLong)obj).Value); + public override bool Equals(object? obj) + { + if (obj is ThreadSafeLong) + return Value.Equals(((ThreadSafeLong)obj).Value); - return Value.Equals(obj); - } + return Value.Equals(obj); + } - public override int GetHashCode() - { - return Value.GetHashCode(); - } + public override int GetHashCode() + { + return Value.GetHashCode(); } } From 153cb23158f8a891d8675e0248df95f1916a0011 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Tue, 3 Jan 2023 06:40:22 +0200 Subject: [PATCH 057/230] Reduce memory allocations performed during ASP.NET Core HTTP request tracking --- Benchmark.NetCore/HttpExporterBenchmarks.cs | 96 +++++++------- History | 2 + Prometheus/GaugeExtensions.cs | 136 +++++++++++--------- 3 files changed, 128 insertions(+), 106 deletions(-) diff --git a/Benchmark.NetCore/HttpExporterBenchmarks.cs b/Benchmark.NetCore/HttpExporterBenchmarks.cs index eacf0291..fd3729c4 100644 --- a/Benchmark.NetCore/HttpExporterBenchmarks.cs +++ b/Benchmark.NetCore/HttpExporterBenchmarks.cs @@ -3,59 +3,61 @@ using Prometheus; using Prometheus.HttpMetrics; -namespace Benchmark.NetCore +namespace Benchmark.NetCore; + +[MemoryDiagnoser] +public class HttpExporterBenchmarks { - [MemoryDiagnoser] - public class HttpExporterBenchmarks - { - private CollectorRegistry _registry; - private MetricFactory _factory; - private HttpInProgressMiddleware _inProgressMiddleware; - private HttpRequestCountMiddleware _countMiddleware; - private HttpRequestDurationMiddleware _durationMiddleware; + private CollectorRegistry _registry; + private MetricFactory _factory; + private HttpInProgressMiddleware _inProgressMiddleware; + private HttpRequestCountMiddleware _countMiddleware; + private HttpRequestDurationMiddleware _durationMiddleware; - [Params(1000, 10000)] - public int RequestCount { get; set; } + [Params(100_000)] + public int RequestCount { get; set; } + + [GlobalSetup] + public void Setup() + { + _registry = Metrics.NewCustomRegistry(); + _factory = Metrics.WithCustomRegistry(_registry); - [GlobalSetup] - public void Setup() + _inProgressMiddleware = new HttpInProgressMiddleware(next => Task.CompletedTask, new HttpInProgressOptions { - _registry = Metrics.NewCustomRegistry(); - _factory = Metrics.WithCustomRegistry(_registry); - - _inProgressMiddleware = new HttpInProgressMiddleware(next => Task.CompletedTask, new HttpInProgressOptions - { - Gauge = _factory.CreateGauge("in_progress", "help") - }); - _countMiddleware = new HttpRequestCountMiddleware(next => Task.CompletedTask, new HttpRequestCountOptions - { - Counter = _factory.CreateCounter("count", "help") - }); - _durationMiddleware = new HttpRequestDurationMiddleware(next => Task.CompletedTask, new HttpRequestDurationOptions - { - Histogram = _factory.CreateHistogram("duration", "help") - }); - } - - [Benchmark] - public async Task HttpInProgress() + Gauge = _factory.CreateGauge("in_progress", "help") + }); + _countMiddleware = new HttpRequestCountMiddleware(next => Task.CompletedTask, new HttpRequestCountOptions { - for (var i = 0; i < RequestCount; i++) - await _inProgressMiddleware.Invoke(new DefaultHttpContext()); - } - - [Benchmark] - public async Task HttpRequestCount() + Counter = _factory.CreateCounter("count", "help") + }); + _durationMiddleware = new HttpRequestDurationMiddleware(next => Task.CompletedTask, new HttpRequestDurationOptions { - for (var i = 0; i < RequestCount; i++) - await _countMiddleware.Invoke(new DefaultHttpContext()); - } + Histogram = _factory.CreateHistogram("duration", "help") + }); + } - [Benchmark] - public async Task HttpRequestDuration() - { - for (var i = 0; i < RequestCount; i++) - await _durationMiddleware.Invoke(new DefaultHttpContext()); - } + // Reuse the same HttpContext for different requests, to not count its overhead in the benchmark. + private static readonly DefaultHttpContext _httpContext = new(); + + [Benchmark] + public async Task HttpInProgress() + { + for (var i = 0; i < RequestCount; i++) + await _inProgressMiddleware.Invoke(_httpContext); + } + + [Benchmark] + public async Task HttpRequestCount() + { + for (var i = 0; i < RequestCount; i++) + await _countMiddleware.Invoke(_httpContext); + } + + [Benchmark] + public async Task HttpRequestDuration() + { + for (var i = 0; i < RequestCount; i++) + await _durationMiddleware.Invoke(_httpContext); } } \ No newline at end of file diff --git a/History b/History index 2d30f766..664ec484 100644 --- a/History +++ b/History @@ -6,6 +6,8 @@ - Fix defect where metrics with different labels could overwrite each other in specific circumstances with multiple metric factories in use (#389). - Ensure even harder that MetricPusher flushes the final state before stopping (#383 and #384) - Simplify DotNetStats built-in collector code for ease of readability and more best practices (#365, #364) +- Slightly improve Counter performance under concurrent load. +- Reduce memory allocations performed during ASP.NET Core HTTP request tracking. * 7.1.0 - Added back .NET Standard 2.0 support as some customers had a hard dependency on .NET Standard 2.0 (despite not being a documented feature even earlier). - Added (Observable)UpDownCounter support to MeterAdapter (.NET 7 specific feature). diff --git a/Prometheus/GaugeExtensions.cs b/Prometheus/GaugeExtensions.cs index 02c3e29f..daa236c9 100644 --- a/Prometheus/GaugeExtensions.cs +++ b/Prometheus/GaugeExtensions.cs @@ -1,76 +1,94 @@ -namespace Prometheus +using Microsoft.Extensions.ObjectPool; + +namespace Prometheus; + +public static class GaugeExtensions { - public static class GaugeExtensions + /// + /// Sets the value of the gauge to the current UTC time as a Unix timestamp in seconds. + /// Value does not include any elapsed leap seconds because Unix timestamps do not include leap seconds. + /// + public static void SetToCurrentTimeUtc(this IGauge gauge) { - /// - /// Sets the value of the gauge to the current UTC time as a Unix timestamp in seconds. - /// Value does not include any elapsed leap seconds because Unix timestamps do not include leap seconds. - /// - public static void SetToCurrentTimeUtc(this IGauge gauge) - { - gauge.Set(TimestampHelpers.ToUnixTimeSecondsAsDouble(DateTimeOffset.UtcNow)); - } + gauge.Set(TimestampHelpers.ToUnixTimeSecondsAsDouble(DateTimeOffset.UtcNow)); + } - /// - /// Sets the value of the gauge to a specific moment as the UTC timezone Unix timestamp in seconds. - /// Value does not include any elapsed leap seconds because Unix timestamps do not include leap seconds. - /// - public static void SetToTimeUtc(this IGauge gauge, DateTimeOffset timestamp) - { - gauge.Set(TimestampHelpers.ToUnixTimeSecondsAsDouble(timestamp)); - } + /// + /// Sets the value of the gauge to a specific moment as the UTC timezone Unix timestamp in seconds. + /// Value does not include any elapsed leap seconds because Unix timestamps do not include leap seconds. + /// + public static void SetToTimeUtc(this IGauge gauge, DateTimeOffset timestamp) + { + gauge.Set(TimestampHelpers.ToUnixTimeSecondsAsDouble(timestamp)); + } - /// - /// Increments the value of the gauge to the current UTC time as a Unix timestamp in seconds. - /// Value does not include any elapsed leap seconds because Unix timestamps do not include leap seconds. - /// Operation is ignored if the current value is already greater. - /// - public static void IncToCurrentTimeUtc(this IGauge gauge) - { - gauge.IncTo(TimestampHelpers.ToUnixTimeSecondsAsDouble(DateTimeOffset.UtcNow)); - } + /// + /// Increments the value of the gauge to the current UTC time as a Unix timestamp in seconds. + /// Value does not include any elapsed leap seconds because Unix timestamps do not include leap seconds. + /// Operation is ignored if the current value is already greater. + /// + public static void IncToCurrentTimeUtc(this IGauge gauge) + { + gauge.IncTo(TimestampHelpers.ToUnixTimeSecondsAsDouble(DateTimeOffset.UtcNow)); + } - /// - /// Increments the value of the gauge to a specific moment as the UTC Unix timestamp in seconds. - /// Value does not include any elapsed leap seconds because Unix timestamps do not include leap seconds. - /// Operation is ignored if the current value is already greater. - /// - public static void IncToTimeUtc(this IGauge gauge, DateTimeOffset timestamp) + /// + /// Increments the value of the gauge to a specific moment as the UTC Unix timestamp in seconds. + /// Value does not include any elapsed leap seconds because Unix timestamps do not include leap seconds. + /// Operation is ignored if the current value is already greater. + /// + public static void IncToTimeUtc(this IGauge gauge, DateTimeOffset timestamp) + { + gauge.IncTo(TimestampHelpers.ToUnixTimeSecondsAsDouble(timestamp)); + } + + private sealed class InProgressTracker : IDisposable + { + public void Dispose() { - gauge.IncTo(TimestampHelpers.ToUnixTimeSecondsAsDouble(timestamp)); + if (_gauge == null) + return; + + _gauge.Dec(); + _gauge = null; + Pool.Return(this); } - private sealed class InProgressTracker : IDisposable - { - public InProgressTracker(IGauge gauge) - { - _gauge = gauge; - } + private IGauge? _gauge; - public void Dispose() - { - _gauge.Dec(); - } + public void Update(IGauge gauge) + { + if (_gauge != null) + throw new InvalidOperationException($"{nameof(InProgressTracker)} was reused before being disposed."); - private readonly IGauge _gauge; + _gauge = gauge; } - /// - /// Tracks the number of in-progress operations taking place. - /// - /// Calling this increments the gauge. Disposing of the returned instance decrements it again. - /// - /// - /// It is safe to track the sum of multiple concurrent in-progress operations with the same gauge. - /// - public static IDisposable TrackInProgress(this IGauge gauge) + public static InProgressTracker Create(IGauge gauge) { - if (gauge == null) - throw new ArgumentNullException(nameof(gauge)); + var instance = Pool.Get(); + instance.Update(gauge); + return instance; + } - gauge.Inc(); + private static readonly ObjectPool Pool = ObjectPool.Create(); + } - return new InProgressTracker(gauge); - } + /// + /// Tracks the number of in-progress operations taking place. + /// + /// Calling this increments the gauge. Disposing of the returned instance decrements it again. + /// + /// + /// It is safe to track the sum of multiple concurrent in-progress operations with the same gauge. + /// + public static IDisposable TrackInProgress(this IGauge gauge) + { + if (gauge == null) + throw new ArgumentNullException(nameof(gauge)); + + gauge.Inc(); + + return InProgressTracker.Create(gauge); } } From 22f9c98eccbdfb092f1eba74882e62a0d8f1a08b Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Tue, 3 Jan 2023 07:05:40 +0200 Subject: [PATCH 058/230] Benchmark tidy --- Benchmark.NetCore/KestrelExporterBenchmarks.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/Benchmark.NetCore/KestrelExporterBenchmarks.cs b/Benchmark.NetCore/KestrelExporterBenchmarks.cs index 541322bf..27e710c0 100644 --- a/Benchmark.NetCore/KestrelExporterBenchmarks.cs +++ b/Benchmark.NetCore/KestrelExporterBenchmarks.cs @@ -16,7 +16,8 @@ public class AspNetCoreExporterBenchmarks static AspNetCoreExporterBenchmarks() { // We use the global singleton metrics registry here, so just populate it with some data. - for (var i = 0; i < 1000; i++) + // Not too much data - we care more about the overhead and do not want to just inflate the numbers. + for (var i = 0; i < 5; i++) Metrics.CreateGauge("dummy_metric_" + i, "For benchmark purposes", "label1", "label2", "label3").WithLabels("1", "2", "3").Inc(); } @@ -51,11 +52,18 @@ public void ConfigureServices(IServiceCollection services) public void Configure(IApplicationBuilder app) { app.UseRouting(); + app.UseHttpMetrics(); app.UseEndpoints(endpoints => { endpoints.MapMetrics(); + + endpoints.MapGet("ok", context => + { + context.Response.StatusCode = 200; + return Task.CompletedTask; + }); }); } } @@ -65,4 +73,10 @@ public async Task GetMetrics() { await _client.GetAsync("/metrics"); } + + [Benchmark] + public async Task Get200Ok() + { + await _client.GetAsync("/ok"); + } } From e7cb6080ad6b39cff12f101c1875b2ac2beb9b88 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Wed, 4 Jan 2023 07:45:06 +0200 Subject: [PATCH 059/230] Benchmark update --- Benchmark.NetCore/MeasurementBenchmarks.cs | 10 ++++++ README.md | 41 +++++----------------- 2 files changed, 19 insertions(+), 32 deletions(-) diff --git a/Benchmark.NetCore/MeasurementBenchmarks.cs b/Benchmark.NetCore/MeasurementBenchmarks.cs index a30f3c32..78c39ffb 100644 --- a/Benchmark.NetCore/MeasurementBenchmarks.cs +++ b/Benchmark.NetCore/MeasurementBenchmarks.cs @@ -73,13 +73,23 @@ public MeasurementBenchmarks() _gauge = gaugeTemplate.WithLabels("label value"); _summary = summaryTemplate.WithLabels("label value"); _histogram = histogramTemplate.WithLabels("label value"); + + // We take a single measurement, to warm things up and avoid any first-call impact. + _counter.Inc(); + _gauge.Set(1); + _summary.Observe(1); + _histogram.Observe(1); } [GlobalSetup] public void GlobalSetup() { if (WithExemplars) + { + // You often do need to allocate new exemplar key-value pairs to measure data from new contexts but this benchmark + // exists to indicate the pure measurement performance independent of this, so we reuse an exemplar. _exemplar = new[] { Exemplar.Key("traceID").WithValue("bar"), Exemplar.Key("traceID2").WithValue("foo") }; + } } [IterationSetup] diff --git a/README.md b/README.md index c8a0f2fd..00fc0d60 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ Refer to the sample projects for quick start instructions: | [Sample.Grpc.Client](Sample.Grpc.Client/Program.cs) | Client app for the above | | [Sample.NetStandard](Sample.NetStandard/ImportantProcess.cs) | Demonstrates how to reference prometheus-net in a .NET Standard class library | | [Sample.Web.DifferentPort](Sample.Web.DifferentPort/Program.cs) | Demonstrates how to set up the metric exporter on a different port from the main web API (e.g. for security purposes) | -| [Sample.Web.MetricExpiration](Sample.Web.MetricExpiration/Program.cs) | Demonstrates how to use [automatic metric deletion](#deleting-metrics) | +| [Sample.Web.MetricExpiration](Sample.Web.MetricExpiration/Program.cs) | Demonstrates how to use [automatic metric deletion](#deleting-metrics) | | [Sample.Web.NetFramework](Sample.Web.NetFramework/Global.asax.cs) | .NET Framework web app that publishes custom metrics | The rest of this document describes how to use individual features of the library. @@ -821,44 +821,21 @@ See also, [Sample.Console.DotNetMeters](Sample.Console.DotNetMeters/Program.cs). A suite of benchmarks is included if you wish to explore the performance characteristics of the app. Simply build and run the `Benchmarks.NetCore` project in Release mode. -As an example of the performance of measuring data using prometheus-net, we have the results of the MeasurementBenchmarks here: - -``` -BenchmarkDotNet=v0.13.2, OS=Windows 10 (10.0.19044.2006/21H2/November2021Update) -AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores -.NET SDK=7.0.100-rc.1.22431.12 - [Host] : .NET 6.0.9 (6.0.922.41905), X64 RyuJIT AVX2 - -| MeasurementCount | ThreadCount | TargetMetricType | Mean | Lock Contentions | Allocated | -|------------------|-------------|------------------|---------------:|-----------------:|----------:| -| 100000 | 1 | Counter | 406.4 us | - | 480 B | -| 100000 | 1 | Gauge | 207.8 us | - | 480 B | -| 100000 | 1 | Histogram | 1,416.5 us | - | 480 B | -| 100000 | 1 | Summary | 42,601.8 us | - | 480 B | -| 100000 | 16 | Counter | 176,601.2 us | 13.0000 | 480 B | -| 100000 | 16 | Gauge | 31,241.0 us | 14.0000 | 480 B | -| 100000 | 16 | Histogram | 179,327.9 us | 14.0000 | 480 B | -| 100000 | 16 | Summary | 1,017,871.1 us | 10332.0000 | 480 B | -``` - -> **Note** -> The 480 byte allocation is benchmark harness overhead. Metric measurements do not allocate memory. - -Converting this to more everyday units: +As an example of the performance of measuring data using prometheus-net, we have the results of the MeasurementBenchmarks here, converted into measurements per second: | Metric type | Concurrency | Measurements per second | |-------------|------------:|------------------------:| -| Counter | 1 thread | 246 million | -| Gauge | 1 thread | 481 million | -| Histogram | 1 thread | 71 million | +| Gauge | 1 thread | 539 million | +| Counter | 1 thread | 57 million | +| Histogram | 1 thread | 56 million | | Summary | 1 thread | 2 million | -| Counter | 16 threads | 9 million | -| Gauge | 16 threads | 51 million | -| Histogram | 16 threads | 9 million | +| Gauge | 16 threads | 56 million | +| Counter | 16 threads | 10 million | +| Histogram | 16 threads | 7 million | | Summary | 16 threads | 2 million | > **Note** -> All measurements on all threads are recorded by the same metric instance, for maximum stress and concurrent load. If you have more than 1 metric in your app, multithreaded performance will likely be similar to single-threaded performance. +> All measurements on all threads are recorded by the same metric instance, for maximum stress and concurrent load. In real-world apps with the load spread across multiple metrics, you can expect even better performance. # Community projects From 1ed397b46cd6325e5e8dd23e5f8cf498f47feb9b Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Mon, 16 Jan 2023 07:19:22 +0200 Subject: [PATCH 060/230] Adjust default exemplar measurement logic to make it more flexible and extensible. This enables a custom callback to be used to provide a default exemplar value (either on factory or metric configuration level). The callback will receive the observed value for the metric, enabling sampling. --- Prometheus/ChildBase.cs | 38 +++++++--------------------- Prometheus/Collector.cs | 9 ++++--- Prometheus/CollectorRegistry.cs | 9 ++++--- Prometheus/Counter.cs | 14 +++++----- Prometheus/CounterConfiguration.cs | 6 +++++ Prometheus/Exemplar.cs | 30 +++++++++++++++++++++- Prometheus/ExemplarBehavior.cs | 19 ++++++++++++++ Prometheus/ExemplarProvider.cs | 8 ++++++ Prometheus/Gauge.cs | 12 ++++----- Prometheus/Histogram.cs | 14 +++++----- Prometheus/HistogramConfiguration.cs | 6 +++++ Prometheus/IMetricFactory.cs | 6 +++++ Prometheus/MetricFactory.cs | 32 ++++++++++++++--------- Prometheus/Summary.cs | 11 ++++---- README.md | 26 ++++++++++++++++++- Sample.Console.Exemplars/Program.cs | 20 +++++++++++++++ 16 files changed, 186 insertions(+), 74 deletions(-) create mode 100644 Prometheus/ExemplarBehavior.cs create mode 100644 Prometheus/ExemplarProvider.cs diff --git a/Prometheus/ChildBase.cs b/Prometheus/ChildBase.cs index 7fcf7b1f..33131e59 100644 --- a/Prometheus/ChildBase.cs +++ b/Prometheus/ChildBase.cs @@ -1,7 +1,3 @@ -#if NET6_0_OR_GREATER -using System.Diagnostics; -#endif - namespace Prometheus; /// @@ -9,15 +5,18 @@ namespace Prometheus; /// public abstract class ChildBase : ICollectorChild, IDisposable { - internal ChildBase(Collector parent, LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish) + internal ChildBase(Collector parent, LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish, ExemplarBehavior exemplarBehavior) { Parent = parent; InstanceLabels = instanceLabels; FlattenedLabels = flattenedLabels; FlattenedLabelsBytes = PrometheusConstants.ExportEncoding.GetBytes(flattenedLabels.Serialize()); _publish = publish; + _exemplarBehavior = exemplarBehavior; } + private readonly ExemplarBehavior _exemplarBehavior; + /// /// Marks the metric as one to be published, even if it might otherwise be suppressed. /// @@ -116,31 +115,12 @@ internal void ReturnBorrowedExemplar(ref ObservedExemplar storage, ObservedExemp } } - // Based on https://opentelemetry.io/docs/reference/specification/compatibility/prometheus_and_openmetrics/ - private static readonly Exemplar.LabelKey TraceIdKey = Exemplar.Key("trace_id"); - private static readonly Exemplar.LabelKey SpanIdKey = Exemplar.Key("span_id"); - - /// - /// Returns either the provided exemplar (if any) or the default exemplar. - /// The default exemplar consists of "trace_id" and "span_id" from the current trace context (.NET Core only). - /// - protected internal Exemplar.LabelPair[] ExemplarOrDefault(Exemplar.LabelPair[] exemplar) + protected Exemplar.LabelPair[] ExemplarOrDefault(Exemplar.LabelPair[] exemplar, double value) { - // A custom exemplar was provided - just use it. - if (exemplar is { Length: > 0 }) { return exemplar; } - -#if NET6_0_OR_GREATER - var activity = Activity.Current; - if (activity != null) - { - // Based on https://opentelemetry.io/docs/reference/specification/compatibility/prometheus_and_openmetrics/ - var traceIdLabel = TraceIdKey.WithValue(activity.TraceId.ToString()); - var spanIdLabel = SpanIdKey.WithValue(activity.SpanId.ToString()); - - return new[] { traceIdLabel, spanIdLabel }; - } -#endif + // If a custom exemplar was provided for the observation, just use it. + if (exemplar is { Length: > 0 }) + return exemplar; - return Array.Empty(); + return _exemplarBehavior.DefaultExemplarProvider?.Invoke(Parent, value) ?? Exemplar.Empty; } } \ No newline at end of file diff --git a/Prometheus/Collector.cs b/Prometheus/Collector.cs index 97ab6d78..f6cb2394 100644 --- a/Prometheus/Collector.cs +++ b/Prometheus/Collector.cs @@ -218,7 +218,7 @@ private TChild CreateLabelledChild(LabelSequence instanceLabels) // Order of labels is 1) instance labels; 2) static labels. var flattenedLabels = instanceLabels.Concat(StaticLabels); - return NewChild(instanceLabels, flattenedLabels, publish: !_suppressInitialValue); + return NewChild(instanceLabels, flattenedLabels, publish: !_suppressInitialValue, _exemplarBehavior); } /// @@ -226,10 +226,11 @@ private TChild CreateLabelledChild(LabelSequence instanceLabels) /// internal LabelSequence[] GetAllInstanceLabels() => _labelledMetrics.Select(p => p.Key).ToArray(); - internal Collector(string name, string help, StringSequence instanceLabelNames, LabelSequence staticLabels, bool suppressInitialValue) + internal Collector(string name, string help, StringSequence instanceLabelNames, LabelSequence staticLabels, bool suppressInitialValue, ExemplarBehavior exemplarBehavior) : base(name, help, instanceLabelNames, staticLabels) { _suppressInitialValue = suppressInitialValue; + _exemplarBehavior = exemplarBehavior; _unlabelledLazy = GetUnlabelledLazyInitializer(); @@ -245,7 +246,7 @@ internal Collector(string name, string help, StringSequence instanceLabelNames, /// /// Creates a new instance of the child collector type. /// - private protected abstract TChild NewChild(LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish); + private protected abstract TChild NewChild(LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish, ExemplarBehavior exemplarBehavior); private readonly byte[][] _familyHeaderLines; @@ -274,4 +275,6 @@ private void EnsureUnlabelledMetricCreatedIfNoLabels() if (!_unlabelledLazy.IsValueCreated && !LabelNames.Any()) GetOrAddLabelled(LabelSequence.Empty); } + + private readonly ExemplarBehavior _exemplarBehavior; } \ No newline at end of file diff --git a/Prometheus/CollectorRegistry.cs b/Prometheus/CollectorRegistry.cs index fac7ac32..6a476750 100644 --- a/Prometheus/CollectorRegistry.cs +++ b/Prometheus/CollectorRegistry.cs @@ -156,12 +156,14 @@ internal readonly struct CollectorInitializer private readonly StringSequence _instanceLabelNames; private readonly LabelSequence _staticLabels; private readonly TConfiguration _configuration; + // This is already resolved to inherit from any parent or defaults. + private readonly ExemplarBehavior _exemplarBehavior; public string Name => _name; public StringSequence InstanceLabelNames => _instanceLabelNames; public LabelSequence StaticLabels => _staticLabels; - public CollectorInitializer(CreateInstanceDelegate createInstance, string name, string help, StringSequence instanceLabelNames, LabelSequence staticLabels, TConfiguration configuration) + public CollectorInitializer(CreateInstanceDelegate createInstance, string name, string help, StringSequence instanceLabelNames, LabelSequence staticLabels, TConfiguration configuration, ExemplarBehavior exemplarBehavior) { _createInstance = createInstance; _name = name; @@ -169,11 +171,12 @@ public CollectorInitializer(CreateInstanceDelegate createInstance, string name, _instanceLabelNames = instanceLabelNames; _staticLabels = staticLabels; _configuration = configuration; + _exemplarBehavior = exemplarBehavior; } - public TCollector CreateInstance(CollectorIdentity _) => _createInstance(_name, _help, _instanceLabelNames, _staticLabels, _configuration); + public TCollector CreateInstance(CollectorIdentity _) => _createInstance(_name, _help, _instanceLabelNames, _staticLabels, _configuration, _exemplarBehavior); - public delegate TCollector CreateInstanceDelegate(string name, string help, StringSequence instanceLabelNames, LabelSequence staticLabels, TConfiguration configuration); + public delegate TCollector CreateInstanceDelegate(string name, string help, StringSequence instanceLabelNames, LabelSequence staticLabels, TConfiguration configuration, ExemplarBehavior exemplarBehavior); } /// diff --git a/Prometheus/Counter.cs b/Prometheus/Counter.cs index 7dda1268..f89b0f0a 100644 --- a/Prometheus/Counter.cs +++ b/Prometheus/Counter.cs @@ -4,8 +4,8 @@ public sealed class Counter : Collector, ICounter { public sealed class Child : ChildBase, ICounter { - internal Child(Collector parent, LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish) - : base(parent, instanceLabels, flattenedLabels, publish) + internal Child(Collector parent, LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish, ExemplarBehavior exemplarBehavior) + : base(parent, instanceLabels, flattenedLabels, publish, exemplarBehavior) { } @@ -37,7 +37,7 @@ public void Inc(double increment = 1.0, params Exemplar.LabelPair[] exemplarLabe if (increment < 0.0) throw new ArgumentOutOfRangeException(nameof(increment), "Counter value cannot decrease."); - exemplarLabels = ExemplarOrDefault(exemplarLabels); + exemplarLabels = ExemplarOrDefault(exemplarLabels, increment); if (exemplarLabels is { Length: > 0 }) { @@ -59,13 +59,13 @@ public void IncTo(double targetValue) } - private protected override Child NewChild(LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish) + private protected override Child NewChild(LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish, ExemplarBehavior exemplarBehavior) { - return new Child(this, instanceLabels, flattenedLabels, publish); + return new Child(this, instanceLabels, flattenedLabels, publish, exemplarBehavior); } - internal Counter(string name, string help, StringSequence instanceLabelNames, LabelSequence staticLabels, bool suppressInitialValue) - : base(name, help, instanceLabelNames, staticLabels, suppressInitialValue) + internal Counter(string name, string help, StringSequence instanceLabelNames, LabelSequence staticLabels, bool suppressInitialValue, ExemplarBehavior exemplarBehavior) + : base(name, help, instanceLabelNames, staticLabels, suppressInitialValue, exemplarBehavior) { } diff --git a/Prometheus/CounterConfiguration.cs b/Prometheus/CounterConfiguration.cs index 2cbc42b7..dbcaebc5 100644 --- a/Prometheus/CounterConfiguration.cs +++ b/Prometheus/CounterConfiguration.cs @@ -3,5 +3,11 @@ public sealed class CounterConfiguration : MetricConfiguration { internal static readonly CounterConfiguration Default = new CounterConfiguration(); + + /// + /// Allows you to configure how exemplars are applied to the published metric. + /// If null, inherits the exemplar behavior from the metric factory. + /// + public ExemplarBehavior? ExemplarBehavior { get; set; } } } diff --git a/Prometheus/Exemplar.cs b/Prometheus/Exemplar.cs index 93ccc5ae..3092aa22 100644 --- a/Prometheus/Exemplar.cs +++ b/Prometheus/Exemplar.cs @@ -1,9 +1,14 @@ -using System.Text; +#if NET6_0_OR_GREATER +using System.Diagnostics; +#endif +using System.Text; namespace Prometheus; public static class Exemplar { + public static readonly LabelPair[] Empty = Array.Empty(); + /// /// An exemplar label key. /// @@ -71,4 +76,27 @@ public static LabelPair Pair(string key, string value) { return Key(key).WithValue(value); } + + // Based on https://opentelemetry.io/docs/reference/specification/compatibility/prometheus_and_openmetrics/ + private static readonly LabelKey DefaultTraceIdKey = Key("trace_id"); + private static readonly LabelKey DefaultSpanIdKey = Key("span_id"); + + public static LabelPair[] FromTraceContext() => FromTraceContext(DefaultTraceIdKey, DefaultSpanIdKey); + + public static LabelPair[] FromTraceContext(LabelKey traceIdKey, LabelKey spanIdKey) + { +#if NET6_0_OR_GREATER + var activity = Activity.Current; + if (activity != null) + { + var traceIdLabel = traceIdKey.WithValue(activity.TraceId.ToString()); + var spanIdLabel = spanIdKey.WithValue(activity.SpanId.ToString()); + + return new[] { traceIdLabel, spanIdLabel }; + } +#endif + + // Trace context based exemplars are only supported in .NET Core, not .NET Framework. + return Empty; + } } \ No newline at end of file diff --git a/Prometheus/ExemplarBehavior.cs b/Prometheus/ExemplarBehavior.cs new file mode 100644 index 00000000..585ce293 --- /dev/null +++ b/Prometheus/ExemplarBehavior.cs @@ -0,0 +1,19 @@ +namespace Prometheus; + +/// +/// Defines how exemplars are obtained and published for metrics. +/// Different metrics can have their own exemplar behavior or simply inherit one from the metric factory. +/// +public sealed class ExemplarBehavior +{ + /// + /// Callback that provides the default exemplar if none is provided by the caller when providing a metric value. + /// Defaults to Exemplar.FromTraceContext(). + /// + public ExemplarProvider? DefaultExemplarProvider { get; set; } + + internal static readonly ExemplarBehavior Default = new ExemplarBehavior + { + DefaultExemplarProvider = (_, _) => Exemplar.FromTraceContext() + }; +} diff --git a/Prometheus/ExemplarProvider.cs b/Prometheus/ExemplarProvider.cs new file mode 100644 index 00000000..38b448fa --- /dev/null +++ b/Prometheus/ExemplarProvider.cs @@ -0,0 +1,8 @@ +namespace Prometheus; + +/// +/// Callback to provide an exemplar for a specific observation. +/// +/// The metric instance for which an exemplar is being provided. +/// Context-dependent - for counters, the increment; for histograms, the observed value. +public delegate Exemplar.LabelPair[] ExemplarProvider(Collector metric, double value); diff --git a/Prometheus/Gauge.cs b/Prometheus/Gauge.cs index 49290053..0f3f0541 100644 --- a/Prometheus/Gauge.cs +++ b/Prometheus/Gauge.cs @@ -4,8 +4,8 @@ public sealed class Gauge : Collector, IGauge { public sealed class Child : ChildBase, IGauge { - internal Child(Collector parent, LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish) - : base(parent, instanceLabels, flattenedLabels, publish) + internal Child(Collector parent, LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish, ExemplarBehavior exemplarBehavior) + : base(parent, instanceLabels, flattenedLabels, publish, exemplarBehavior) { } @@ -49,13 +49,13 @@ public void DecTo(double targetValue) public double Value => _value.Value; } - private protected override Child NewChild(LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish) + private protected override Child NewChild(LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish, ExemplarBehavior exemplarBehavior) { - return new Child(this, instanceLabels, flattenedLabels, publish); + return new Child(this, instanceLabels, flattenedLabels, publish, exemplarBehavior); } - internal Gauge(string name, string help, StringSequence instanceLabelNames, LabelSequence staticLabels, bool suppressInitialValue) - : base(name, help, instanceLabelNames, staticLabels, suppressInitialValue) + internal Gauge(string name, string help, StringSequence instanceLabelNames, LabelSequence staticLabels, bool suppressInitialValue, ExemplarBehavior exemplarBehavior) + : base(name, help, instanceLabelNames, staticLabels, suppressInitialValue, exemplarBehavior) { } diff --git a/Prometheus/Histogram.cs b/Prometheus/Histogram.cs index 472e9782..4cb81735 100644 --- a/Prometheus/Histogram.cs +++ b/Prometheus/Histogram.cs @@ -11,8 +11,8 @@ public sealed class Histogram : Collector, IHistogram private static readonly double[] DefaultBuckets = { .005, .01, .025, .05, .075, .1, .25, .5, .75, 1, 2.5, 5, 7.5, 10 }; private readonly double[] _buckets; - internal Histogram(string name, string help, StringSequence instanceLabelNames, LabelSequence staticLabels, bool suppressInitialValue, double[]? buckets) - : base(name, help, instanceLabelNames, staticLabels, suppressInitialValue) + internal Histogram(string name, string help, StringSequence instanceLabelNames, LabelSequence staticLabels, bool suppressInitialValue, double[]? buckets, ExemplarBehavior exemplarBehavior) + : base(name, help, instanceLabelNames, staticLabels, suppressInitialValue, exemplarBehavior) { if (instanceLabelNames.Contains("le")) { @@ -39,15 +39,15 @@ internal Histogram(string name, string help, StringSequence instanceLabelNames, } } - private protected override Child NewChild(LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish) + private protected override Child NewChild(LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish, ExemplarBehavior exemplarBehavior) { - return new Child(this, instanceLabels, flattenedLabels, publish); + return new Child(this, instanceLabels, flattenedLabels, publish, exemplarBehavior); } public sealed class Child : ChildBase, IHistogram { - internal Child(Histogram parent, LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish) - : base(parent, instanceLabels, flattenedLabels, publish) + internal Child(Histogram parent, LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish, ExemplarBehavior exemplarBehavior) + : base(parent, instanceLabels, flattenedLabels, publish, exemplarBehavior) { Parent = parent; @@ -136,7 +136,7 @@ private void ObserveInternal(double val, long count, params Exemplar.LabelPair[] return; } - exemplarLabels = ExemplarOrDefault(exemplarLabels); + exemplarLabels = ExemplarOrDefault(exemplarLabels, val); for (int i = 0; i < _upperBounds.Length; i++) { diff --git a/Prometheus/HistogramConfiguration.cs b/Prometheus/HistogramConfiguration.cs index 893c499d..7f147403 100644 --- a/Prometheus/HistogramConfiguration.cs +++ b/Prometheus/HistogramConfiguration.cs @@ -8,5 +8,11 @@ public sealed class HistogramConfiguration : MetricConfiguration /// Custom histogram buckets to use. If null, will use Histogram.DefaultBuckets. /// public double[]? Buckets { get; set; } + + /// + /// Allows you to configure how exemplars are applied to the published metric. + /// If null, inherits the exemplar behavior from the metric factory. + /// + public ExemplarBehavior? ExemplarBehavior { get; set; } } } diff --git a/Prometheus/IMetricFactory.cs b/Prometheus/IMetricFactory.cs index f2318318..442771c3 100644 --- a/Prometheus/IMetricFactory.cs +++ b/Prometheus/IMetricFactory.cs @@ -39,5 +39,11 @@ public interface IMetricFactory /// The expiration timer is reset to zero for the duration of any active lifetime-extension lease that is taken on a specific metric. /// IManagedLifetimeMetricFactory WithManagedLifetime(TimeSpan expiresAfter); + + /// + /// Allows you to configure how exemplars are applied to published metrics. If null, uses default behavior (see ). + /// This is inherited by all metrics by default, although may be overridden in the configuration of an individual metric. + /// + ExemplarBehavior? ExemplarBehavior { get; set; } } } diff --git a/Prometheus/MetricFactory.cs b/Prometheus/MetricFactory.cs index 2a3be341..b42987e7 100644 --- a/Prometheus/MetricFactory.cs +++ b/Prometheus/MetricFactory.cs @@ -85,46 +85,52 @@ public Summary CreateSummary(string name, string help, string[] labelNames, Summ internal Counter CreateCounter(string name, string help, StringSequence instanceLabelNames, CounterConfiguration? configuration) { - static Counter CreateInstance(string finalName, string finalHelp, StringSequence finalInstanceLabelNames, LabelSequence finalStaticLabels, CounterConfiguration finalConfiguration) + static Counter CreateInstance(string finalName, string finalHelp, StringSequence finalInstanceLabelNames, LabelSequence finalStaticLabels, CounterConfiguration finalConfiguration, ExemplarBehavior finalExemplarBehavior) { - return new Counter(finalName, finalHelp, finalInstanceLabelNames, finalStaticLabels, finalConfiguration.SuppressInitialValue); + return new Counter(finalName, finalHelp, finalInstanceLabelNames, finalStaticLabels, finalConfiguration.SuppressInitialValue, finalExemplarBehavior); } - var initializer = new CollectorRegistry.CollectorInitializer(CreateInstance, name, help, instanceLabelNames, _staticLabelsLazy.Value, configuration ?? CounterConfiguration.Default); + var exemplarBehavior = configuration?.ExemplarBehavior ?? ExemplarBehavior ?? ExemplarBehavior.Default; + var initializer = new CollectorRegistry.CollectorInitializer(CreateInstance, name, help, instanceLabelNames, _staticLabelsLazy.Value, configuration ?? CounterConfiguration.Default, exemplarBehavior); return _registry.GetOrAdd(initializer); } internal Gauge CreateGauge(string name, string help, StringSequence instanceLabelNames, GaugeConfiguration? configuration) { - static Gauge CreateInstance(string finalName, string finalHelp, StringSequence finalInstanceLabelNames, LabelSequence finalStaticLabels, GaugeConfiguration finalConfiguration) + static Gauge CreateInstance(string finalName, string finalHelp, StringSequence finalInstanceLabelNames, LabelSequence finalStaticLabels, GaugeConfiguration finalConfiguration, ExemplarBehavior finalExemplarBehavior) { - return new Gauge(finalName, finalHelp, finalInstanceLabelNames, finalStaticLabels, finalConfiguration.SuppressInitialValue); + return new Gauge(finalName, finalHelp, finalInstanceLabelNames, finalStaticLabels, finalConfiguration.SuppressInitialValue, finalExemplarBehavior); } - var initializer = new CollectorRegistry.CollectorInitializer(CreateInstance, name, help, instanceLabelNames, _staticLabelsLazy.Value, configuration ?? GaugeConfiguration.Default); + // Note: exemplars are not supported for gauges. We just pass it along here to avoid forked APIs downsream. + var exemplarBehavior = ExemplarBehavior ?? ExemplarBehavior.Default; + var initializer = new CollectorRegistry.CollectorInitializer(CreateInstance, name, help, instanceLabelNames, _staticLabelsLazy.Value, configuration ?? GaugeConfiguration.Default, exemplarBehavior); return _registry.GetOrAdd(initializer); } internal Histogram CreateHistogram(string name, string help, StringSequence instanceLabelNames, HistogramConfiguration? configuration) { - static Histogram CreateInstance(string finalName, string finalHelp, StringSequence finalInstanceLabelNames, LabelSequence finalStaticLabels, HistogramConfiguration finalConfiguration) + static Histogram CreateInstance(string finalName, string finalHelp, StringSequence finalInstanceLabelNames, LabelSequence finalStaticLabels, HistogramConfiguration finalConfiguration, ExemplarBehavior finalExemplarBehavior) { - return new Histogram(finalName, finalHelp, finalInstanceLabelNames, finalStaticLabels, finalConfiguration.SuppressInitialValue, finalConfiguration.Buckets); + return new Histogram(finalName, finalHelp, finalInstanceLabelNames, finalStaticLabels, finalConfiguration.SuppressInitialValue, finalConfiguration.Buckets, finalExemplarBehavior); } - var initializer = new CollectorRegistry.CollectorInitializer(CreateInstance, name, help, instanceLabelNames, _staticLabelsLazy.Value, configuration ?? HistogramConfiguration.Default); + var exemplarBehavior = configuration?.ExemplarBehavior ?? ExemplarBehavior ?? ExemplarBehavior.Default; + var initializer = new CollectorRegistry.CollectorInitializer(CreateInstance, name, help, instanceLabelNames, _staticLabelsLazy.Value, configuration ?? HistogramConfiguration.Default, exemplarBehavior); return _registry.GetOrAdd(initializer); } internal Summary CreateSummary(string name, string help, StringSequence instanceLabelNames, SummaryConfiguration? configuration) { - static Summary CreateInstance(string finalName, string finalHelp, StringSequence finalInstanceLabelNames, LabelSequence finalStaticLabels, SummaryConfiguration finalConfiguration) + static Summary CreateInstance(string finalName, string finalHelp, StringSequence finalInstanceLabelNames, LabelSequence finalStaticLabels, SummaryConfiguration finalConfiguration, ExemplarBehavior finalExemplarBehavior) { - return new Summary(finalName, finalHelp, finalInstanceLabelNames, finalStaticLabels, finalConfiguration.SuppressInitialValue, + return new Summary(finalName, finalHelp, finalInstanceLabelNames, finalStaticLabels, finalExemplarBehavior, finalConfiguration.SuppressInitialValue, finalConfiguration.Objectives, finalConfiguration.MaxAge, finalConfiguration.AgeBuckets, finalConfiguration.BufferSize); } - var initializer = new CollectorRegistry.CollectorInitializer(CreateInstance, name, help, instanceLabelNames, _staticLabelsLazy.Value, configuration ?? SummaryConfiguration.Default); + // Note: exemplars are not supported for summaries. We just pass it along here to avoid forked APIs downsream. + var exemplarBehavior = ExemplarBehavior ?? ExemplarBehavior.Default; + var initializer = new CollectorRegistry.CollectorInitializer(CreateInstance, name, help, instanceLabelNames, _staticLabelsLazy.Value, configuration ?? SummaryConfiguration.Default, exemplarBehavior); return _registry.GetOrAdd(initializer); } @@ -171,4 +177,6 @@ internal StringSequence GetAllStaticLabelNames() public IManagedLifetimeMetricFactory WithManagedLifetime(TimeSpan expiresAfter) => new ManagedLifetimeMetricFactory(this, expiresAfter); + + public ExemplarBehavior? ExemplarBehavior { get; set; } } \ No newline at end of file diff --git a/Prometheus/Summary.cs b/Prometheus/Summary.cs index f017498f..a147ee2e 100644 --- a/Prometheus/Summary.cs +++ b/Prometheus/Summary.cs @@ -32,12 +32,13 @@ internal Summary( string help, StringSequence instanceLabelNames, LabelSequence staticLabels, + ExemplarBehavior exemplarBehavior, bool suppressInitialValue = false, IReadOnlyList? objectives = null, TimeSpan? maxAge = null, int? ageBuckets = null, int? bufCap = null) - : base(name, help, instanceLabelNames, staticLabels, suppressInitialValue) + : base(name, help, instanceLabelNames, staticLabels, suppressInitialValue, exemplarBehavior) { _objectives = objectives ?? DefObjectivesArray; _maxAge = maxAge ?? DefMaxAge; @@ -60,17 +61,17 @@ internal Summary( throw new ArgumentException($"{QuantileLabel} is a reserved label name"); } - private protected override Child NewChild(LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish) + private protected override Child NewChild(LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish, ExemplarBehavior exemplarBehavior) { - return new Child(this, instanceLabels, flattenedLabels, publish); + return new Child(this, instanceLabels, flattenedLabels, publish, exemplarBehavior); } internal override MetricType Type => MetricType.Summary; public sealed class Child : ChildBase, ISummary { - internal Child(Summary parent, LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish) - : base(parent, instanceLabels, flattenedLabels, publish) + internal Child(Summary parent, LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish, ExemplarBehavior exemplarBehavior) + : base(parent, instanceLabels, flattenedLabels, publish, exemplarBehavior) { _objectives = parent._objectives; _maxAge = parent._maxAge; diff --git a/README.md b/README.md index 00fc0d60..07b34ee0 100644 --- a/README.md +++ b/README.md @@ -352,7 +352,31 @@ This will be published as the following metric point: sample_sleep_seconds_total 251.03833569999986 # {trace_id="08ad1c8cec52bf5284538abae7e6d26a",span_id="4761a4918922879b"} 1.0010688 1672634812.125 ``` -You can override the default exemplar by providing your own when updating the value of the metric: +You can customize the default exemplar provider via `IMetricFactory.ExemplarBehavior` or `CounterConfiguration.ExemplarBehavior` and `HistogramConfiguration.ExemplarBehavior`, which allow you to provide your own method to generate exemplars: + +```csharp +// For the next histogram we only want to record exemplars for values larger than 0.1 (i.e. when record processing goes slowly). +static Exemplar.LabelPair[] RecordExemplarForSlowRecordProcessingDuration(Collector metric, double value) +{ + if (value < 0.1) + return Exemplar.Empty; + + return Exemplar.FromTraceContext(); +} + +var recordProcessingDuration = Metrics + .CreateHistogram("sample_record_processing_duration_seconds", "How long it took to process a record, in seconds.", + new HistogramConfiguration + { + Buckets = Histogram.PowersOfTenDividedBuckets(-4, 1, 5), + ExemplarBehavior = new() + { + DefaultExemplarProvider = RecordExemplarForSlowRecordProcessingDuration + } + }); +``` + +You can also override any default exemplar logic by providing your own exemplar when updating the value of the metric: ```csharp private static readonly Counter RecordsProcessed = Metrics diff --git a/Sample.Console.Exemplars/Program.cs b/Sample.Console.Exemplars/Program.cs index a8de82bd..b8ddc10e 100644 --- a/Sample.Console.Exemplars/Program.cs +++ b/Sample.Console.Exemplars/Program.cs @@ -25,6 +25,24 @@ Buckets = Histogram.PowersOfTenDividedBuckets(0, 2, 10) }); +// SAMPLED EXEMPLAR: For the next histogram we only want to record exemplars for values larger than 0.1 (i.e. when record processing goes slowly). +static Exemplar.LabelPair[] RecordExemplarForSlowRecordProcessingDuration(Collector metric, double value) +{ + if (value < 0.1) + return Exemplar.Empty; + + return Exemplar.FromTraceContext(); +} + +var recordProcessingDuration = Metrics.CreateHistogram("sample_record_processing_duration_seconds", "How long it took to process a record, in seconds.", new HistogramConfiguration +{ + Buckets = Histogram.PowersOfTenDividedBuckets(-4, 1, 5), + ExemplarBehavior = new() + { + DefaultExemplarProvider = RecordExemplarForSlowRecordProcessingDuration + } +}); + var totalSleepTime = Metrics.CreateCounter("sample_sleep_seconds_total", "Total amount of time spent sleeping."); // CUSTOM EXEMPLAR: The key from an exemplar key-value pair should be created once and reused to minimize memory allocations. @@ -47,6 +65,8 @@ totalSleepTime.Inc(sleepStopwatch.Elapsed.TotalSeconds); } + using var processingDurationTimer = recordProcessingDuration.NewTimer(); + // Pretend to process a record approximately every second, just for changing sample data. var recordId = Guid.NewGuid(); var recordPageCount = Random.Shared.Next(minValue: 5, maxValue: 100); From 83e24166b81364d5407661e24a5336a26a9b7fe0 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Mon, 16 Jan 2023 13:01:29 +0200 Subject: [PATCH 061/230] Add SDK comparison benchmarks --- Benchmark.NetCore/Benchmark.NetCore.csproj | 1 + Benchmark.NetCore/Program.cs | 15 +- Benchmark.NetCore/SdkComparisonBenchmarks.cs | 234 +++++++++++++++++++ Prometheus/ChildBase.cs | 2 +- Prometheus/Exemplar.cs | 4 +- Prometheus/ExemplarBehavior.cs | 5 + README.md | 23 +- Sample.Console.Exemplars/Program.cs | 2 +- 8 files changed, 273 insertions(+), 13 deletions(-) create mode 100644 Benchmark.NetCore/SdkComparisonBenchmarks.cs diff --git a/Benchmark.NetCore/Benchmark.NetCore.csproj b/Benchmark.NetCore/Benchmark.NetCore.csproj index 57baef3e..c2493714 100644 --- a/Benchmark.NetCore/Benchmark.NetCore.csproj +++ b/Benchmark.NetCore/Benchmark.NetCore.csproj @@ -26,6 +26,7 @@ + diff --git a/Benchmark.NetCore/Program.cs b/Benchmark.NetCore/Program.cs index 9daa49ab..94822efb 100644 --- a/Benchmark.NetCore/Program.cs +++ b/Benchmark.NetCore/Program.cs @@ -1,14 +1,13 @@ using BenchmarkDotNet.Running; -namespace Benchmark.NetCore +namespace Benchmark.NetCore; + +internal class Program { - internal class Program + private static void Main(string[] args) { - private static void Main(string[] args) - { - // Give user possibility to choose which benchmark to run. - // Can be overridden from the command line with the --filter option. - new BenchmarkSwitcher(typeof(Program).Assembly).Run(args); - } + // Give user possibility to choose which benchmark to run. + // Can be overridden from the command line with the --filter option. + new BenchmarkSwitcher(typeof(Program).Assembly).Run(args); } } diff --git a/Benchmark.NetCore/SdkComparisonBenchmarks.cs b/Benchmark.NetCore/SdkComparisonBenchmarks.cs new file mode 100644 index 00000000..e0c88245 --- /dev/null +++ b/Benchmark.NetCore/SdkComparisonBenchmarks.cs @@ -0,0 +1,234 @@ +using System.Diagnostics; +using System.Diagnostics.Metrics; +using BenchmarkDotNet.Attributes; +using OpenTelemetry.Metrics; +using Prometheus; + +namespace Benchmark.NetCore; + +/// +/// We compare pure measurement (not serializing the data) with prometheus-net SDK and OpenTelemetry .NET SDK. +/// +/// +/// Design logic: +/// * Metrics are initialized once on application startup. +/// * Metrics typically measure "sessions" - there are sets of metrics that are related through shared identifiers and a shared lifetime (e.g. HTTP request), +/// with all the identifiers for the metrics created when the sesison is initialized (e.g. when the HTTP connection is established). +/// +/// Excluded from measurement: +/// * Meter setup (because meters are created once on application setup and not impactful later). +/// * Test data generation (session numbers and identifier strings) as it is SDK-neutral. +/// +/// We have a separate benchmark to compare the setup stage (just re-runs the setup logic in measured phase). +/// +/// We also do not benchmark "observable" metrics that are only polled at time of collection. +/// Both SDKs support it as an optimization (though OpenTelemetry forces it for counters) but let's try keep the logic here simple and exclude it for now. +/// +[MemoryDiagnoser] +public class SdkComparisonBenchmarks +{ + private const int CounterCount = 100; + private const int HistogramCount = 100; + + // Unique sets of label/tag values per metric. You can think of each one as a "session" we are reporting data for. + private const int TimeseriesPerMetric = 100; + + private static readonly string[] LabelNames = new[] { "environment", "server", "session_id" }; + private const string Label1Value = "production"; + private const string Label2Value = "hkhk298599-qps010-n200"; + + // How many observations we take during a single benchmark invocation (for each timeseries). + private const int ObservationCount = 1000; + + private static readonly string[] SessionIds = new string[TimeseriesPerMetric]; + + static SdkComparisonBenchmarks() + { + for (var i = 0; i < SessionIds.Length; i++) + SessionIds[i] = Guid.NewGuid().ToString(); + } + + [Params(MetricsSdk.PrometheusNet, MetricsSdk.OpenTelemetry)] + public MetricsSdk Sdk { get; set; } + + public enum MetricsSdk + { + PrometheusNet, + OpenTelemetry + } + + /// + /// Contains all the context that gets initialized at iteration setup time. + /// + /// This data set is: + /// 1) Not included in the performance measurements. + /// 2) Reused for each invocation that is part of the same iteration. + /// + private abstract class MetricsContext : IDisposable + { + /// + /// Records an observation with all the counter-type metrics for each session. + /// + public abstract void ObserveCounter(double value); + + /// + /// Records an observation with all the histogram-type metrics for each session. + /// + public abstract void ObserveHistogram(double value); + + public virtual void Dispose() { } + } + + private sealed class PrometheusNetMetricsContext : MetricsContext + { + private readonly List _counterInstances = new(CounterCount * TimeseriesPerMetric); + private readonly List _histogramInstances = new(HistogramCount * TimeseriesPerMetric); + + public PrometheusNetMetricsContext() + { + var registry = Metrics.NewCustomRegistry(); + var factory = Metrics.WithCustomRegistry(registry); + + // Do not emit any exemplars in this benchmark, as they are not yet equally supported by the SDKs. + factory.ExemplarBehavior = ExemplarBehavior.NoExemplars(); + + for (var counterIndex = 0; counterIndex < CounterCount; counterIndex++) + { + var counter = factory.CreateCounter("counter_" + counterIndex, "", LabelNames); + + for (var i = 0; i < TimeseriesPerMetric; i++) + _counterInstances.Add(counter.WithLabels(Label1Value, Label2Value, SessionIds[i])); + } + + for (var histogramIndex = 0; histogramIndex < HistogramCount; histogramIndex++) + { + var histogram = factory.CreateHistogram("histogram_" + histogramIndex, "", LabelNames); + + for (var i = 0; i < TimeseriesPerMetric; i++) + _histogramInstances.Add(histogram.WithLabels(Label1Value, Label2Value, SessionIds[i])); + } + } + + public override void ObserveCounter(double value) + { + foreach (var counter in _counterInstances) + counter.Inc(value); + } + + public override void ObserveHistogram(double value) + { + foreach (var histogram in _histogramInstances) + histogram.Observe(value); + } + } + + private sealed class OpenTelemetryMetricsContext : MetricsContext + { + private const string MeterBaseName = "benchmark"; + + private readonly Meter _meter; + private readonly MeterProvider _provider; + + private readonly List> _counters = new(CounterCount); + private readonly List> _histograms = new(HistogramCount); + + private readonly List _sessions = new(TimeseriesPerMetric); + + public OpenTelemetryMetricsContext() + { + // We use a randomized name every time because otherwise there appears to be some "shared state" between benchmark invocations, + // at least for the "setup" benchmark which keeps getting slower every time we call it with the same metric name. + _meter = new Meter(MeterBaseName + Guid.NewGuid()); + + _provider = OpenTelemetry.Sdk.CreateMeterProviderBuilder() + .AddPrometheusExporter() + .AddMeter(_meter.Name) + .Build(); + + for (var i = 0; i < CounterCount; i++) + _counters.Add(_meter.CreateCounter("counter_" + i)); + + for (var i = 0; i < HistogramCount; i++) + _histograms.Add(_meter.CreateHistogram("histogram_" + i)); + + for (var i = 0; i < TimeseriesPerMetric; i++) + { + var tag1 = new KeyValuePair(LabelNames[0], Label1Value); + var tag2 = new KeyValuePair(LabelNames[1], Label2Value); + var tag3 = new KeyValuePair(LabelNames[2], SessionIds[i]); + + var tagList = new TagList(new[] { tag1, tag2, tag3 }); + _sessions.Add(tagList); + } + } + + public override void ObserveCounter(double value) + { + foreach (var session in _sessions) + { + foreach (var counter in _counters) + counter.Add(value, session); + } + } + + public override void ObserveHistogram(double value) + { + foreach (var session in _sessions) + { + foreach (var histogram in _histograms) + histogram.Record(value, session); + } + } + + public override void Dispose() + { + base.Dispose(); + + _provider.Dispose(); + } + } + + private MetricsContext _context; + + [IterationSetup] + public void Setup() + { + _context = Sdk switch + { + MetricsSdk.PrometheusNet => new PrometheusNetMetricsContext(), + MetricsSdk.OpenTelemetry => new OpenTelemetryMetricsContext(), + _ => throw new NotImplementedException(), + }; + } + + [Benchmark] + public void CounterMeasurements() + { + for (var observation = 0; observation < ObservationCount; observation++) + _context.ObserveCounter(observation); + } + + [Benchmark] + public void HistogramMeasurements() + { + for (var observation = 0; observation < ObservationCount; observation++) + _context.ObserveHistogram(observation); + } + + [IterationCleanup] + public void Cleanup() + { + _context.Dispose(); + } + + [Benchmark] + public void SetupBenchmark() + { + // Here we just do the setup again, but this time as part of the measured data set, to compare the setup cost between SDKs. + + // We need to dispose of the automatically created context, in case there are any SDK-level singleton resources (which we do not want to accidentally reuse). + _context.Dispose(); + + Setup(); + } +} diff --git a/Prometheus/ChildBase.cs b/Prometheus/ChildBase.cs index 33131e59..7d54e3c8 100644 --- a/Prometheus/ChildBase.cs +++ b/Prometheus/ChildBase.cs @@ -121,6 +121,6 @@ protected Exemplar.LabelPair[] ExemplarOrDefault(Exemplar.LabelPair[] exemplar, if (exemplar is { Length: > 0 }) return exemplar; - return _exemplarBehavior.DefaultExemplarProvider?.Invoke(Parent, value) ?? Exemplar.Empty; + return _exemplarBehavior.DefaultExemplarProvider?.Invoke(Parent, value) ?? Exemplar.None; } } \ No newline at end of file diff --git a/Prometheus/Exemplar.cs b/Prometheus/Exemplar.cs index 3092aa22..719a74a9 100644 --- a/Prometheus/Exemplar.cs +++ b/Prometheus/Exemplar.cs @@ -7,7 +7,7 @@ namespace Prometheus; public static class Exemplar { - public static readonly LabelPair[] Empty = Array.Empty(); + public static readonly LabelPair[] None = Array.Empty(); /// /// An exemplar label key. @@ -97,6 +97,6 @@ public static LabelPair[] FromTraceContext(LabelKey traceIdKey, LabelKey spanIdK #endif // Trace context based exemplars are only supported in .NET Core, not .NET Framework. - return Empty; + return None; } } \ No newline at end of file diff --git a/Prometheus/ExemplarBehavior.cs b/Prometheus/ExemplarBehavior.cs index 585ce293..136eedb8 100644 --- a/Prometheus/ExemplarBehavior.cs +++ b/Prometheus/ExemplarBehavior.cs @@ -16,4 +16,9 @@ public sealed class ExemplarBehavior { DefaultExemplarProvider = (_, _) => Exemplar.FromTraceContext() }; + + public static ExemplarBehavior NoExemplars() => new ExemplarBehavior + { + DefaultExemplarProvider = (_, _) => Exemplar.None + }; } diff --git a/README.md b/README.md index 07b34ee0..d0189070 100644 --- a/README.md +++ b/README.md @@ -359,7 +359,7 @@ You can customize the default exemplar provider via `IMetricFactory.ExemplarBeha static Exemplar.LabelPair[] RecordExemplarForSlowRecordProcessingDuration(Collector metric, double value) { if (value < 0.1) - return Exemplar.Empty; + return Exemplar.None; return Exemplar.FromTraceContext(); } @@ -861,6 +861,27 @@ As an example of the performance of measuring data using prometheus-net, we have > **Note** > All measurements on all threads are recorded by the same metric instance, for maximum stress and concurrent load. In real-world apps with the load spread across multiple metrics, you can expect even better performance. +Another popular .NET SDK with Prometheus support is the OpenTelemetry SDK. To help you choose, we have a benchmark to compare the two SDKs and give some idea of how they differer in the performance tradeoffs made. In the following benchmark we have 100 metrics of a particular type, for which we take 100 000 measurements, which are divided into 100 individual timeseries. + +```text +| Method | Sdk | Mean | Allocated | +|---------------------- |-------------- |-------------:|-------------:| +| CounterMeasurements | PrometheusNet | 78.468 ms | 384 B | +| CounterMeasurements | OpenTelemetry | 1,346.181 ms | 3 889 472 B | +| HistogramMeasurements | PrometheusNet | 208.221 ms | 384 B | +| HistogramMeasurements | OpenTelemetry | 1,416.000 ms | 7 522 320 B | +| SetupBenchmark | PrometheusNet | 45.729 ms | 49 337 312 B | +| SetupBenchmark | OpenTelemetry | 2.713 ms | 28 491 544 B | +``` + +As you can see: + +* prometheus-net consumes slightly more resources when the metrics are being first set up (preallocating data for later use) and is effectively allocation-free and relatively fast when recording measurements. +* OpenTelemetry consumes slightly fewer resources when the metrics are being first set up and allocates significantly more memory and consumed significantly more CPU time when recording measurements. + +> **Note** +> As authors of this SDK are not necessarily experts in use of other SDKs, please feel free to submit pull requests with benchmark improvements to better highlight the strengths or weaknesses here. + # Community projects Some useful related projects are: diff --git a/Sample.Console.Exemplars/Program.cs b/Sample.Console.Exemplars/Program.cs index b8ddc10e..29f05050 100644 --- a/Sample.Console.Exemplars/Program.cs +++ b/Sample.Console.Exemplars/Program.cs @@ -29,7 +29,7 @@ static Exemplar.LabelPair[] RecordExemplarForSlowRecordProcessingDuration(Collector metric, double value) { if (value < 0.1) - return Exemplar.Empty; + return Exemplar.None; return Exemplar.FromTraceContext(); } From 0bc5f968f812b33ec2dbe3fa4fc752d0d3e3a3b3 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Mon, 16 Jan 2023 16:10:04 +0200 Subject: [PATCH 062/230] Graphical SDK comparison benchmark results --- Docs/SdkComparison-MeasurementCpuUsage.png | Bin 0 -> 29214 bytes Docs/SdkComparison-MeasurementMemoryUsage.png | Bin 0 -> 27757 bytes Docs/SdkComparison-SetupCpuUsage.png | Bin 0 -> 26970 bytes Docs/SdkComparison-SetupMemoryUsage.png | Bin 0 -> 25858 bytes Docs/SdkPerformanceComparison.xlsx | Bin 0 -> 28059 bytes README.md | 21 +++++------------- 6 files changed, 5 insertions(+), 16 deletions(-) create mode 100644 Docs/SdkComparison-MeasurementCpuUsage.png create mode 100644 Docs/SdkComparison-MeasurementMemoryUsage.png create mode 100644 Docs/SdkComparison-SetupCpuUsage.png create mode 100644 Docs/SdkComparison-SetupMemoryUsage.png create mode 100644 Docs/SdkPerformanceComparison.xlsx diff --git a/Docs/SdkComparison-MeasurementCpuUsage.png b/Docs/SdkComparison-MeasurementCpuUsage.png new file mode 100644 index 0000000000000000000000000000000000000000..6f8a37af85a5f3bf4924754ac452bfbb4cc961d2 GIT binary patch literal 29214 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 literal 0 HcmV?d00001 diff --git a/Docs/SdkComparison-MeasurementMemoryUsage.png b/Docs/SdkComparison-MeasurementMemoryUsage.png new file mode 100644 index 0000000000000000000000000000000000000000..cff28f8fa2227141862d70de6516e31b8d29408a GIT binary patch literal 27757 zcmeFZcT`hpyEp8Nj#xoOL20oD>Ai`76$AoEmu{%ir6pp3j7m`uQ6jymph%Zq0z^Pc z2t_&}1f+zXL<58pLcR_9oN=6UzO~-}&U)WrErslz-1k*}SG}*DCpUF94({XGw`0eS zgPJ$48SL1x53*y&@4b6=0#|$?0g1q`-+T--uI$L~;F|?r>~gxSb9u*(!l?Zl&_96J zeK#RS*9Qj&2L=Z4c>K`N5b*!cKi9lJnVFecTU*=N**QBqdw6*G`1strcP}U?=+UD` zj~_n{f1DoiBqK61@_AI&i>U1A=;-+P_>`2CH*el#W@hH*<`xzfmVE3jD=RB+7(k=Z zA3uIdJyBlQI%BO_nFd>LyjB9TZlFD+)st8=kdb8~YG z3k&2xDROT!g+ifHsozrUX{ip&%ggkk<~0U$eFV3@zRskQn2QTcCUcXq#sYpAV}Ak+ z?)5d$HN0{AUhL>JeP#1>T{HjI#k)7me0J=R{Ox%kQ zQ9-{W9hCimPVRF4Jt3L>#Ga8H<&0A{S5IDg^^L{Syb)!T?O*by_kKW1^S4@@IC|1R zQ;3+4Wi9Ek=4^ls{yeeShotMV-Xiw^uOLe{OcWCMV+UlpQyxOudINS33iuTtDY+MT zf&cf#|G5k{vdMIu+V4TbjK=$cOmf-|eK(zgs&7$VulArmcHXlFrC3%o1j&?v<^->R ztEty`Oy6Phf?lIl@`@H_8r7UHGOy#zqO-V4CZk|7^trSzl83<$(hAFX2P2W)Q;YSr zS1lQuAEVY~f15PDPbei%HQHPy&q|ngL_Lh-9t%a)Yl4b5g0VYSrdxwD zFJk8K>;cwkc(KMR)?=}`(E7>BKqBLbCDC$;o%Re)2(a(vHlDqK^d^V-E3ydG?@+*uHkCJz2<8 z3?4avI<=pxLyG}so_!=xC<}ux&K4u38~V$JsFn-Mi%^4nvGtgSVfXeTPs7L$)MyQ> zYn8jg4%VMK0})U}tF%sPJ>RePR-OGRmUqwqB6A2#y15vYT(oI5vOKs4)#Z42KaJ4#e^|jgp@WT@B z&X%30ndXzgkj+0GLNt|Z>%1sC)#ESTh3$N_uaoAdZMBqH2{OomLA+E68Lm2a+``Y7+}eAnSvw z!+3L*(uIcE+``4>5~%`fH7#OQLxzTrPUk5x0KXfaR(LdkGe1)h^xN4A@pVa^#(h^c~Y7wv-nZCRL3Sx z>_!rCRq*nBwU6rnQ8m4O^09GG&muX=1wI5SwAIq4If}qavzB8m8TF-d+BSw{3vBd* zb5x>Yhvl^#*OVZ_c#vzeLJ$bBa_sJpPmg-<%Ju5Rb1N^ma$H@W`6{?^)+RQ^RN*82 zaac0PJqZ$zp#Pu#?MUcb@?;*znvfcG^>vk8`+nvGu9`N@-(T?MWuObGPU86=`QR)O z<=HN^!dgoB_4lmLo_djadO~gH#DweA1E|JGo|^7I$~XrurIks}zU#C76KyXDf=^vBYBsHm*R>4x6L_MKohUtB@Tfzpk zmn+L|#H@7QJ?${m`&0HTd5x|UBcSZk7<|ru8GiYEUulNj*ihoz%q)6=y(TMq#e;8J z+?IMF=Iv?U8}IZm(?N*vYW*y*>70w6+9wP_e`6(}>@XS=c#ZmC#~)NJXz&q!a`O!~uQAa8R*#2`kO zR9;$NIiM&DT@2EC`}1*Q2bUmHS>VNV4I^xH z$J`Dp*TJ&+Ik2ZGqqtuETgq!RNj((X#ri$H<19G?OJIiq7@ zot)~QIpFas(DF&CpS5}KU&0s`O-qG6oa|soYzIHZ$$Wii?a~NX*t?D@Uate~lqsOd22bu1!#0w1^$Q2?=5! zEa!b1{-8Uhf?M~n46n3;o)oOHysTUY^Ost&2gE9?tXj@8%dnC@>vG{1&HOhf}+kudy{+6{dA-o^MRd%hF z72mM%zby>XdKp4_eD`YQm0H|DhcqqJMRe?2#to0PEDe6`ZZ7b2-UE*mXi*`g=m-zg z+hCj7`~7#t4pb0Nt_{sOSAK&Q6`6-|O|!YgbDP;qW05&;#@oj*nd5%6Z=^D60$iPZ zpU@l!V7;;DMb10Sv}r3}jV&#k%awVx_WH5%TgS~S3Zc?k=d?p(1MW;H`X3JK&vMJx zLb!R|+{&jm*U;`E%H>tQ;gJW}D2gUx7g{WE%?~zydw!QzN@z>rYKXw_>ITU@IERq0 zf7j-|DvYihR#I%lY6lU_0;Ak8S}Xl9Ht*Iax%THxTiRllUh-yt%Lz0PTJIsBJKuy$ zRF6+a@4ny%0ClYRnWWNdjb0t^-UaWz14|oP3F)R!MXHumKHI!IVla5OlaCub z0butomQDT{irec>iv0cK?0bO#jaNge^|qFGcMHkCwV~x*XTygZbHs+?0z#_C-dD%? z4BU_9c+diqXgd{VsI_8IGVIlV*F2U0>o3C>*}n}`pjG>JmW02yJT+Qvp|^e_yD&GW z+x9rMXelMB|2to$!I}z*MgxC#9R>?7J+7MNzkTPR!i`ug{E=_@@QkFHRoC39zo-o7&yQuuVBuBl;=?VH zCo-eYJwx_Wrn(qvFS6 zA`Q-IV7Z4|L$!-^Af*_5iv9Edf?HL9&(3DZ?3q4nlsf-eU_*&YOTGO~ZVwtnk#=&K?&tji@ z05xydDbqX4iaJ<;{DoMn(AAf`xrKaRgwj&%drxcYs6aYwc3ROnQ0a(k4U_DW!JlbC+vXkbj zHL0lM&H;rgng(ZE8MD(5^bLlT3{{wmRH^rS3PrX9TA1W?*?$hjbFL+N3UTx&CX&m^ zjy}a@_Ro3UDy1Svinfy*p>CbIgDAj-dl6r+hh+MUU`ug+WqNs^TDMuDzk9L8N_oDe zEsu3YzoRoG0tkw)&w+*%J8pg2^b_81WWk|$d|M--0fwgtNaRf&m=;MxF{kUY{FOY~ zqdV>G!yj+;zR;PBxNEK3TxKF}`R#;;xI+H0S)Q$;=|ZagqHBukF%n=7`44ji>+W|M z7xhW>-HVd@To)-~SD%rrn5MzykNYIhC0V)iz>3hsM3dENoAZI!5V-aOD7DXx(?@5) z%VF4#@PP_dbG!^*g*rL(q?Vo)bIbpNYoBc2sH{h0$uXF+wEnSrDU%M%?x6X=kDiastDv{lFIkhy)TF3z3CRbh_Q+Ok2A-(cM_OR<4 zJBxXGYGr%oQNyP-erM|KHwpaxUW1V`Mt1Q1-2S)j9JSKtKp}D;umky3=)O4B=N@Ic*-T0`L*~Lb@NlYMpa9X zi0C=xCBAD)QqPModt=pS`Bk7xhLHGHN^ILShG&5CXj=iN;$Rb{0#FnpxyTES39+z~uG zd$1;Fy*+nwOJ^u3+bdYIvXke|02i2a!!*!``t0rW{9Bs6ebk-!;om|Ty9K`EX_ZS_Nv3ceDMf94zcmPGw%r%quxahviQ4 zP<_U*%ex?6UAVJ)ZPMjb$im@+s4Yjk4l{G`W6nj5mF7QH$3;3X#YP7KDL-HpJnX2v zV+Hd8r4E2e@VcedW)_?17|ti{HiZx@Nopx$A+GMRO(i#CL{FG|=Xg#i+8s{G_Y+2% zHrBA+y#=5tze@A&v}w|(ljtE@gxFkH9XZSu^QzO?V=9qAXuUNlt|r9(PLU#ZHS_2( zvm0f}DQHLY2`k*$c}7{#LYbwKZ#R~-pNm;W9&Xq_ zFeDo3`~rv=C6gwjOr-lZyAW1iWdw*q16PkOv>X_rLd6>aB6*ca17b=){t?Xj^ zhPP}&?2NnIYVX9&=`xdiP@&CZ62rUG8%Ql2jVm50QN+g|$;A%1PN(baMR0qgMG-!#xwyk*THWq!gcx!>&a6c25?q~P(}AqzH&>#<&MN_OV(@DCTb7-bb2LYm8hsu_H@inxxBQ+3^VhJYr|+kAn91p=c+QzgNe@(cPle@} zS$BV_uk3bh?@s30PM6oiNHZ#n+my-K9Kf^2u?!Bd(mD%JUx;f($(_eThW-qe+(UxtrJAcon-XOMY9LF=? zo04U?w_Ne$7Q&FMw=-XKvx{5LTot=e(SD;-VQ|XP>rRFrHj^etIJn-J3G;L%?Te1t zz^sTndAOPesIX$s$XW5;^)Ckngt;tmVMUq<8a7-K{ zlnvGcg2q;Qd;1`+tBMx#uAr#dp@O6?d!O(lF;vKUyx(I~Z{@ZPz>n)pM}i0tJczd$ zy;`}dDa9^{_eXk*ffBqm0Cc4K%CvyxRt(oo(#H<8PrXK$vc*~Ydj3#RYk5p7_ddQO zTfJ%|i!^hx{2fg5F=u$DMNU!tW zduduv$ea?`4E2Sl-8{PPYdTWY`rvf{L)pkqs8Ug`K;QT383?6x_fScwW_fzy%e)Z> ztTy7z=WC;afe_QKZs^)$}~adBc(GCQOJO{7I@}erkc$=M8_NrUYIKzBFb6+TZsDi}TJPam02)A*T;E@yPAL$rB z?0T@|4hPCZ!wNR9)+pgTdj#UITB*_7HnW52ItVB}8FL=w`SecZYjx_aJRp6^XzKJ$}(^Qcp|SwX^L!p}!2;S-0bJ7Kf#MSozI zoVbF-yG!N#?YdePrH-#3ptU}|PrlUqlImbF>OryB_@dQuq;-EOw_|R8S{c=b;8eb& z@d!uns)hKRr2gH-rF+u?I4xHj>+};_wiuGf!iSggns+dZZ$4$c>|359Z4T9BdIyF> z;z!P8Px=12DYp>%DV+_qS8YRw@>EJp;q#1D&N7e$O#(@fK_tJ96LnKxtFp4 z6Slh6Cj%5a%jJ6MpVrwRHgY;K3X#@QuzWlCTm#;*_{Ei{wu19t>Puy{86{aUzBz?U zO2%hmhv!BVb*;8)2Bec+!5?84wv06?xYZPfnFp(toJ#Dfa}RU5U&d@WlZWv#3t{vx z$n6(fz1h1uJ63hEz(2+Ap8W2c#ct~XPTo+%P@QS%Sw*mAio5dV(Ux@|SMvMLtm zW|ABOH*0x$LgkIE?iPw88%G9TyBpz_74A>FM2~$jEl;zuOV>T+`?N zHGDPuXLRVO14z?j`RrBl``CHUCCYZQ!1jfQfqzu=f{|ZF@d_L-46nOhsHn2o@!ral zA*2D=9t~|1$-c`?J~22uoKCMbSxZtr<0T6reB2|x!W7fOXO{Sveo)? zK;4`@rFeczIlLiSzIW}eIYcXSR&(*IX7d)hF>l#NH#v4OMQt3k7o9VW)Q5-`q#UXy z`JPVk=ZCCkR%rKo5md%FVKs7%OUx$o>gxj1Sy)L{AdZq#sK3uy#A4~GWMwnY9-1Ma ztFWp2%au2&{i@f%9dn1z$tYi<@@xu|Wh*6%jq9~Mw=}l0?Njq^tdQS$-z!q&A4p01 zJnE1$c@mk$kDdk^+9UL=xIu$~CqHVurVa;Xn(H?Wub~5}|HWl?MlhC+3(fV?9svrB zipMqcM)FpDInVFj05nJW<{hY?Y$Rq~Fko8R^IlD1e%X&{nM`c%SvD^-%$!HZwO-CB zPg-6tlYSgTOgfT1-4h>Go9l@f0g01dwS8O9_crQt;W8hu)l-;q{`T!2RK8Zj72X-? zX;|$2n!3VWrX@H(9~L{7iEbLVE0(_wDgY64qqvDU*WYys!bH1+ldAIWlpMQJdXu;_ ztgJR~*t>fuQ)5^0wm1eY2fG$Fbh*|^u$mJD@0!Sw;;kZ4Psn>E$Q5jyG~?sr97>+R zWmaEj04Ev?%EDD)d?M9{A&zN-9s1WXZp8gBLyJfea8L= zDoFb?an(S+>~$E?pM{nWcpv$S8+t^z#rj3-3&9=7iZsf+^;NkbWzJI1cm?r_t?s9* zkhk~F9R|OyE9c91R6*yFX z+cWtvVvIAQaF;i+?X&+Iy9{6@?wBS`8AZNe%{wOrL*Z%J?6;>qlm@+xC1N##D&Kkc z}WgbrpuITyc(5HcB#xi2Ppb8 z8LIl#F#Ka3im4GZUkUURx8EYmtD#hf3LIgJlzf6oQ*NmK<_g`Bz2Kf{l59~O0Bg2? z`DoZ#{+ahfK@2YN^B&YHqf(|VEJ*9H;yv%3-1(JOwG%hkDmmTx$jq-%O0vG&Z^53J z;~`ZL z7vu=^h1?Ki-7LSFb^c)XK^4=4M_DsnzI#wZl4nJ(UXOWwx%5KBSa+udq=7~FFT;?nRu_rNjZP+-A-m-<&$|AOIvgWItpx<_U9%p@;icNK8zcdo<-pZxdF|K)O6#WhS40FS@KeG>Bf4%t(XD;p%JG_D9EsB;AT+m6IT z;8Vaci$qfUy5r(1B~ATnq5CRLqAx+Ra+xD83Ye+<94xuF&?$bS!aCuipsoP3q{eFX zh_(k)+i&Vz{n;HmFb{zHHu2RQ`rQ|pUr{S1bW+UlYTR6#_P2&f7-I_5c>M=h21Otv zgUN7jIIF5sF*eMrdlJ{A)LJCN`Y`{gC2bGN!tg!vIFP&Tc(oUL#5PUR-*e|A3RxuA zC84~cH?D{+a%Fwk58VFK20-568AxqGk=yck+5*fIHa+#(_%95mjD{N?ANTq~ey^&QhZc%r7%Dlg`$buMEs5g&FJ!7#!S5 zqQd8wrap4XOgOFk& zfg*8U6iwsM0A?n)bqX*B2gex0&jlrY@S7D#JV1vQy((_y984e9yfIGjO8rfQdNDr( zdq3VSfq1cPzf0iqr#~*wXCFO!HPNkY$qjTLR!APl#>|1i$fTnB-vbjh<=u5J{ZL#y z2rNO?MGK=gH_md(x*rW-2Gq#Cb|p!_QOWXlk5W)ex(8c$^d31)UnI3vTUILhi$TiT zJ-};U1~=!r^vkN4i<`G)(ls&793bQZ^C_)iQB20YoTKa}PE`)o?ro*&DX!x!TbxhZ z*cfc-e)N~v|B-M%iA3(cPkPaK*!6bE!MA2(sc2DYwp%IF-V0% zE-=ASL53lT|CTMm=u>U$$nfP9UIZ<9SiNceLs8mhlc0)hn+Wintg5$eFxH4|GlnRBT zm4$&%c%ZZnwGpyz@j|^ykwzsBcVYK`zzd5nan7!F?XdQm5J+GPQcL4I)G^YI0lS3W za*UD)+u66KOje2`^&HB97Wu&z3nF~23Z@(gyt&h+UP-rS*)z)}^i?3K$25v{gf^eo zWc3zajQ{h8F-j>zh9-d)zUe7w0(V1@GOsduv#!U$XhXVZn0UXHdQtrP&2%2Hg%gL* z3T7_9o6g}L>PsryT5t++>!8IkcpJiM1gdo8Kg4cAJy08S(~!hq67ptOaQ>mgWX`0I zZE8;Q$^PTsmTM_sm352#Ph8^NQdHWAEEhS5^)#uhqjwML`gIt3zs=PoVXLL&kGaz< z%E=7{*3{@m`Q~enFav$RI0@-M*))wTlf)NZ3xsx$D76TPVHBi#brOKhkO#jE zFgrK(iYTKj9OkHC>7DQK>lCo?hNM4U(e<;y8ofD4{V0CzUT!eP)Z=jHTP;ptZ29_0$PuvLDad$vI|qusJGyU_vjk0EpmOuX z9GC(VP)D>gK~t^Nh5Z11(|~|oND(j)w&LmFS$doz7_*W;|E_QNe_}_~LvB_h##Z3y;oCX)D zq>_H#AK(E-jtOxssagr6(M2b~3XZ8!x$A~1XZ|K7@?kJJAG>gx!`KtNeoXC&oq&^* z_zQfS(28RT=3)!%Na_oAu}fxi*xdr{G!wK}Xs zi`!&4@sRVyEM*!S2BSwV?z>-8#*h^p;dviOw*?@lTyH;r1Y2u_8)+EcFx-zam~F+r zvHLsU$G~t;4}}y(Vb+m1{2Au~vI&b8pl#X~Vy2BsdsW zQ@aoDR&8VRl4|LXu^#Z)iI1u2b2Ut=FG@VwB>;qqncJLk)OC z?H%5~mzI4m|DS@XH}sU*3E}JmK+ENOS#=0p3cX?dHv?E z4gOtfy9E7TCacJB{Uve~@=LWR^AR;H0xiK>rC$A|^K~$N#yHk$HLTw*mcLS?(=1k{ zDhzq*7bV|0etp2KjLwl4`Q$m}#X*z;Wz6RBmWTQYK-s`EEdER50{zt?HF3CK8f;08 zQEymn`J34~DZt=j%)!KN!iYTX-j9;udd zB(+BjpPxaX10|_=p0|j7fz=+A^0dt-PEJ+jR6>>M$mEALoa3Tfni(~Uck$Wy?J7jG z>8hwJ5aRHnt9W|nG)oXsi|)hM_b1~Rn+`N#C2Y!kRqTiLmsq1s;5Gf+^#y-024A1u@V z*1Z8ooI~)ZK|WRck9d^BINl}UswY-OaUB8zWC3hn4mN#2Zv|%aPQvf^U(|noFUpDX zNgev=4dPTVcJZsWqG!HY4)>S%FXi4V=^Is&5*1+_VD2hI(I~R2Ixg0!8#*FmttYV2 zU?F1$)2V{DIN>3#-S1SMZMtix1iVW6g(a94G(CQG;SP+SY)@%-{IC$delOpfvpJamMwN z*Hb}ah?MfPA5j~@t;8HCEZ!O&64}gXp=~$6fay6 zc$scHW``!GmIh-p4dSc85OJQL%?waKT+e&DxhJz)9|!GD{d}u6A=3}MgAk>U0Ry+9YyFil2EK*A`&XwAB~pybqj5f$MkUbN6e!V)$Vyj)0lMjXQ`(Gf^texXwc()w05 z;cSCChx%&A^@3jx&%AjBMZUA3?6FTDf45$& zR7zXm7h=jF3KgL=iNAgBaxgZG6RvlX!HcJqdo1`z$2lerGnz*Xx06l@eO5C=42qUfWe z`9z(d{D>qJ24~eKK{E-mB*2p`FRoA1&*NkCqeeM+Ajx-~D7tI%rA?c6m7c7I|wWb7?}!>mP?M z-DB?qt})7#A)9=^IHBhz7jm`KA{GPXC(bXA{1Owcn$O;*O(dYbK2sKjaVBaoSv2P_ z-u;FMf2s`XWPyBBWX)F#@d8aCFZx>m%ZL%D!>fIo$4*m&|GE5T5IYbANn_0`ZuxsH*@|uBK)(;h)mR9nqPBCacG`rz}3bIG=4EIvGf_H zek|JPh~VB|(m=bT%nOT`4RSYEv5d6}%hB~P%)?(yIs$Z^ygsg%P!34k+#J-!jirkf8jcg$xGQ#6r5P6#Z}i}0K;RXObAPV&K6=eK2~=WP60l|-!z zx@OTd%>9d>I~t6Q)V$};*o_fy-`~*{kw%B620!{T^$o=wfLz3@48FRf((1~wnLvsj zU&%uz=Glv@r6uu9i?$2@lR$2;=z5rg_Y*iQ$PTU>y1!bvAwwr)dftxFZ>mhzOBK@@ zlWtha@~;?i^U!U>gMLxy=BDJD!)am&((?TuDx8NKoS*sBXE2}dV+7S_@C0lARf=yg zq1_a}i_0AQ3&1lWz2`NuNc)m2q1^(41s_5Vf%{JWG~S?oIsRg2Q4I3ds@=m>m7`>m z)6I^9D7q}R+5NIO`V=Jl^3RFzLrG_ez7FaTXF%eUv9lVL+zyIP()N(Wdv5V1nVRyd zk0i5p|7-~R>N5@owxeT{R&Q;H>O78>KN`B>BoGB`@OyGo!SFw6DN#7=Ns*W3j0pDk z92xVt?jilIGu&HX@@D*jl-opaC*NOKy#su~1GqQks3{@9awth{2&CSI8d>qGKs?8W zs~!!C532!EW5E+a;4UsxjG7YE;NZ`hFn|hTjHaK)3T4GZ)x*?#tnA=(N^=R^hJJ*f z#ruT5OM7nwvTYs^=Lq}xVblgg7N>hPF<|U>KA_e1*Jz3UVK8Rl0Yy}~a>z1%INkvc zdqa$?sJcI>I*K1Wk2=7+qnI!~?aC?Fj$qVh@Tp>ms@5$OEcc?lA7g`F8Ca_XJB9j1 za)1k>5GY9p@~wJ^1sgu7-$@1@P`FNbx&r5UL_H z^F79{j!9d0&K&F2nCJ?r$8}l>fXSsJr;aB2(yI9F9B;BNSgc%@VB)zFz6PTG1Z~Vgz+voFFXzr_dR;)1 zYy8br3DsYW{(JA~mmKT+vHV{}*8dY-rp8S68jSsi=z!ixtN?gaA>%kSyf_q0ynPr1y`++D?r@EQtA)!7lul+zOt=w&J*5&C{em@lz4QiLIoXJd;jOggqt+Jx&_w@| zq_e@7wZ(yERJ*CFPeK;hCq>OuLB1}Ku;EuM_nL+WH%)&PK-{~BCJeQ}yrT0wMlse4 zyc=D%%7iC@8}2*P`QkSRJ#=7b0uQ~VBE{q37u*V=W(`ha!OW*zfp|9MfG1U0m4CWa z9?O>! zXsNMYmP7pSt=wNGL7Q#@Yj$0Ei=`o&i%qFJdpP$=Nc>ZI>hZo$T+0!1QN(ALl;!l0 zXzIkc7}~wXfet+30!MQ%es<7ISm-R4EhY_M0}>z4PTWy}te=K~pI4O(s@hn@SCdsF zRbJoyCum*o#~+yH4FZu8a1g5+?3oE{M$y+#yCvejX!!&sz$fD>2I07!oH|jH4;+fV z)F6!ZqsAIMY0czCzlDU0v#Qen$pHU;~LmJAM=a&Dtk zy-cFSzvmWjEcC4PiUOV|Ndr`ERKO=1F?Ej>1i23^}Qbfe6yd z%Wh!!z1$2|-N7FwT~$A67<8{Xu(nLndDO*vrMVLhnreT>=aqI-@t>AisuwRS)b5bq z+Pg^ORC>yE1|0~&j+b40Hrl6j988#Y1p@L5FWO0@A05T7bnm}sv{Q1H{f_Co)Y!Be z@h^2%kAih}i@|;e>~xdOZFhq`BY+GA=A>(i627(tjZcT9IfxZQ&L>MM{V7(lb*`{v z>A?Q(4dA?n1R;rnW64^e!js*TbgCiRmY#GvXEy9JNx#`AsTvfP^Myq_=4 zW~g63X{*D5f|jS(e0zhIu~T}C7kOoM@hJFSrS>Zv75J7P@JIgA4Y}mdq+NcKHS{%# z)^FVz1n>Bl(hZT4kM@Sd{nK><#vf{NZK!vKQewCl<>~0X>gKCP28a>Yw9d`Vl@v1wYIpnm5peMY8v^x4!XnJkCFSP5|0dA%G)xXn$qK41e&GA` zG!luw(8_7LrjiHkCSPrP*6+%QVnkk#n-WDR>1?e{)la=8IF%<384{c?4TR(L=l&-l zTO-#j{0nHK(OF?wVw&N~Gg0L@-dKxrb8Xy|+KZY9RK&OD* zZ$ijq>}5x^P)OW==sVMQK%OFBJL(EoN(fH1`vv`kI5Vim6ZZjK>lh2b z=onZz^(9tExcb&VJ$bI=6SIHB*}sn_13~$hqG1aizvO6Jf&G`dW;2I(1^oK}#lKI8{I7ciX02Ima||T3f?-a8SXi}Q z@=akaU>c>VWxZJTO5tajbG35D-fYQBE*gqqZ2yOO+u7`KE%5j&HL!V(nJx5GAd&EztvI1$+fmaeHGA=BHaK;sABPuxVWl zF(aPUy0ex!5vP;P)GDVj%D*e=fza~$GGc{kt0u%@=IQwLg1-z$tw&pLaTe#JTcm3r z+KaUDZiHheLLVa*S7|*L@0GtE()%GkNBc5Tn546XscP;>Ws}a9&L12^5iG5Pa<@c! zUKNsSrlNTalg8BK=?n5z^V=xY?)KG%#Nit}NU5~D3*P=) zpXOT#lYEj)EcV%StSyPDpUg>^3Ugw}4Y}(zZ-c7=Tgnf5>WMzM%%x5C z;n22%_i>JxyfwUetrV2fn!a~i29EO86wmbDsJ4~MqpyGLZpNi+@ z5a)$*$Fns1B$It8)5cn5L|gNJD2?M^bP$}O5CUvqQ82XL;<9mB=7iwZwr$VCDP1}E z~yvC7z-*uS4 zJUt1kYnlU$&K^w`IQI5Yf4TfLy-v%g7ndE-?`@TBMBsUosliD8t^7T{B>Q{*IwXD!}xX@m>@R7mZXH4NMfIBZ%L3mm#6Q>Yu2g z?Ued1zXe&KIR@q$ZSEvmKmJa&UFdEZfyBNUwVdv#g}6}l>A95SpM(YXF%EA+zbiPD z!|;w5w!o0A5yZl8uY059}wV0<26B6$#)~-gdQ=Dk@UA% zqr>c@&gv#?tt!oHLhQ&u(YSh373}pC__mp~EK4<|wFp2&R9jN7*l%t-AYD<%KT-qZ z8th9!{N;Z6wOcBA^C7(!dHmD1(#9H>j==M6Uk^pKwR*t5-#=a4h)!{w>UweU&^wS- z67svULe`EXq)FXAlEQminAxHZK^JWU@;@>&B?;0+UC;^NceSVYio$O=8IK%`rXK%( z$=~HRptr_+mpBxaWU20sns0TF04Smya`(+2`48l>+sZNqi{b@6qGG>F zM5Y~on)cRjTu zrD5{=ThQ8=_r{i|)gLS^uQd0hl#i`ie)lnLqi>x%1_;2@*-T()o^Q0$TyXb@mowbb#`AkTzCjT9M&^4hAHAG zW2dL|LsoP_LreMxhN)KA#Woq4z-AOg6*)GkWwyeKrVfH%c)OTHmtsQ{B%@ZOVXM#AW{ zW={*3Ga>$t@-p9Tbh7X0yyYsR*U0p}%?}%e?%vBv0>IV|^_~tAA(8seDq;j&MS4ZA z0bXP)j&p_x$>8!++f7fdSskyGep_{gdFA^C**-mwm{%><&Tj>_!s_^_LS3dlWoVz@ zeyJ@<@GV40uBF!w_m6(%j4e0<&Ov(qf8|~KTasxSuDQBar<}CRS<9K+w$f76Aq&sa zl%sWdU>Ze6O~n%^8mzAAC<<;GmTfs^nWo^Zt=Z_RD3}?dQ<>+(sCmF+;b{%fOPxSO zU_ZW>X8wr%4IZxV<9VO=e(w8zxp?0Xnos=N*Cpdo68N+zzM^ zbTbj$pZ|Fv>BtOhBn+<~%ux!Gw42fVhR5^@`(Txx_`aDu+Z}?Q%yS((uDx={JSjm!w>irHZHL2eB_B z=k(kaLx6|sTU8>uWgpYqccQnDnClB`lNZf0(49DZ$VT=H#_ZON%bi zAt(j`VfEPj+6bYHm-z?E%I=cARBU1?kcO_)ixyLVnu*IO{)o-ALVxFao%PUO!dNh3 zOzdHX$<@$-jsiM|{{+WnU8w$|T^|`#5FQlHx<@t)V+ll9h;KAKv!;_)^d|K{O+Od+ zTV;3I2 zF-%T<1J|rfjv8^ckXWHi3WPx<^5+8z-TXFpMEW^aVY&jufMB zT5ay@AFjPl3>y15?w ze7eZq+H|>Y%zxr&AV|%sHJ9n~Jx+Y#dE%o^fTj~tm5$jbKw$b-aCt~H@P~E_!&0xY z22sOA?sjrNxaUT?-^{+)R?7XY!R`-_K!Kfo zT2-=*xP64i2by>6HX-S9fsEvTvh zm|w=ln4C^h^>0V>Up{13C2MS;aU4x=$>1S|eX?dKH}0p8S|Y3q{VnHL3nw=dEpm~C zX98UGB1A!}9_z|5z>GklV;U<~_R<(~QGR>`9FITEiCidXL#=hjdo_ssl0{`vj~mrZzIS$y1#y&fUf6-9AZ|{L3U1z?=VV-G?*01 zkX7swlDPdRL6bXd%+AX(=l#ni==;H$gyR?dO+mZ66}n-Mdrhn+bf}t-D5yyC9yI)o z#&HGzDNVhSe>Ozo-4YrJ=ZehgTO`ZE7<-a4mKywIob5IRC}xza#q5K~yJtD3kcvWj zZLQczHur2Y75EPSo}Bm$Hl?g?=UM3_-yL=pul?EMSBL-rZ<`PFNJjfZE*-BfksYOn zi|_re(Us8uJ+9l|9~4f<`7Erw0Ic(FDD9l9`z`|Y{eTl4RqRVA^Z``aJEq+I5FK6~ zoWFwW6^H0H;-rdpL`IN4sBO=^(roCFC7b`A(RRm+KCDWKr;=QMQ6qxkX*XW!HGh^b z4)I=%Z#o4-67jh93!^{ZHc1*_iaewY#}@4YWD@t_L2S zb#Xpq{@VAPoi6q2=Tz)?6^k=0>d21W=y*W!0UrGyUv88$Sc=0c@_Qz8)|DbA_e8Wh zfThc>$Kzts#%6O}>}l~fn}|6a9LHMq>CJT3)e70h&UXkqULdB1q(z*H0V(ZET3`m z`;vI+-1ISuL|je#h~{zqu>B4P%XY8mt2W-C&%8Wo1{YH>%eQe_YAu}z{>x0f{aI%i zHcG*l7n-o~-FK%w-&RLavcL+xu-826EBVzG*P-1X^W`Y8302OtoEd*fs~4; zZ37BsIl}2N`c>?Nr#;Atzq!s4*E?V4Z*pnh9Yz7ak$12@=SL&WqKZffmC;-X0`H~{ zt>1k`H-fy~IpE=R#a66CGTmI*YP=Y0Upc_NW15q>Ra(E)q$SzRbOSSFG_|#9c2q5e zZ@=N|8fsTuab;n<9Cke7RjPt3XhyYX+u437Z?R2;8)~qbQf2vf1%B(2G8+6|A(hIT zkqdU@>HY9>k0F!HJ+1>SFCIt?P(S&r29o`%mc~Ux*3$-TjAAe%Xz7rKt@kb-g?6Ct zz(c16L1+CC{_ozHLfgo9WH_kgrK(u9<^ffO|1DK7qt!&{CH@XwWuv0U=H2%|l&QO* P$c~HO__)=a!(aO!7evGq literal 0 HcmV?d00001 diff --git a/Docs/SdkComparison-SetupCpuUsage.png b/Docs/SdkComparison-SetupCpuUsage.png new file mode 100644 index 0000000000000000000000000000000000000000..795931373f8efb0dcb29620f7ab0726ba17ef8ea GIT binary patch literal 26970 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 D_BxD- literal 0 HcmV?d00001 diff --git a/Docs/SdkComparison-SetupMemoryUsage.png b/Docs/SdkComparison-SetupMemoryUsage.png new file mode 100644 index 0000000000000000000000000000000000000000..15b977c3a301d3117b98f254083497a8a506c804 GIT binary patch literal 25858 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 H-v9pqzF*J> literal 0 HcmV?d00001 diff --git a/Docs/SdkPerformanceComparison.xlsx b/Docs/SdkPerformanceComparison.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..9b5427f0362b495213b1f2ff863b1d8ba4b90ca5 GIT binary patch literal 28059 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 a?W **Note** > All measurements on all threads are recorded by the same metric instance, for maximum stress and concurrent load. In real-world apps with the load spread across multiple metrics, you can expect even better performance. -Another popular .NET SDK with Prometheus support is the OpenTelemetry SDK. To help you choose, we have a benchmark to compare the two SDKs and give some idea of how they differer in the performance tradeoffs made. In the following benchmark we have 100 metrics of a particular type, for which we take 100 000 measurements, which are divided into 100 individual timeseries. +Another popular .NET SDK with Prometheus support is the OpenTelemetry SDK. To help you choose, we have [SdkComparisonBenchmarks.cs](Benchmark.NetCore/SdkComparisonBenchmarks.cs) to compare the two SDKs and give some idea of how they differer in the performance tradeoffs made. -```text -| Method | Sdk | Mean | Allocated | -|---------------------- |-------------- |-------------:|-------------:| -| CounterMeasurements | PrometheusNet | 78.468 ms | 384 B | -| CounterMeasurements | OpenTelemetry | 1,346.181 ms | 3 889 472 B | -| HistogramMeasurements | PrometheusNet | 208.221 ms | 384 B | -| HistogramMeasurements | OpenTelemetry | 1,416.000 ms | 7 522 320 B | -| SetupBenchmark | PrometheusNet | 45.729 ms | 49 337 312 B | -| SetupBenchmark | OpenTelemetry | 2.713 ms | 28 491 544 B | -``` - -As you can see: - -* prometheus-net consumes slightly more resources when the metrics are being first set up (preallocating data for later use) and is effectively allocation-free and relatively fast when recording measurements. -* OpenTelemetry consumes slightly fewer resources when the metrics are being first set up and allocates significantly more memory and consumed significantly more CPU time when recording measurements. +![](Docs/SdkComparison-MeasurementCpuUsage.png) +![](Docs/SdkComparison-MeasurementMemoryUsage.png) +![](Docs/SdkComparison-SetupCpuUsage.png) +![](Docs/SdkComparison-SetupMemoryUsage.png) > **Note** > As authors of this SDK are not necessarily experts in use of other SDKs, please feel free to submit pull requests with benchmark improvements to better highlight the strengths or weaknesses here. From 6fef05f779a69b0e64b7f6dbe3686195e38a2806 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Mon, 16 Jan 2023 16:12:42 +0200 Subject: [PATCH 063/230] docs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 94a67ef5..311ef3bd 100644 --- a/README.md +++ b/README.md @@ -861,7 +861,7 @@ As an example of the performance of measuring data using prometheus-net, we have > **Note** > All measurements on all threads are recorded by the same metric instance, for maximum stress and concurrent load. In real-world apps with the load spread across multiple metrics, you can expect even better performance. -Another popular .NET SDK with Prometheus support is the OpenTelemetry SDK. To help you choose, we have [SdkComparisonBenchmarks.cs](Benchmark.NetCore/SdkComparisonBenchmarks.cs) to compare the two SDKs and give some idea of how they differer in the performance tradeoffs made. +Another popular .NET SDK with Prometheus support is the OpenTelemetry SDK. To help you choose, we have [SdkComparisonBenchmarks.cs](Benchmark.NetCore/SdkComparisonBenchmarks.cs) to compare the two SDKs and give some idea of how they differer in the performance tradeoffs made. Both SDKs are evaluated in single-threaded mode under a comparable workload and enabled feature set. ![](Docs/SdkComparison-MeasurementCpuUsage.png) ![](Docs/SdkComparison-MeasurementMemoryUsage.png) From 7f0ef3a5dfc74039d8926c7b44318e7317465c52 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Tue, 17 Jan 2023 07:10:17 +0200 Subject: [PATCH 064/230] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 311ef3bd..805ebbfa 100644 --- a/README.md +++ b/README.md @@ -843,7 +843,7 @@ See also, [Sample.Console.DotNetMeters](Sample.Console.DotNetMeters/Program.cs). # Benchmarks -A suite of benchmarks is included if you wish to explore the performance characteristics of the app. Simply build and run the `Benchmarks.NetCore` project in Release mode. +A suite of benchmarks is included if you wish to explore the performance characteristics of the library. Simply build and run the `Benchmarks.NetCore` project in Release mode. As an example of the performance of measuring data using prometheus-net, we have the results of the MeasurementBenchmarks here, converted into measurements per second: From a14be07cb31c4a1963a02050aeae68b1383cf12a Mon Sep 17 00:00:00 2001 From: Hassan Syed <91477794+hsyed-dojo@users.noreply.github.com> Date: Thu, 19 Jan 2023 05:04:27 +0000 Subject: [PATCH 065/230] Make the default metric factory public to enable configuring metric behaviours globally. (#396) Make the default metric factory public to enable configuring metric behaviours globally. --- Prometheus/Metrics.cs | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/Prometheus/Metrics.cs b/Prometheus/Metrics.cs index 438e6e01..4a952c29 100644 --- a/Prometheus/Metrics.cs +++ b/Prometheus/Metrics.cs @@ -12,8 +12,11 @@ public static class Metrics /// The default registry where all metrics are registered by default. /// public static CollectorRegistry DefaultRegistry { get; private set; } - - private static MetricFactory _defaultFactory; + + /// + /// The default metric factory used to create collectors. + /// + public static MetricFactory DefaultFactory { get; private set; } /// /// Creates a new registry. You may want to use multiple registries if you want to @@ -41,79 +44,79 @@ public static IMetricFactory WithLabels(IDictionary labels) => /// The expiration timer is reset to zero for the duration of any active lifetime-extension lease that is taken on a specific metric. /// public static IManagedLifetimeMetricFactory WithManagedLifetime(TimeSpan expiresAfter) => - _defaultFactory.WithManagedLifetime(expiresAfter); + DefaultFactory.WithManagedLifetime(expiresAfter); /// /// Counters only increase in value and reset to zero when the process restarts. /// public static Counter CreateCounter(string name, string help, CounterConfiguration? configuration = null) => - _defaultFactory.CreateCounter(name, help, configuration); + DefaultFactory.CreateCounter(name, help, configuration); /// /// Gauges can have any numeric value and change arbitrarily. /// public static Gauge CreateGauge(string name, string help, GaugeConfiguration? configuration = null) => - _defaultFactory.CreateGauge(name, help, configuration); + DefaultFactory.CreateGauge(name, help, configuration); /// /// Summaries track the trends in events over time (10 minutes by default). /// public static Summary CreateSummary(string name, string help, SummaryConfiguration? configuration = null) => - _defaultFactory.CreateSummary(name, help, configuration); + DefaultFactory.CreateSummary(name, help, configuration); /// /// Histograms track the size and number of events in buckets. /// public static Histogram CreateHistogram(string name, string help, HistogramConfiguration? configuration = null) => - _defaultFactory.CreateHistogram(name, help, configuration); + DefaultFactory.CreateHistogram(name, help, configuration); /// /// Counters only increase in value and reset to zero when the process restarts. /// public static Counter CreateCounter(string name, string help, string[] labelNames, CounterConfiguration? configuration = null) => - _defaultFactory.CreateCounter(name, help, labelNames, configuration); + DefaultFactory.CreateCounter(name, help, labelNames, configuration); /// /// Gauges can have any numeric value and change arbitrarily. /// public static Gauge CreateGauge(string name, string help, string[] labelNames, GaugeConfiguration? configuration = null) => - _defaultFactory.CreateGauge(name, help, labelNames, configuration); + DefaultFactory.CreateGauge(name, help, labelNames, configuration); /// /// Summaries track the trends in events over time (10 minutes by default). /// public static Summary CreateSummary(string name, string help, string[] labelNames, SummaryConfiguration? configuration = null) => - _defaultFactory.CreateSummary(name, help, labelNames, configuration); + DefaultFactory.CreateSummary(name, help, labelNames, configuration); /// /// Histograms track the size and number of events in buckets. /// public static Histogram CreateHistogram(string name, string help, string[] labelNames, HistogramConfiguration? configuration = null) => - _defaultFactory.CreateHistogram(name, help, labelNames, configuration); + DefaultFactory.CreateHistogram(name, help, labelNames, configuration); /// /// Counters only increase in value and reset to zero when the process restarts. /// public static Counter CreateCounter(string name, string help, params string[] labelNames) => - _defaultFactory.CreateCounter(name, help, labelNames); + DefaultFactory.CreateCounter(name, help, labelNames); /// /// Gauges can have any numeric value and change arbitrarily. /// public static Gauge CreateGauge(string name, string help, params string[] labelNames) => - _defaultFactory.CreateGauge(name, help, labelNames); + DefaultFactory.CreateGauge(name, help, labelNames); /// /// Summaries track the trends in events over time (10 minutes by default). /// public static Summary CreateSummary(string name, string help, params string[] labelNames) => - _defaultFactory.CreateSummary(name, help, labelNames); + DefaultFactory.CreateSummary(name, help, labelNames); /// /// Histograms track the size and number of events in buckets. /// public static Histogram CreateHistogram(string name, string help, params string[] labelNames) => - _defaultFactory.CreateHistogram(name, help, labelNames); + DefaultFactory.CreateHistogram(name, help, labelNames); static Metrics() { @@ -122,7 +125,7 @@ static Metrics() // Configures defaults to their default behaviors, can be overridden by user if they desire (before first collection). SuppressDefaultMetrics(SuppressDefaultMetricOptions.SuppressNone); - _defaultFactory = new MetricFactory(DefaultRegistry); + DefaultFactory = new MetricFactory(DefaultRegistry); } /// From 312c2e95d36ac7f069a9759848c568f8ae8fec99 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 19 Jan 2023 08:50:57 +0200 Subject: [PATCH 066/230] By default, EventCounterAdapter will only listen to a small predefined set of general-purpose useful event sources, to minimize resource consumption in the default configuration. A custom event source filter must now be provided to enable listening for additional event sources. --- History | 1 + Prometheus/EventCounterAdapter.cs | 27 ++++++++++++++++------ Prometheus/EventCounterAdapterOptions.cs | 29 ++++++++++++------------ README.md | 2 ++ 4 files changed, 37 insertions(+), 22 deletions(-) diff --git a/History b/History index 664ec484..b4692ce3 100644 --- a/History +++ b/History @@ -8,6 +8,7 @@ - Simplify DotNetStats built-in collector code for ease of readability and more best practices (#365, #364) - Slightly improve Counter performance under concurrent load. - Reduce memory allocations performed during ASP.NET Core HTTP request tracking. +- By default, EventCounterAdapter will only listen to a small predefined set of general-purpose useful event sources, to minimize resource consumption in the default configuration. A custom event source filter must now be provided to enable listening for additional event sources. * 7.1.0 - Added back .NET Standard 2.0 support as some customers had a hard dependency on .NET Standard 2.0 (despite not being a documented feature even earlier). - Added (Observable)UpDownCounter support to MeterAdapter (.NET 7 specific feature). diff --git a/Prometheus/EventCounterAdapter.cs b/Prometheus/EventCounterAdapter.cs index 8636e383..a902f9a6 100644 --- a/Prometheus/EventCounterAdapter.cs +++ b/Prometheus/EventCounterAdapter.cs @@ -30,7 +30,7 @@ private EventCounterAdapter(EventCounterAdapterOptions options) _eventSourcesConnected = _metricFactory.CreateGauge("prometheus_net_eventcounteradapter_sources_connected_total", "Number of event sources that are currently connected to the adapter."); - _listener = new Listener(OnEventSourceCreated, ConfigureEventSource, OnEventWritten); + _listener = new Listener(ShouldUseEventSource, ConfigureEventSource, OnEventWritten); } public void Dispose() @@ -47,7 +47,7 @@ public void Dispose() // We never decrease it in the current implementation but perhaps might in a future implementation, so might as well make it a gauge. private readonly Gauge _eventSourcesConnected; - private bool OnEventSourceCreated(EventSource source) + private bool ShouldUseEventSource(EventSource source) { bool connect = _options.EventSourceFilterPredicate(source.Name); @@ -151,11 +151,11 @@ private void OnEventWritten(EventWrittenEventArgs args) private sealed class Listener : EventListener { public Listener( - Func onEventSourceCreated, + Func shouldUseEventSource, Func configureEventSosurce, Action onEventWritten) { - _onEventSourceCreated = onEventSourceCreated; + _shouldUseEventSource = shouldUseEventSource; _configureEventSosurce = configureEventSosurce; _onEventWritten = onEventWritten; @@ -167,13 +167,13 @@ public Listener( private readonly List _preRegisteredEventSources = new List(); - private readonly Func _onEventSourceCreated; + private readonly Func _shouldUseEventSource; private readonly Func _configureEventSosurce; private readonly Action _onEventWritten; protected override void OnEventSourceCreated(EventSource eventSource) { - if (_onEventSourceCreated == null) + if (_shouldUseEventSource == null) { // The way this EventListener thing works is rather strange. Immediately in the base class constructor, before we // have even had time to wire up our subclass, it starts calling OnEventSourceCreated for all already-existing event sources... @@ -182,7 +182,7 @@ protected override void OnEventSourceCreated(EventSource eventSource) return; } - if (!_onEventSourceCreated(eventSource)) + if (!_shouldUseEventSource(eventSource)) return; try @@ -207,5 +207,18 @@ protected override void OnEventWritten(EventWrittenEventArgs eventData) _onEventWritten(eventData); } } + + /// + /// By default we enable event sources that start with any of these strings. This is a manually curated list to try enable some useful ones + /// without just enabling everything under the sky (because .NET has no way to say "enable only the event counters", you have to enable all diagnostic events). + /// + private static readonly IReadOnlyList DefaultEventSourcePrefixes = new[] + { + "System.Runtime", + "Microsoft-AspNetCore-", + "System.Net" + }; + + public static readonly Func DefaultEventSourceFilterPredicate = name => DefaultEventSourcePrefixes.Any(x => name.StartsWith(x, StringComparison.Ordinal)); } } diff --git a/Prometheus/EventCounterAdapterOptions.cs b/Prometheus/EventCounterAdapterOptions.cs index b5a5af1c..e04a4709 100644 --- a/Prometheus/EventCounterAdapterOptions.cs +++ b/Prometheus/EventCounterAdapterOptions.cs @@ -1,20 +1,19 @@ -namespace Prometheus +namespace Prometheus; + +public sealed class EventCounterAdapterOptions { - public sealed class EventCounterAdapterOptions - { - public static readonly EventCounterAdapterOptions Default = new(); + public static readonly EventCounterAdapterOptions Default = new(); - /// - /// By default we subscribe to event counters from all event sources but this allows you to filter by event source name. - /// - public Func EventSourceFilterPredicate { get; set; } = _ => true; + /// + /// By default we subscribe to a predefined set of generally useful event counters but this allows you to specify a custom filter by event source name. + /// + public Func EventSourceFilterPredicate { get; set; } = EventCounterAdapter.DefaultEventSourceFilterPredicate; - /// - /// By default, we subscribe to event counters at Informational level from every event source. - /// You can customize these settings via this callback (with the event source name as the string given as input). - /// - public Func EventSourceSettingsProvider { get; set; } = _ => new(); + /// + /// By default, we subscribe to event counters at Informational level from every event source. + /// You can customize these settings via this callback (with the event source name as the string given as input). + /// + public Func EventSourceSettingsProvider { get; set; } = _ => new(); - public CollectorRegistry Registry { get; set; } = Metrics.DefaultRegistry; - } + public CollectorRegistry Registry { get; set; } = Metrics.DefaultRegistry; } diff --git a/README.md b/README.md index 805ebbfa..b8012494 100644 --- a/README.md +++ b/README.md @@ -828,6 +828,8 @@ The level of detail obtained from this is rather low - only the total count for You can configure the integration using `Metrics.ConfigureEventCounterAdapter()`. +By default, prometheus-net will only publish a small predefined set of general-purpose useful event counters to minimize resource consumption in the default configuration. A custom event source filter must now be provided in the configuration to enable publishing of additional event counters. + See also, [Sample.Console](Sample.Console/Program.cs). # .NET Meters integration From 3002b0c867b7b092ea91715df087c3a89b3c3faf Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Fri, 20 Jan 2023 09:17:34 +0200 Subject: [PATCH 067/230] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b8012494..7e2969d7 100644 --- a/README.md +++ b/README.md @@ -399,7 +399,7 @@ foreach (var record in recordsToProcess) Exemplars are only published if the metrics are being scraped by an OpenMetrics-capable client. For development purposes, you can force the library to use the OpenMetrics exposition format by adding `?accept=application/openmetrics-text` to the `/metrics` URL. > **Note** -> The Prometheus database automatically negotiates OpenMetrics support when scraping metrics - you do not need to apply any special scraping configuration in production scenarios. You may need to [enable exemplar storage](https://prometheus.io/docs/prometheus/latest/feature_flags/#exemplars-storage), though.. +> The Prometheus database automatically negotiates OpenMetrics support when scraping metrics - you do not need to apply any special scraping configuration in production scenarios. You may need to [enable exemplar storage](https://prometheus.io/docs/prometheus/latest/feature_flags/#exemplars-storage), though. See also, [Sample.Console.Exemplars](Sample.Console.Exemplars/Program.cs). From 210a1ab09d613b16484beec9e3c0db0f70452dd6 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 26 Jan 2023 16:34:22 +0200 Subject: [PATCH 068/230] Added `IManagedLifetimeMetricFactory.WithLabels()` to enable simpler label enrichment in scenarios where lifetime-managed metric instances are used. --- History | 1 + Prometheus/AutoLeasingCounter.cs | 2 +- Prometheus/IManagedLifetimeMetricFactory.cs | 65 ++++----- Prometheus/LabelEnrichingAutoLeasingMetric.cs | 23 ++++ .../LabelEnrichingManagedLifetimeCounter.cs | 49 +++++++ .../LabelEnrichingManagedLifetimeGauge.cs | 48 +++++++ .../LabelEnrichingManagedLifetimeHistogram.cs | 48 +++++++ ...elEnrichingManagedLifetimeMetricFactory.cs | 96 +++++++++++++ .../LabelEnrichingManagedLifetimeSummary.cs | 48 +++++++ Prometheus/ManagedLifetimeMetricFactory.cs | 5 + Tests.NetCore/MetricExpirationTests.cs | 126 ++++++++++++++++-- 11 files changed, 468 insertions(+), 43 deletions(-) create mode 100644 Prometheus/LabelEnrichingAutoLeasingMetric.cs create mode 100644 Prometheus/LabelEnrichingManagedLifetimeCounter.cs create mode 100644 Prometheus/LabelEnrichingManagedLifetimeGauge.cs create mode 100644 Prometheus/LabelEnrichingManagedLifetimeHistogram.cs create mode 100644 Prometheus/LabelEnrichingManagedLifetimeMetricFactory.cs create mode 100644 Prometheus/LabelEnrichingManagedLifetimeSummary.cs diff --git a/History b/History index b4692ce3..7bed824e 100644 --- a/History +++ b/History @@ -9,6 +9,7 @@ - Slightly improve Counter performance under concurrent load. - Reduce memory allocations performed during ASP.NET Core HTTP request tracking. - By default, EventCounterAdapter will only listen to a small predefined set of general-purpose useful event sources, to minimize resource consumption in the default configuration. A custom event source filter must now be provided to enable listening for additional event sources. +- Added `IManagedLifetimeMetricFactory.WithLabels()` to enable simpler label enrichment in scenarios where lifetime-managed metric instances are used. * 7.1.0 - Added back .NET Standard 2.0 support as some customers had a hard dependency on .NET Standard 2.0 (despite not being a documented feature even earlier). - Added (Observable)UpDownCounter support to MeterAdapter (.NET 7 specific feature). diff --git a/Prometheus/AutoLeasingCounter.cs b/Prometheus/AutoLeasingCounter.cs index a8facedd..7e8c140d 100644 --- a/Prometheus/AutoLeasingCounter.cs +++ b/Prometheus/AutoLeasingCounter.cs @@ -44,7 +44,7 @@ public void Inc(params Exemplar.LabelPair[] exemplar) Inc(increment:1, exemplar: exemplar); } - public void Inc(double increment = 1, params Exemplar.LabelPair[] exemplar) + public void Inc(double increment = 1, params Exemplar.LabelPair[] exemplar) { _inner.WithLease(x => x.Inc(increment, exemplar), _labelValues); } diff --git a/Prometheus/IManagedLifetimeMetricFactory.cs b/Prometheus/IManagedLifetimeMetricFactory.cs index 7b554166..79c4d486 100644 --- a/Prometheus/IManagedLifetimeMetricFactory.cs +++ b/Prometheus/IManagedLifetimeMetricFactory.cs @@ -1,37 +1,42 @@ -namespace Prometheus +namespace Prometheus; + +/// +/// A metric factory for creating metrics that use a managed lifetime, whereby the metric may +/// be deleted based on logic other than disposal or similar explicit deletion. +/// +/// +/// The lifetime management logic is associated with a metric handle. Calling CreateXyz() with equivalent identity parameters will return +/// the same handle. However, using multiple factories will create independent handles (which will delete the same metric independently). +/// +public interface IManagedLifetimeMetricFactory { /// - /// A metric factory for creating metrics that use a managed lifetime, whereby the metric may - /// be deleted based on logic other than disposal or similar explicit deletion. + /// Creates a metric with a lease-extended lifetime. + /// A timeseries will expire N seconds after the last lease is released, with N determined at factory create-time. /// - /// - /// The lifetime management logic is associated with a metric handle. Calling CreateXyz() with equivalent identity parameters will return - /// the same handle. However, using multiple factories will create independent handles (which will delete the same metric independently). - /// - public interface IManagedLifetimeMetricFactory - { - /// - /// Creates a metric with a lease-extended lifetime. - /// A timeseries will expire N seconds after the last lease is released, with N determined at factory create-time. - /// - IManagedLifetimeMetricHandle CreateCounter(string name, string help, string[] labelNames, CounterConfiguration? configuration = null); + IManagedLifetimeMetricHandle CreateCounter(string name, string help, string[] labelNames, CounterConfiguration? configuration = null); - /// - /// Creates a metric with a lease-extended lifetime. - /// A timeseries will expire N seconds after the last lease is released, with N determined at factory create-time. - /// - IManagedLifetimeMetricHandle CreateGauge(string name, string help, string[] labelNames, GaugeConfiguration? configuration = null); + /// + /// Creates a metric with a lease-extended lifetime. + /// A timeseries will expire N seconds after the last lease is released, with N determined at factory create-time. + /// + IManagedLifetimeMetricHandle CreateGauge(string name, string help, string[] labelNames, GaugeConfiguration? configuration = null); + + /// + /// Creates a metric with a lease-extended lifetime. + /// A timeseries will expire N seconds after the last lease is released, with N determined at factory create-time. + /// + IManagedLifetimeMetricHandle CreateHistogram(string name, string help, string[] labelNames, HistogramConfiguration? configuration = null); - /// - /// Creates a metric with a lease-extended lifetime. - /// A timeseries will expire N seconds after the last lease is released, with N determined at factory create-time. - /// - IManagedLifetimeMetricHandle CreateHistogram(string name, string help, string[] labelNames, HistogramConfiguration? configuration = null); + /// + /// Creates a metric with a lease-extended lifetime. + /// A timeseries will expire N seconds after the last lease is released, with N determined at factory create-time. + /// + IManagedLifetimeMetricHandle CreateSummary(string name, string help, string[] labelNames, SummaryConfiguration? configuration = null); - /// - /// Creates a metric with a lease-extended lifetime. - /// A timeseries will expire N seconds after the last lease is released, with N determined at factory create-time. - /// - IManagedLifetimeMetricHandle CreateSummary(string name, string help, string[] labelNames, SummaryConfiguration? configuration = null); - } + /// + /// Returns a new metric factory that will add the specified labels to any metrics created using it. + /// Different instances returned for the same labels are equivalent and any metrics created via them share their lifetimes. + /// + IManagedLifetimeMetricFactory WithLabels(IDictionary labels); } diff --git a/Prometheus/LabelEnrichingAutoLeasingMetric.cs b/Prometheus/LabelEnrichingAutoLeasingMetric.cs new file mode 100644 index 00000000..c0da24db --- /dev/null +++ b/Prometheus/LabelEnrichingAutoLeasingMetric.cs @@ -0,0 +1,23 @@ +namespace Prometheus; + +internal sealed class LabelEnrichingAutoLeasingMetric : ICollector + where TMetric : ICollectorChild +{ + public LabelEnrichingAutoLeasingMetric(ICollector inner, string[] enrichWithLabelValues) + { + _inner = inner; + _enrichedLabelValues = enrichWithLabelValues; + } + + private readonly ICollector _inner; + private readonly string[] _enrichedLabelValues; + + public TMetric Unlabelled => _inner.Unlabelled; + public string Name => _inner.Name; + public string Help => _inner.Help; + + // We do not display the enriched labels, they are transparent - this is only the instance-specific label names. + public string[] LabelNames => _inner.LabelNames; + + public TMetric WithLabels(params string[] labelValues) => _inner.WithLabels(_enrichedLabelValues.Concat(labelValues).ToArray()); +} diff --git a/Prometheus/LabelEnrichingManagedLifetimeCounter.cs b/Prometheus/LabelEnrichingManagedLifetimeCounter.cs new file mode 100644 index 00000000..149da1ac --- /dev/null +++ b/Prometheus/LabelEnrichingManagedLifetimeCounter.cs @@ -0,0 +1,49 @@ +namespace Prometheus; + +internal sealed class LabelEnrichingManagedLifetimeCounter : IManagedLifetimeMetricHandle +{ + public LabelEnrichingManagedLifetimeCounter(IManagedLifetimeMetricHandle inner, string[] enrichWithLabelValues) + { + _inner = inner; + _enrichWithLabelValues = enrichWithLabelValues; + } + + // Internal for manipulation during testing. + internal readonly IManagedLifetimeMetricHandle _inner; + private readonly string[] _enrichWithLabelValues; + + public IDisposable AcquireLease(out ICounter metric, params string[] labelValues) + { + return _inner.AcquireLease(out metric, WithEnrichedLabelValues(labelValues)); + } + + public ICollector WithExtendLifetimeOnUse() + { + return new LabelEnrichingAutoLeasingMetric(_inner.WithExtendLifetimeOnUse(), _enrichWithLabelValues); + } + + public void WithLease(Action action, params string[] labelValues) + { + _inner.WithLease(action, WithEnrichedLabelValues(labelValues)); + } + + public TResult WithLease(Func func, params string[] labelValues) + { + return _inner.WithLease(func, WithEnrichedLabelValues(labelValues)); + } + + public Task WithLeaseAsync(Func func, params string[] labelValues) + { + return _inner.WithLeaseAsync(func, WithEnrichedLabelValues(labelValues)); + } + + public Task WithLeaseAsync(Func> action, params string[] labelValues) + { + return _inner.WithLeaseAsync(action, WithEnrichedLabelValues(labelValues)); + } + + private string[] WithEnrichedLabelValues(string[] instanceLabelValues) + { + return _enrichWithLabelValues.Concat(instanceLabelValues).ToArray(); + } +} diff --git a/Prometheus/LabelEnrichingManagedLifetimeGauge.cs b/Prometheus/LabelEnrichingManagedLifetimeGauge.cs new file mode 100644 index 00000000..f51e31cf --- /dev/null +++ b/Prometheus/LabelEnrichingManagedLifetimeGauge.cs @@ -0,0 +1,48 @@ +namespace Prometheus; + +internal sealed class LabelEnrichingManagedLifetimeGauge : IManagedLifetimeMetricHandle +{ + public LabelEnrichingManagedLifetimeGauge(IManagedLifetimeMetricHandle inner, string[] enrichWithLabelValues) + { + _inner = inner; + _enrichWithLabelValues = enrichWithLabelValues; + } + + private readonly IManagedLifetimeMetricHandle _inner; + private readonly string[] _enrichWithLabelValues; + + public IDisposable AcquireLease(out IGauge metric, params string[] labelValues) + { + return _inner.AcquireLease(out metric, WithEnrichedLabelValues(labelValues)); + } + + public ICollector WithExtendLifetimeOnUse() + { + return new LabelEnrichingAutoLeasingMetric(_inner.WithExtendLifetimeOnUse(), _enrichWithLabelValues); + } + + public void WithLease(Action action, params string[] labelValues) + { + _inner.WithLease(action, WithEnrichedLabelValues(labelValues)); + } + + public TResult WithLease(Func func, params string[] labelValues) + { + return _inner.WithLease(func, WithEnrichedLabelValues(labelValues)); + } + + public Task WithLeaseAsync(Func func, params string[] labelValues) + { + return _inner.WithLeaseAsync(func, WithEnrichedLabelValues(labelValues)); + } + + public Task WithLeaseAsync(Func> action, params string[] labelValues) + { + return _inner.WithLeaseAsync(action, WithEnrichedLabelValues(labelValues)); + } + + private string[] WithEnrichedLabelValues(string[] instanceLabelValues) + { + return _enrichWithLabelValues.Concat(instanceLabelValues).ToArray(); + } +} diff --git a/Prometheus/LabelEnrichingManagedLifetimeHistogram.cs b/Prometheus/LabelEnrichingManagedLifetimeHistogram.cs new file mode 100644 index 00000000..ecda1fe8 --- /dev/null +++ b/Prometheus/LabelEnrichingManagedLifetimeHistogram.cs @@ -0,0 +1,48 @@ +namespace Prometheus; + +internal sealed class LabelEnrichingManagedLifetimeHistogram : IManagedLifetimeMetricHandle +{ + public LabelEnrichingManagedLifetimeHistogram(IManagedLifetimeMetricHandle inner, string[] enrichWithLabelValues) + { + _inner = inner; + _enrichWithLabelValues = enrichWithLabelValues; + } + + private readonly IManagedLifetimeMetricHandle _inner; + private readonly string[] _enrichWithLabelValues; + + public IDisposable AcquireLease(out IHistogram metric, params string[] labelValues) + { + return _inner.AcquireLease(out metric, WithEnrichedLabelValues(labelValues)); + } + + public ICollector WithExtendLifetimeOnUse() + { + return new LabelEnrichingAutoLeasingMetric(_inner.WithExtendLifetimeOnUse(), _enrichWithLabelValues); + } + + public void WithLease(Action action, params string[] labelValues) + { + _inner.WithLease(action, WithEnrichedLabelValues(labelValues)); + } + + public TResult WithLease(Func func, params string[] labelValues) + { + return _inner.WithLease(func, WithEnrichedLabelValues(labelValues)); + } + + public Task WithLeaseAsync(Func func, params string[] labelValues) + { + return _inner.WithLeaseAsync(func, WithEnrichedLabelValues(labelValues)); + } + + public Task WithLeaseAsync(Func> action, params string[] labelValues) + { + return _inner.WithLeaseAsync(action, WithEnrichedLabelValues(labelValues)); + } + + private string[] WithEnrichedLabelValues(string[] instanceLabelValues) + { + return _enrichWithLabelValues.Concat(instanceLabelValues).ToArray(); + } +} diff --git a/Prometheus/LabelEnrichingManagedLifetimeMetricFactory.cs b/Prometheus/LabelEnrichingManagedLifetimeMetricFactory.cs new file mode 100644 index 00000000..8ad14160 --- /dev/null +++ b/Prometheus/LabelEnrichingManagedLifetimeMetricFactory.cs @@ -0,0 +1,96 @@ +using System.Collections.Concurrent; + +namespace Prometheus; + +/// +/// Applies a set of static labels to lifetime-managed metrics. Multiple instances are functionally equivalent for the same label set. +/// +internal sealed class LabelEnrichingManagedLifetimeMetricFactory : IManagedLifetimeMetricFactory +{ + public LabelEnrichingManagedLifetimeMetricFactory(ManagedLifetimeMetricFactory inner, IDictionary enrichWithLabels) + { + _inner = inner; + + // We just need the items to be consistently ordered between equivalent instances but it does not actually matter what the order is. + _labels = enrichWithLabels.OrderBy(x => x.Key, StringComparer.Ordinal).ToList(); + + _enrichWithLabelNames = enrichWithLabels.Select(x => x.Key).ToArray(); + _enrichWithLabelValues = enrichWithLabels.Select(x => x.Value).ToArray(); + } + + private readonly ManagedLifetimeMetricFactory _inner; + + // This is an ordered list because labels have specific order. + private readonly IReadOnlyList> _labels; + + // Cache the names/values to enrich with, for reuse. + // We could perhaps improve even further via StringSequence but that requires creating separate internal APIs so can be a future optimization. + private readonly string[] _enrichWithLabelNames; + private readonly string[] _enrichWithLabelValues; + + public IManagedLifetimeMetricHandle CreateCounter(string name, string help, string[] labelNames, CounterConfiguration? configuration = null) + { + var combinedLabelNames = WithEnrichedLabelNames(labelNames); + var innerHandle = _inner.CreateCounter(name, help, combinedLabelNames, configuration); + + // 1-1 relationship between instance of inner handle and our labeling handle. + // We expect lifetime of each to match the lifetime of the respective factory, so no need to cleanup anything. + return _counters.GetOrAdd(innerHandle, CreateCounterCore); + } + + private LabelEnrichingManagedLifetimeCounter CreateCounterCore(IManagedLifetimeMetricHandle inner) => new LabelEnrichingManagedLifetimeCounter(inner, _enrichWithLabelValues); + private readonly ConcurrentDictionary, LabelEnrichingManagedLifetimeCounter> _counters = new(); + + public IManagedLifetimeMetricHandle CreateGauge(string name, string help, string[] labelNames, GaugeConfiguration? configuration = null) + { + var combinedLabelNames = WithEnrichedLabelNames(labelNames); + var innerHandle = _inner.CreateGauge(name, help, combinedLabelNames, configuration); + + // 1-1 relationship between instance of inner handle and our labeling handle. + // We expect lifetime of each to match the lifetime of the respective factory, so no need to cleanup anything. + return _gauges.GetOrAdd(innerHandle, CreateGaugeCore); + } + + private LabelEnrichingManagedLifetimeGauge CreateGaugeCore(IManagedLifetimeMetricHandle inner) => new LabelEnrichingManagedLifetimeGauge(inner, _enrichWithLabelValues); + private readonly ConcurrentDictionary, LabelEnrichingManagedLifetimeGauge> _gauges = new(); + + public IManagedLifetimeMetricHandle CreateHistogram(string name, string help, string[] labelNames, HistogramConfiguration? configuration = null) + { + var combinedLabelNames = WithEnrichedLabelNames(labelNames); + var innerHandle = _inner.CreateHistogram(name, help, combinedLabelNames, configuration); + + // 1-1 relationship between instance of inner handle and our labeling handle. + // We expect lifetime of each to match the lifetime of the respective factory, so no need to cleanup anything. + return _histograms.GetOrAdd(innerHandle, CreateHistogramCore); + } + + private LabelEnrichingManagedLifetimeHistogram CreateHistogramCore(IManagedLifetimeMetricHandle inner) => new LabelEnrichingManagedLifetimeHistogram(inner, _enrichWithLabelValues); + private readonly ConcurrentDictionary, LabelEnrichingManagedLifetimeHistogram> _histograms = new(); + + public IManagedLifetimeMetricHandle CreateSummary(string name, string help, string[] labelNames, SummaryConfiguration? configuration = null) + { + var combinedLabelNames = WithEnrichedLabelNames(labelNames); + var innerHandle = _inner.CreateSummary(name, help, combinedLabelNames, configuration); + + // 1-1 relationship between instance of inner handle and our labeling handle. + // We expect lifetime of each to match the lifetime of the respective factory, so no need to cleanup anything. + return _summaries.GetOrAdd(innerHandle, CreateSummaryCore); + } + + private LabelEnrichingManagedLifetimeSummary CreateSummaryCore(IManagedLifetimeMetricHandle inner) => new LabelEnrichingManagedLifetimeSummary(inner, _enrichWithLabelValues); + private readonly ConcurrentDictionary, LabelEnrichingManagedLifetimeSummary> _summaries = new(); + + public IManagedLifetimeMetricFactory WithLabels(IDictionary labels) + { + var combinedLabels = _labels.Concat(labels).ToDictionary(x => x.Key, x => x.Value); + + // Inner factory takes care of applying the correct ordering for labels. + return _inner.WithLabels(combinedLabels); + } + + private string[] WithEnrichedLabelNames(string[] instanceLabelNames) + { + // Enrichment labels always go first when we are communicating with the inner factory. + return _enrichWithLabelNames.Concat(instanceLabelNames).ToArray(); + } +} diff --git a/Prometheus/LabelEnrichingManagedLifetimeSummary.cs b/Prometheus/LabelEnrichingManagedLifetimeSummary.cs new file mode 100644 index 00000000..39e7b6bc --- /dev/null +++ b/Prometheus/LabelEnrichingManagedLifetimeSummary.cs @@ -0,0 +1,48 @@ +namespace Prometheus; + +internal sealed class LabelEnrichingManagedLifetimeSummary : IManagedLifetimeMetricHandle +{ + public LabelEnrichingManagedLifetimeSummary(IManagedLifetimeMetricHandle inner, string[] enrichWithLabelValues) + { + _inner = inner; + _enrichWithLabelValues = enrichWithLabelValues; + } + + private readonly IManagedLifetimeMetricHandle _inner; + private readonly string[] _enrichWithLabelValues; + + public IDisposable AcquireLease(out ISummary metric, params string[] labelValues) + { + return _inner.AcquireLease(out metric, WithEnrichedLabelValues(labelValues)); + } + + public ICollector WithExtendLifetimeOnUse() + { + return new LabelEnrichingAutoLeasingMetric(_inner.WithExtendLifetimeOnUse(), _enrichWithLabelValues); + } + + public void WithLease(Action action, params string[] labelValues) + { + _inner.WithLease(action, WithEnrichedLabelValues(labelValues)); + } + + public TResult WithLease(Func func, params string[] labelValues) + { + return _inner.WithLease(func, WithEnrichedLabelValues(labelValues)); + } + + public Task WithLeaseAsync(Func func, params string[] labelValues) + { + return _inner.WithLeaseAsync(func, WithEnrichedLabelValues(labelValues)); + } + + public Task WithLeaseAsync(Func> action, params string[] labelValues) + { + return _inner.WithLeaseAsync(action, WithEnrichedLabelValues(labelValues)); + } + + private string[] WithEnrichedLabelValues(string[] instanceLabelValues) + { + return _enrichWithLabelValues.Concat(instanceLabelValues).ToArray(); + } +} diff --git a/Prometheus/ManagedLifetimeMetricFactory.cs b/Prometheus/ManagedLifetimeMetricFactory.cs index 80f2e1a1..159f3c9f 100644 --- a/Prometheus/ManagedLifetimeMetricFactory.cs +++ b/Prometheus/ManagedLifetimeMetricFactory.cs @@ -18,6 +18,11 @@ public ManagedLifetimeMetricFactory(MetricFactory inner, TimeSpan expiresAfter) private readonly MetricFactory _inner; private readonly TimeSpan _expiresAfter; + public IManagedLifetimeMetricFactory WithLabels(IDictionary labels) + { + return new LabelEnrichingManagedLifetimeMetricFactory(this, labels); + } + public IManagedLifetimeMetricHandle CreateCounter(string name, string help, string[] instanceLabelNames, CounterConfiguration? configuration = null) { var identity = new ManagedLifetimeMetricIdentity(name, StringSequence.From(instanceLabelNames)); diff --git a/Tests.NetCore/MetricExpirationTests.cs b/Tests.NetCore/MetricExpirationTests.cs index 363b2c5c..68cb3c56 100644 --- a/Tests.NetCore/MetricExpirationTests.cs +++ b/Tests.NetCore/MetricExpirationTests.cs @@ -1,5 +1,6 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -27,13 +28,11 @@ public MetricExpirationTests() // This has a slight dependency on the performance of the PC executing the tests - maybe not ideal long term strategy but what can you do. private static readonly TimeSpan WaitForAsyncActionSleepTime = TimeSpan.FromSeconds(0.1); - private static readonly string[] _labels = Array.Empty(); - [TestMethod] public void ManagedLifetimeMetric_IsSameMetricAsNormalMetric() { - var counter1 = _metrics.CreateCounter(MetricName, "", _labels); - var counterHandle = _expiringMetrics.CreateCounter(MetricName, "", _labels); + var counter1 = _metrics.CreateCounter(MetricName, ""); + var counterHandle = _expiringMetrics.CreateCounter(MetricName, "", Array.Empty()); counter1.Inc(); @@ -56,8 +55,8 @@ public void ManagedLifetimeMetric_IsSameMetricAsNormalMetric() [TestMethod] public void ManagedLifetimeMetric_MultipleHandlesFromSameFactory_AreSameHandle() { - var handle1 = _expiringMetrics.CreateCounter(MetricName, "", _labels); - var handle2 = _expiringMetrics.CreateCounter(MetricName, "", _labels); + var handle1 = _expiringMetrics.CreateCounter(MetricName, "", Array.Empty()); + var handle2 = _expiringMetrics.CreateCounter(MetricName, "", Array.Empty()); Assert.AreSame(handle1, handle2); } @@ -65,10 +64,10 @@ public void ManagedLifetimeMetric_MultipleHandlesFromSameFactory_AreSameHandle() [TestMethod] public void ManagedLifetimeMetric_ViaDifferentFactories_IsSameMetric() { - var handle1 = _expiringMetrics.CreateCounter(MetricName, "", _labels); + var handle1 = _expiringMetrics.CreateCounter(MetricName, "", Array.Empty()); var expiringMetrics2 = _metrics.WithManagedLifetime(expiresAfter: TimeSpan.FromHours(24)); - var handle2 = expiringMetrics2.CreateCounter(MetricName, "", _labels); + var handle2 = expiringMetrics2.CreateCounter(MetricName, "", Array.Empty()); using (var lease = handle1.AcquireLease(out var instance)) instance.Inc(); @@ -87,7 +86,7 @@ public void ManagedLifetimeMetric_ViaDifferentFactories_IsSameMetric() [TestMethod] public async Task ManagedLifetimeMetric_ExpiresOnlyAfterAllLeasesReleased() { - var handle = _expiringMetrics.CreateCounter(MetricName, "", _labels); + var handle = _expiringMetrics.CreateCounter(MetricName, "", Array.Empty()); // We break delays on demand to force any expiring-eligible metrics to expire. var delayer = new BreakableDelayer(); @@ -112,7 +111,110 @@ public async Task ManagedLifetimeMetric_ExpiresOnlyAfterAllLeasesReleased() await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and finish expiring. // 2 leases remain - should not have expired yet. Check with a fresh copy from the root registry. - Assert.AreEqual(2, _metrics.CreateCounter(MetricName, "", _labels).Value); + Assert.AreEqual(2, _metrics.CreateCounter(MetricName, "").Value); + } + + await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and start expiring. + + delayer.BreakAllDelays(); + await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and finish expiring. + delayer.BreakAllDelays(); + await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and finish expiring. + + // 1 lease remains - should not have expired yet. Check with a fresh copy from the root registry. + Assert.AreEqual(2, _metrics.CreateCounter(MetricName, "").Value); + } + + await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and start expiring. + + delayer.BreakAllDelays(); + await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and finish expiring. + delayer.BreakAllDelays(); + await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and finish expiring. + + // 0 leases remains - should have expired. Check with a fresh copy from the root registry. + Assert.AreEqual(0, _metrics.CreateCounter(MetricName, "").Value); + } + + [TestMethod] + public async Task ManagedLifetimeMetric_WithMultipleLabelingPaths_SharedSameLifetime() + { + // Two calls with the same X, Y to ManagedLifetimeMetricFactory.WithLabels(X).CreateCounter().AcquireLease(Y) + // will impact the same metric lifetime (and, implicitly, share the same metric instance data). + // A call with matching ManagedLifetimeMetricFactory.CreateCounter(X).AcquireLease(Y) will do the same. + + var label1Key = "test_label_1"; + var label2Key = "test_label_2"; + var label1Value = "some value 1"; + var label2Value = "some value 2"; + var labels = new Dictionary + { + { label1Key, label1Value }, + { label2Key, label2Value }, + }; + + // Must be ordinal-sorted to match the WithLabels() sorting. + var labelNames = new[] { label1Key, label2Key }; + var labelValues = new[] { label1Value, label2Value }; + + var labelingFactory1 = _expiringMetrics.WithLabels(labels); + var labelingFactory2 = _expiringMetrics.WithLabels(labels); + + var factory1Handle = labelingFactory1.CreateCounter(MetricName, "", Array.Empty()); + var factory2Handle = labelingFactory2.CreateCounter(MetricName, "", Array.Empty()); + var rawHandle = _expiringMetrics.CreateCounter(MetricName, "", labelNames); + + // We break delays on demand to force any expiring-eligible metrics to expire. + var delayer = new BreakableDelayer(); + ((ManagedLifetimeCounter)((LabelEnrichingManagedLifetimeCounter)factory1Handle)._inner).Delayer = delayer; + ((ManagedLifetimeCounter)((LabelEnrichingManagedLifetimeCounter)factory2Handle)._inner).Delayer = delayer; + ((ManagedLifetimeCounter)rawHandle).Delayer = delayer; + + // We detect expiration by the value having been reset when we try allocate the counter again. + // We break 2 delays on every use, to ensure that the expiration logic has enough iterations to make up its mind. + + using (factory1Handle.AcquireLease(out var instance1)) + { + instance1.Inc(); + + using (factory2Handle.AcquireLease(out var instance2)) + { + instance2.Inc(); + + await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and start expiring. + + delayer.BreakAllDelays(); + await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and finish expiring. + delayer.BreakAllDelays(); + await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and finish expiring. + + // 2 leases remain - should not have expired yet. Check with a fresh copy from the root registry. + Assert.AreEqual(2, _metrics.CreateCounter(MetricName, "", labelNames).WithLabels(labelValues).Value); + } + + await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and start expiring. + + delayer.BreakAllDelays(); + await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and finish expiring. + delayer.BreakAllDelays(); + await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and finish expiring. + + // 1 lease remains - should not have expired yet. Check with a fresh copy from the root registry. + Assert.AreEqual(2, _metrics.CreateCounter(MetricName, "", labelNames).WithLabels(labelValues).Value); + + using (rawHandle.AcquireLease(out var instance3, labelValues)) + { + instance3.Inc(); + + await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and start expiring. + + delayer.BreakAllDelays(); + await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and finish expiring. + delayer.BreakAllDelays(); + await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and finish expiring. + + // 2 leases remain - should not have expired yet. Check with a fresh copy from the root registry. + Assert.AreEqual(3, _metrics.CreateCounter(MetricName, "", labelNames).WithLabels(labelValues).Value); } await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and start expiring. @@ -123,7 +225,7 @@ public async Task ManagedLifetimeMetric_ExpiresOnlyAfterAllLeasesReleased() await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and finish expiring. // 1 lease remains - should not have expired yet. Check with a fresh copy from the root registry. - Assert.AreEqual(2, _metrics.CreateCounter(MetricName, "", _labels).Value); + Assert.AreEqual(3, _metrics.CreateCounter(MetricName, "", labelNames).WithLabels(labelValues).Value); } await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and start expiring. @@ -134,7 +236,7 @@ public async Task ManagedLifetimeMetric_ExpiresOnlyAfterAllLeasesReleased() await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and finish expiring. // 0 leases remains - should have expired. Check with a fresh copy from the root registry. - Assert.AreEqual(0, _metrics.CreateCounter(MetricName, "", _labels).Value); + Assert.AreEqual(0, _metrics.CreateCounter(MetricName, "", labelNames).WithLabels(labelValues).Value); } } } From f3eb0cdc4169d78c4d9c965cb68095ccc6e09c18 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Fri, 27 Jan 2023 09:54:11 +0200 Subject: [PATCH 069/230] The space after metric name in "HELP" is mandatory --- Prometheus/TextSerializer.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Prometheus/TextSerializer.cs b/Prometheus/TextSerializer.cs index 230f3ba1..1dd06be5 100644 --- a/Prometheus/TextSerializer.cs +++ b/Prometheus/TextSerializer.cs @@ -76,9 +76,10 @@ public async Task WriteFamilyDeclarationAsync(string name, byte[] nameBytes, byt await _stream.Value.WriteAsync(HashHelpSpace, 0, HashHelpSpace.Length, cancel); await _stream.Value.WriteAsync(nameBytes, 0, nameLen, cancel); + // The space after the name in "HELP" is mandatory as per ABNF, even if there is no help text. + await _stream.Value.WriteAsync(Space, 0, Space.Length, cancel); if (helpBytes.Length > 0) { - await _stream.Value.WriteAsync(Space, 0, Space.Length, cancel); await _stream.Value.WriteAsync(helpBytes, 0, helpBytes.Length, cancel); } await _stream.Value.WriteAsync(NewlineHashTypeSpace, 0, NewlineHashTypeSpace.Length, cancel); From bfc104126561a40a8e3331d4d67ac8d3c58306d7 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Fri, 27 Jan 2023 09:57:07 +0200 Subject: [PATCH 070/230] Fixup tests to match HELP line bugfix --- Tests.NetCore/TextSerializerTests.cs | 46 ++++++++++++++-------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/Tests.NetCore/TextSerializerTests.cs b/Tests.NetCore/TextSerializerTests.cs index 32f33fed..8575fc80 100644 --- a/Tests.NetCore/TextSerializerTests.cs +++ b/Tests.NetCore/TextSerializerTests.cs @@ -14,7 +14,7 @@ public static void BeforeClass(TestContext testContext) { ObservedExemplar.NowProvider = new TestNowProvider(); } - + [TestMethod] public async Task ValidateTextFmtSummaryExposition_Labels() { @@ -31,8 +31,8 @@ public async Task ValidateTextFmtSummaryExposition_Labels() summary.WithLabels("foo").Observe(3); }); - - result.ShouldBe(@"# HELP boom_bam + + result.ShouldBe(@"# HELP boom_bam # TYPE boom_bam summary boom_bam_sum{blah=""foo""} 3 boom_bam_count{blah=""foo""} 1 @@ -54,7 +54,7 @@ public async Task ValidateTextFmtSummaryExposition_NoLabels() }); summary.Observe(3); }); - + result.ShouldBe(@"# HELP boom_bam something # TYPE boom_bam summary boom_bam_sum 3 @@ -76,8 +76,8 @@ public async Task ValidateTextFmtGaugeExposition_Labels() gauge.WithLabels("foo").IncTo(10); }); - - result.ShouldBe(@"# HELP boom_bam + + result.ShouldBe(@"# HELP boom_bam # TYPE boom_bam gauge boom_bam{blah=""foo""} 10 "); @@ -96,11 +96,11 @@ public async Task ValidateTextFmtCounterExposition_Labels() counter.WithLabels("foo").IncTo(10); }); - result.ShouldBe("# HELP boom_bam\n" + + result.ShouldBe("# HELP boom_bam \n" + "# TYPE boom_bam counter\n" + "boom_bam{blah=\"foo\"} 10\n"); } - + [TestMethod] public async Task ValidateTextFmtCounterExposition_TotalSuffixInName() { @@ -113,10 +113,10 @@ public async Task ValidateTextFmtCounterExposition_TotalSuffixInName() counter.WithLabels("foo").IncTo(10); }); - + // This tests that the counter exposition format isn't influenced by openmetrics codepaths when it comes to the // _total suffix - result.ShouldBe("# HELP boom_bam_total\n" + + result.ShouldBe("# HELP boom_bam_total \n" + "# TYPE boom_bam_total counter\n" + "boom_bam_total{blah=\"foo\"} 10\n"); } @@ -135,7 +135,7 @@ public async Task ValidateTextFmtHistogramExposition_Labels() counter.WithLabels("foo").Observe(0.5); }); - result.ShouldBe(@"# HELP boom_bam + result.ShouldBe(@"# HELP boom_bam # TYPE boom_bam histogram boom_bam_sum{blah=""foo""} 0.5 boom_bam_count{blah=""foo""} 1 @@ -180,7 +180,7 @@ public async Task ValidateOpenMetricsFmtHistogram_Basic() counter.Observe(1.5); counter.Observe(1); }); - + // This asserts that the le label has been modified and that we have a EOF result.ShouldBe(@"# HELP boom_bam something # TYPE boom_bam histogram @@ -192,7 +192,7 @@ boom_bam_count 2 # EOF "); } - + [TestMethod] public async Task ValidateOpenMetricsFmtHistogram_WithExemplar() { @@ -200,15 +200,15 @@ public async Task ValidateOpenMetricsFmtHistogram_WithExemplar() { var counter = factory.CreateHistogram("boom_bam", "something", new HistogramConfiguration { - Buckets = new[] { 1, 2.5, 3, Math.Pow(10, 45)} + Buckets = new[] { 1, 2.5, 3, Math.Pow(10, 45) } }); counter.Observe(1, Exemplar.Pair("traceID", "1")); counter.Observe(1.5, Exemplar.Pair("traceID", "2")); counter.Observe(4, Exemplar.Pair("traceID", "3")); - counter.Observe(Math.Pow(10,44), Exemplar.Pair("traceID", "4")); + counter.Observe(Math.Pow(10, 44), Exemplar.Pair("traceID", "4")); }); - + // This asserts histogram OpenMetrics form with exemplars and also using numbers which are large enough for // scientific notation result.ShouldBe(@"# HELP boom_bam something @@ -223,7 +223,7 @@ boom_bam_count 4 # EOF "); } - + [TestMethod] public async Task ValidateOpenMetricsFmtCounter_MultiItemExemplar() { @@ -234,17 +234,17 @@ public async Task ValidateOpenMetricsFmtCounter_MultiItemExemplar() LabelNames = new[] { "blah" } }); - counter.WithLabels("foo").Inc(1, + counter.WithLabels("foo").Inc(1, Exemplar.Pair("traceID", "1234"), Exemplar.Pair("yaay", "4321")); }); // This asserts that multi-labeled exemplars work as well not supplying a _total suffix in the counter name. - result.ShouldBe(@"# HELP boom_bam + result.ShouldBe(@"# HELP boom_bam # TYPE boom_bam unknown boom_bam{blah=""foo""} 1.0 # {traceID=""1234"",yaay=""4321""} 1.0 1668779954.714 # EOF "); } - + [TestMethod] public async Task ValidateOpenMetricsFmtCounter_TotalInNameSuffix() { @@ -255,11 +255,11 @@ public async Task ValidateOpenMetricsFmtCounter_TotalInNameSuffix() LabelNames = new[] { "blah" } }); - counter.WithLabels("foo").Inc(1, + counter.WithLabels("foo").Inc(1, Exemplar.Pair("traceID", "1234"), Exemplar.Pair("yaay", "4321")); }); // This tests the shape of OpenMetrics when _total suffix is supplied - result.ShouldBe(@"# HELP boom_bam + result.ShouldBe(@"# HELP boom_bam # TYPE boom_bam counter boom_bam_total{blah=""foo""} 1.0 # {traceID=""1234"",yaay=""4321""} 1.0 1668779954.714 # EOF @@ -274,7 +274,7 @@ public double Now() return TestNow; } } - + private class TestCase { private readonly String raw; From 90f4311ec3f09e3b0a98c73d034b26b78ee2dbc2 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Fri, 27 Jan 2023 10:40:42 +0200 Subject: [PATCH 071/230] Preserve Counter.Child.Inc(double) method signature for ABI compatibility --- Prometheus/Counter.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Prometheus/Counter.cs b/Prometheus/Counter.cs index f89b0f0a..97249fb8 100644 --- a/Prometheus/Counter.cs +++ b/Prometheus/Counter.cs @@ -27,6 +27,11 @@ await serializer.WriteMetricPointAsync( ReturnBorrowedExemplar(ref _observedExemplar, exemplar); } + public void Inc(double increment = 1.0) + { + Inc(increment: increment, Exemplar.None); + } + public void Inc(params Exemplar.LabelPair[] exemplarLabels) { Inc(increment: 1, exemplarLabels: exemplarLabels); From c5ab7796bcdcec63cdc9ef938a94f88842ec820a Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Fri, 27 Jan 2023 11:31:17 +0200 Subject: [PATCH 072/230] Update EventCounterAdapter at 10 second interval by default, to reduce garbage generated in memory --- History | 1 + Prometheus/EventCounterAdapter.cs | 8 ++++++-- Prometheus/EventCounterAdapterOptions.cs | 9 +++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/History b/History index 7bed824e..c9bc216a 100644 --- a/History +++ b/History @@ -9,6 +9,7 @@ - Slightly improve Counter performance under concurrent load. - Reduce memory allocations performed during ASP.NET Core HTTP request tracking. - By default, EventCounterAdapter will only listen to a small predefined set of general-purpose useful event sources, to minimize resource consumption in the default configuration. A custom event source filter must now be provided to enable listening for additional event sources. +- EventCounterAdapter will only refresh data every 10 seconds by default, to reduce amount of garbage generated in memory (.NET event counters are very noisy and create many temporary objects). - Added `IManagedLifetimeMetricFactory.WithLabels()` to enable simpler label enrichment in scenarios where lifetime-managed metric instances are used. * 7.1.0 - Added back .NET Standard 2.0 support as some customers had a hard dependency on .NET Standard 2.0 (despite not being a documented feature even earlier). diff --git a/Prometheus/EventCounterAdapter.cs b/Prometheus/EventCounterAdapter.cs index a902f9a6..e6d74f9a 100644 --- a/Prometheus/EventCounterAdapter.cs +++ b/Prometheus/EventCounterAdapter.cs @@ -1,6 +1,7 @@ using System.Collections.Concurrent; using System.Diagnostics; using System.Diagnostics.Tracing; +using System.Globalization; namespace Prometheus { @@ -30,7 +31,7 @@ private EventCounterAdapter(EventCounterAdapterOptions options) _eventSourcesConnected = _metricFactory.CreateGauge("prometheus_net_eventcounteradapter_sources_connected_total", "Number of event sources that are currently connected to the adapter."); - _listener = new Listener(ShouldUseEventSource, ConfigureEventSource, OnEventWritten); + _listener = new Listener(ShouldUseEventSource, ConfigureEventSource, options.UpdateInterval, OnEventWritten); } public void Dispose() @@ -153,10 +154,12 @@ private sealed class Listener : EventListener public Listener( Func shouldUseEventSource, Func configureEventSosurce, + TimeSpan updateInterval, Action onEventWritten) { _shouldUseEventSource = shouldUseEventSource; _configureEventSosurce = configureEventSosurce; + _updateInterval = updateInterval; _onEventWritten = onEventWritten; foreach (var eventSource in _preRegisteredEventSources) @@ -169,6 +172,7 @@ public Listener( private readonly Func _shouldUseEventSource; private readonly Func _configureEventSosurce; + private readonly TimeSpan _updateInterval; private readonly Action _onEventWritten; protected override void OnEventSourceCreated(EventSource eventSource) @@ -191,7 +195,7 @@ protected override void OnEventSourceCreated(EventSource eventSource) EnableEvents(eventSource, options.MinimumLevel, options.MatchKeywords, new Dictionary() { - ["EventCounterIntervalSec"] = "1" + ["EventCounterIntervalSec"] = ((int)Math.Max(1, _updateInterval.TotalSeconds)).ToString(CultureInfo.InvariantCulture), }); } catch (Exception ex) diff --git a/Prometheus/EventCounterAdapterOptions.cs b/Prometheus/EventCounterAdapterOptions.cs index e04a4709..856d0bbc 100644 --- a/Prometheus/EventCounterAdapterOptions.cs +++ b/Prometheus/EventCounterAdapterOptions.cs @@ -15,5 +15,14 @@ public sealed class EventCounterAdapterOptions /// public Func EventSourceSettingsProvider { get; set; } = _ => new(); + /// + /// How often we update event counter data. + /// + /// + /// Event counters are quite noisy in terms of generating a lot of temporary objects in memory, so we keep the default moderate. + /// All this memory is immediately GC-able but in a near-idle app it can make for a scary upward trend on the RAM usage graph because the GC might not immediately release the memory to the OS. + /// + public TimeSpan UpdateInterval { get; set; } = TimeSpan.FromSeconds(10); + public CollectorRegistry Registry { get; set; } = Metrics.DefaultRegistry; } From 53b961afb38f6de1a77eb01f6ef503e02887c4af Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Fri, 27 Jan 2023 12:33:35 +0200 Subject: [PATCH 073/230] run.sh needs LF line endings --- .gitattributes | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitattributes b/.gitattributes index 1ff0c423..7b17c9b1 100644 --- a/.gitattributes +++ b/.gitattributes @@ -3,6 +3,9 @@ ############################################################################### * text=auto +# Linux scripts should have Linux file endings. +*.sh text eol=lf + ############################################################################### # Set default behavior for command prompt diff. # From 67c7574aae319f331bbc19dc2f489ad1b338c94e Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Fri, 27 Jan 2023 12:33:47 +0200 Subject: [PATCH 074/230] Add missing dotted AspNetCore event source to default filters --- Prometheus/EventCounterAdapter.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Prometheus/EventCounterAdapter.cs b/Prometheus/EventCounterAdapter.cs index e6d74f9a..7e409ec7 100644 --- a/Prometheus/EventCounterAdapter.cs +++ b/Prometheus/EventCounterAdapter.cs @@ -219,7 +219,8 @@ protected override void OnEventWritten(EventWrittenEventArgs eventData) private static readonly IReadOnlyList DefaultEventSourcePrefixes = new[] { "System.Runtime", - "Microsoft-AspNetCore-", + "Microsoft-AspNetCore", + "Microsoft.AspNetCore", "System.Net" }; From d72115a89bdc4d6f1434d798ec3c6f615b6d7f47 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Fri, 27 Jan 2023 13:17:54 +0200 Subject: [PATCH 075/230] Event counter memory warden - collect garbage to avoid piling up --- Prometheus/EventCounterAdapter.cs | 2 + Prometheus/EventCounterAdapterMemoryWarden.cs | 41 +++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 Prometheus/EventCounterAdapterMemoryWarden.cs diff --git a/Prometheus/EventCounterAdapter.cs b/Prometheus/EventCounterAdapter.cs index 7e409ec7..5052a376 100644 --- a/Prometheus/EventCounterAdapter.cs +++ b/Prometheus/EventCounterAdapter.cs @@ -31,6 +31,8 @@ private EventCounterAdapter(EventCounterAdapterOptions options) _eventSourcesConnected = _metricFactory.CreateGauge("prometheus_net_eventcounteradapter_sources_connected_total", "Number of event sources that are currently connected to the adapter."); + EventCounterAdapterMemoryWarden.EnsureStarted(); + _listener = new Listener(ShouldUseEventSource, ConfigureEventSource, options.UpdateInterval, OnEventWritten); } diff --git a/Prometheus/EventCounterAdapterMemoryWarden.cs b/Prometheus/EventCounterAdapterMemoryWarden.cs new file mode 100644 index 00000000..a43abff3 --- /dev/null +++ b/Prometheus/EventCounterAdapterMemoryWarden.cs @@ -0,0 +1,41 @@ +namespace Prometheus; + +/// +/// .NET EventCounters are very noisy in terms of generating a lot of garbage. At the same time, apps in development environments typically do not get loaded much, so rarely collect garbage. +/// This can mean that as soon as you plug prometheus-net into an app, its memory usage shoots up due to gen 0 garbage piling up. It will all get collected... eventually, when the GC runs. +/// This might not happen for 12+ hours! It presents a major user perception issue, as they just see the process memory usage rise and rise and rise. +/// +/// This class exists to prevent this problem. We simply force a gen 0 GC every N minutes if EventCounterAdapter is enabled and if no GC has occurred in the last N minutes already. +/// +internal static class EventCounterAdapterMemoryWarden +{ + private static readonly TimeSpan ForcedCollectionInterval = TimeSpan.FromMinutes(10); + + public static void EnsureStarted() + { + // The constructor does all the work, this is just here to signal intent. + } + + static EventCounterAdapterMemoryWarden() + { + Task.Run(Execute); + } + + private static async Task Execute() + { + while (true) + { + // Capture pre-delay state so we can check if a collection is required. + var preDelayCollectionCount = GC.CollectionCount(0); + + await Task.Delay(ForcedCollectionInterval); + + var postDelayCollectionCount = GC.CollectionCount(0); + + if (preDelayCollectionCount != postDelayCollectionCount) + continue; // GC already happened, go chill. + + GC.Collect(0); + } + } +} From 97f5bda0366a9bb3939ff245cb086b8e01eaac9e Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Fri, 27 Jan 2023 14:40:15 +0200 Subject: [PATCH 076/230] API UX by adding default label values to managed-lifetime interface. The labels can now be inherited, so might not be defined at this call site. --- Prometheus/IManagedLifetimeMetricFactory.cs | 8 ++++---- ...LabelEnrichingManagedLifetimeMetricFactory.cs | 16 ++++++++-------- Prometheus/ManagedLifetimeMetricFactory.cs | 16 ++++++++-------- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/Prometheus/IManagedLifetimeMetricFactory.cs b/Prometheus/IManagedLifetimeMetricFactory.cs index 79c4d486..ee9f9c4e 100644 --- a/Prometheus/IManagedLifetimeMetricFactory.cs +++ b/Prometheus/IManagedLifetimeMetricFactory.cs @@ -14,25 +14,25 @@ public interface IManagedLifetimeMetricFactory /// Creates a metric with a lease-extended lifetime. /// A timeseries will expire N seconds after the last lease is released, with N determined at factory create-time. /// - IManagedLifetimeMetricHandle CreateCounter(string name, string help, string[] labelNames, CounterConfiguration? configuration = null); + IManagedLifetimeMetricHandle CreateCounter(string name, string help, string[]? labelNames = null, CounterConfiguration? configuration = null); /// /// Creates a metric with a lease-extended lifetime. /// A timeseries will expire N seconds after the last lease is released, with N determined at factory create-time. /// - IManagedLifetimeMetricHandle CreateGauge(string name, string help, string[] labelNames, GaugeConfiguration? configuration = null); + IManagedLifetimeMetricHandle CreateGauge(string name, string help, string[]? labelNames = null, GaugeConfiguration? configuration = null); /// /// Creates a metric with a lease-extended lifetime. /// A timeseries will expire N seconds after the last lease is released, with N determined at factory create-time. /// - IManagedLifetimeMetricHandle CreateHistogram(string name, string help, string[] labelNames, HistogramConfiguration? configuration = null); + IManagedLifetimeMetricHandle CreateHistogram(string name, string help, string[]? labelNames = null, HistogramConfiguration? configuration = null); /// /// Creates a metric with a lease-extended lifetime. /// A timeseries will expire N seconds after the last lease is released, with N determined at factory create-time. /// - IManagedLifetimeMetricHandle CreateSummary(string name, string help, string[] labelNames, SummaryConfiguration? configuration = null); + IManagedLifetimeMetricHandle CreateSummary(string name, string help, string[]? labelNames = null, SummaryConfiguration? configuration = null); /// /// Returns a new metric factory that will add the specified labels to any metrics created using it. diff --git a/Prometheus/LabelEnrichingManagedLifetimeMetricFactory.cs b/Prometheus/LabelEnrichingManagedLifetimeMetricFactory.cs index 8ad14160..51feb066 100644 --- a/Prometheus/LabelEnrichingManagedLifetimeMetricFactory.cs +++ b/Prometheus/LabelEnrichingManagedLifetimeMetricFactory.cs @@ -28,9 +28,9 @@ public LabelEnrichingManagedLifetimeMetricFactory(ManagedLifetimeMetricFactory i private readonly string[] _enrichWithLabelNames; private readonly string[] _enrichWithLabelValues; - public IManagedLifetimeMetricHandle CreateCounter(string name, string help, string[] labelNames, CounterConfiguration? configuration = null) + public IManagedLifetimeMetricHandle CreateCounter(string name, string help, string[]? instanceLabelNames, CounterConfiguration? configuration) { - var combinedLabelNames = WithEnrichedLabelNames(labelNames); + var combinedLabelNames = WithEnrichedLabelNames(instanceLabelNames ?? Array.Empty()); var innerHandle = _inner.CreateCounter(name, help, combinedLabelNames, configuration); // 1-1 relationship between instance of inner handle and our labeling handle. @@ -41,9 +41,9 @@ public IManagedLifetimeMetricHandle CreateCounter(string name, string private LabelEnrichingManagedLifetimeCounter CreateCounterCore(IManagedLifetimeMetricHandle inner) => new LabelEnrichingManagedLifetimeCounter(inner, _enrichWithLabelValues); private readonly ConcurrentDictionary, LabelEnrichingManagedLifetimeCounter> _counters = new(); - public IManagedLifetimeMetricHandle CreateGauge(string name, string help, string[] labelNames, GaugeConfiguration? configuration = null) + public IManagedLifetimeMetricHandle CreateGauge(string name, string help, string[]? instanceLabelNames, GaugeConfiguration? configuration) { - var combinedLabelNames = WithEnrichedLabelNames(labelNames); + var combinedLabelNames = WithEnrichedLabelNames(instanceLabelNames ?? Array.Empty()); var innerHandle = _inner.CreateGauge(name, help, combinedLabelNames, configuration); // 1-1 relationship between instance of inner handle and our labeling handle. @@ -54,9 +54,9 @@ public IManagedLifetimeMetricHandle CreateGauge(string name, string help private LabelEnrichingManagedLifetimeGauge CreateGaugeCore(IManagedLifetimeMetricHandle inner) => new LabelEnrichingManagedLifetimeGauge(inner, _enrichWithLabelValues); private readonly ConcurrentDictionary, LabelEnrichingManagedLifetimeGauge> _gauges = new(); - public IManagedLifetimeMetricHandle CreateHistogram(string name, string help, string[] labelNames, HistogramConfiguration? configuration = null) + public IManagedLifetimeMetricHandle CreateHistogram(string name, string help, string[]? instanceLabelNames, HistogramConfiguration? configuration) { - var combinedLabelNames = WithEnrichedLabelNames(labelNames); + var combinedLabelNames = WithEnrichedLabelNames(instanceLabelNames ?? Array.Empty()); var innerHandle = _inner.CreateHistogram(name, help, combinedLabelNames, configuration); // 1-1 relationship between instance of inner handle and our labeling handle. @@ -67,9 +67,9 @@ public IManagedLifetimeMetricHandle CreateHistogram(string name, str private LabelEnrichingManagedLifetimeHistogram CreateHistogramCore(IManagedLifetimeMetricHandle inner) => new LabelEnrichingManagedLifetimeHistogram(inner, _enrichWithLabelValues); private readonly ConcurrentDictionary, LabelEnrichingManagedLifetimeHistogram> _histograms = new(); - public IManagedLifetimeMetricHandle CreateSummary(string name, string help, string[] labelNames, SummaryConfiguration? configuration = null) + public IManagedLifetimeMetricHandle CreateSummary(string name, string help, string[]? instanceLabelNames, SummaryConfiguration? configuration) { - var combinedLabelNames = WithEnrichedLabelNames(labelNames); + var combinedLabelNames = WithEnrichedLabelNames(instanceLabelNames ?? Array.Empty()); var innerHandle = _inner.CreateSummary(name, help, combinedLabelNames, configuration); // 1-1 relationship between instance of inner handle and our labeling handle. diff --git a/Prometheus/ManagedLifetimeMetricFactory.cs b/Prometheus/ManagedLifetimeMetricFactory.cs index 159f3c9f..9616a9d1 100644 --- a/Prometheus/ManagedLifetimeMetricFactory.cs +++ b/Prometheus/ManagedLifetimeMetricFactory.cs @@ -23,9 +23,9 @@ public IManagedLifetimeMetricFactory WithLabels(IDictionary labe return new LabelEnrichingManagedLifetimeMetricFactory(this, labels); } - public IManagedLifetimeMetricHandle CreateCounter(string name, string help, string[] instanceLabelNames, CounterConfiguration? configuration = null) + public IManagedLifetimeMetricHandle CreateCounter(string name, string help, string[]? instanceLabelNames, CounterConfiguration? configuration) { - var identity = new ManagedLifetimeMetricIdentity(name, StringSequence.From(instanceLabelNames)); + var identity = new ManagedLifetimeMetricIdentity(name, StringSequence.From(instanceLabelNames ?? Array.Empty())); // Let's be optimistic and assume that in the typical case, the metric will already exist. if (_counters.TryGetValue(identity, out var existing)) @@ -35,9 +35,9 @@ public IManagedLifetimeMetricHandle CreateCounter(string name, string return _counters.GetOrAdd(identity, initializer.CreateInstance); } - public IManagedLifetimeMetricHandle CreateGauge(string name, string help, string[] instanceLabelNames, GaugeConfiguration? configuration = null) + public IManagedLifetimeMetricHandle CreateGauge(string name, string help, string[]? instanceLabelNames, GaugeConfiguration? configuration) { - var identity = new ManagedLifetimeMetricIdentity(name, StringSequence.From(instanceLabelNames)); + var identity = new ManagedLifetimeMetricIdentity(name, StringSequence.From(instanceLabelNames ?? Array.Empty())); // Let's be optimistic and assume that in the typical case, the metric will already exist. if (_gauges.TryGetValue(identity, out var existing)) @@ -47,9 +47,9 @@ public IManagedLifetimeMetricHandle CreateGauge(string name, string help return _gauges.GetOrAdd(identity, initializer.CreateInstance); } - public IManagedLifetimeMetricHandle CreateHistogram(string name, string help, string[] instanceLabelNames, HistogramConfiguration? configuration = null) + public IManagedLifetimeMetricHandle CreateHistogram(string name, string help, string[]? instanceLabelNames, HistogramConfiguration? configuration) { - var identity = new ManagedLifetimeMetricIdentity(name, StringSequence.From(instanceLabelNames)); + var identity = new ManagedLifetimeMetricIdentity(name, StringSequence.From(instanceLabelNames ?? Array.Empty())); // Let's be optimistic and assume that in the typical case, the metric will already exist. if (_histograms.TryGetValue(identity, out var existing)) @@ -59,9 +59,9 @@ public IManagedLifetimeMetricHandle CreateHistogram(string name, str return _histograms.GetOrAdd(identity, initializer.CreateInstance); } - public IManagedLifetimeMetricHandle CreateSummary(string name, string help, string[] instanceLabelNames, SummaryConfiguration? configuration = null) + public IManagedLifetimeMetricHandle CreateSummary(string name, string help, string[]? instanceLabelNames, SummaryConfiguration? configuration) { - var identity = new ManagedLifetimeMetricIdentity(name, StringSequence.From(instanceLabelNames)); + var identity = new ManagedLifetimeMetricIdentity(name, StringSequence.From(instanceLabelNames ?? Array.Empty())); // Let's be optimistic and assume that in the typical case, the metric will already exist. if (_summaries.TryGetValue(identity, out var existing)) From 8b7c7e126664c6181a3b9ef87ac251949f502b26 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Fri, 27 Jan 2023 14:41:21 +0200 Subject: [PATCH 077/230] Fixup tests --- Prometheus/MeterAdapter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Prometheus/MeterAdapter.cs b/Prometheus/MeterAdapter.cs index 0cedd6aa..745ee1ff 100644 --- a/Prometheus/MeterAdapter.cs +++ b/Prometheus/MeterAdapter.cs @@ -24,7 +24,7 @@ private MeterAdapter(MeterAdapterOptions options) _factory = (ManagedLifetimeMetricFactory)Metrics.WithCustomRegistry(_options.Registry) .WithManagedLifetime(expiresAfter: options.MetricsExpireAfter); - _inheritedStaticLabelNames = _factory.GetAllStaticLabelNames().ToArray(); + _inheritedStaticLabelNames = ((ManagedLifetimeMetricFactory)_factory).GetAllStaticLabelNames().ToArray(); _listener.InstrumentPublished = OnInstrumentPublished; _listener.MeasurementsCompleted += OnMeasurementsCompleted; @@ -56,7 +56,7 @@ private MeterAdapter(MeterAdapterOptions options) private readonly MeterAdapterOptions _options; private readonly CollectorRegistry _registry; - private readonly ManagedLifetimeMetricFactory _factory; + private readonly IManagedLifetimeMetricFactory _factory; private readonly string[] _inheritedStaticLabelNames; private readonly Gauge _instrumentsConnected; From 9ec9e9b13d275c826c2cf6f027dab54ed65475f6 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Fri, 27 Jan 2023 17:40:55 +0200 Subject: [PATCH 078/230] Bugfix: auto-leasing label enrichment failed to enrich if 0 extra lebels were provided --- Prometheus/LabelEnrichingAutoLeasingMetric.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Prometheus/LabelEnrichingAutoLeasingMetric.cs b/Prometheus/LabelEnrichingAutoLeasingMetric.cs index c0da24db..6228b5d3 100644 --- a/Prometheus/LabelEnrichingAutoLeasingMetric.cs +++ b/Prometheus/LabelEnrichingAutoLeasingMetric.cs @@ -12,7 +12,7 @@ public LabelEnrichingAutoLeasingMetric(ICollector inner, string[] enric private readonly ICollector _inner; private readonly string[] _enrichedLabelValues; - public TMetric Unlabelled => _inner.Unlabelled; + public TMetric Unlabelled => _inner.WithLabels(_enrichedLabelValues); public string Name => _inner.Name; public string Help => _inner.Help; From d20e0beaacc49b4f3470620bd9c1d061f34e9dce Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Sun, 29 Jan 2023 06:42:30 +0200 Subject: [PATCH 079/230] Dispose of HTTP responses in sample code for faster and more granular cleanup --- Sample.Web/SampleService.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sample.Web/SampleService.cs b/Sample.Web/SampleService.cs index 3d197ef5..859f7663 100644 --- a/Sample.Web/SampleService.cs +++ b/Sample.Web/SampleService.cs @@ -56,13 +56,13 @@ private async Task ReadySetGoAsync(CancellationToken cancel) var googleTask = Task.Run(async delegate { - await httpClient.GetAsync(googleUrl, cancel); + using var response = await httpClient.GetAsync(googleUrl, cancel); googleStopwatch.Stop(); }, cancel); var microsoftTask = Task.Run(async delegate { - await httpClient.GetAsync(microsoftUrl, cancel); + using var response = await httpClient.GetAsync(microsoftUrl, cancel); microsoftStopwatch.Stop(); }, cancel); From cefb61cefee8561ac14cc7101d7ebce433a43abe Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Sun, 29 Jan 2023 07:18:12 +0200 Subject: [PATCH 080/230] Update benchmark results for .NET 7 --- Benchmark.NetCore/Benchmark.NetCore.csproj | 2 +- Docs/MeasurementsBenchmarks.xlsx | Bin 0 -> 10322 bytes Docs/SdkComparison-MeasurementCpuUsage.png | Bin 29214 -> 29189 bytes Docs/SdkComparison-MeasurementMemoryUsage.png | Bin 27757 -> 29872 bytes Docs/SdkComparison-SetupCpuUsage.png | Bin 26970 -> 26945 bytes Docs/SdkComparison-SetupMemoryUsage.png | Bin 25858 -> 25448 bytes Docs/SdkPerformanceComparison.xlsx | Bin 28059 -> 27721 bytes README.md | 10 +++++----- 8 files changed, 6 insertions(+), 6 deletions(-) create mode 100644 Docs/MeasurementsBenchmarks.xlsx diff --git a/Benchmark.NetCore/Benchmark.NetCore.csproj b/Benchmark.NetCore/Benchmark.NetCore.csproj index c2493714..5562708a 100644 --- a/Benchmark.NetCore/Benchmark.NetCore.csproj +++ b/Benchmark.NetCore/Benchmark.NetCore.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net7.0 true ..\Resources\prometheus-net.snk diff --git a/Docs/MeasurementsBenchmarks.xlsx b/Docs/MeasurementsBenchmarks.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..524f34d48ef2a813a6b227a9d3045e93c6e8109b GIT binary patch literal 10322 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 N4;tj9+>}3`{vTlS;nDyA literal 0 HcmV?d00001 diff --git a/Docs/SdkComparison-MeasurementCpuUsage.png b/Docs/SdkComparison-MeasurementCpuUsage.png index 6f8a37af85a5f3bf4924754ac452bfbb4cc961d2..8c0d4fd749eb120af3d5b0fc40ad3b711e096305 100644 GIT binary patch literal 29189 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 literal 29214 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 diff --git a/Docs/SdkComparison-MeasurementMemoryUsage.png b/Docs/SdkComparison-MeasurementMemoryUsage.png index cff28f8fa2227141862d70de6516e31b8d29408a..e3c7ff1a94d83170df8c906263c5365356e4fb53 100644 GIT binary patch literal 29872 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 P$c~HO__)=a!(aO!7evGq diff --git a/Docs/SdkComparison-SetupCpuUsage.png b/Docs/SdkComparison-SetupCpuUsage.png index 795931373f8efb0dcb29620f7ab0726ba17ef8ea..51bebfe1cce3632c037c503fd280dbe2ab861ae2 100644 GIT binary patch literal 26945 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 P6^m2HBSX4_fB5-dFoi;- literal 26970 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 D_BxD- diff --git a/Docs/SdkComparison-SetupMemoryUsage.png b/Docs/SdkComparison-SetupMemoryUsage.png index 15b977c3a301d3117b98f254083497a8a506c804..0c0ecc34f91c18146ac327a820b703995dd51559 100644 GIT binary patch literal 25448 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 H-v9pqzF*J> diff --git a/Docs/SdkPerformanceComparison.xlsx b/Docs/SdkPerformanceComparison.xlsx index 9b5427f0362b495213b1f2ff863b1d8ba4b90ca5..b878ad2ce5663e9f8d580e321b7577c4110cc7d3 100644 GIT binary patch delta 12100 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 delta 12365 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 E2PRl8cK`qY diff --git a/README.md b/README.md index 7e2969d7..499f2423 100644 --- a/README.md +++ b/README.md @@ -851,13 +851,13 @@ As an example of the performance of measuring data using prometheus-net, we have | Metric type | Concurrency | Measurements per second | |-------------|------------:|------------------------:| -| Gauge | 1 thread | 539 million | -| Counter | 1 thread | 57 million | -| Histogram | 1 thread | 56 million | +| Gauge | 1 thread | 521 million | +| Counter | 1 thread | 87 million | +| Histogram | 1 thread | 46 million | | Summary | 1 thread | 2 million | -| Gauge | 16 threads | 56 million | +| Gauge | 16 threads | 55 million | | Counter | 16 threads | 10 million | -| Histogram | 16 threads | 7 million | +| Histogram | 16 threads | 14 million | | Summary | 16 threads | 2 million | > **Note** From 1941aaf947f35c60be7eacb8aec84e9cea14e7db Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Mon, 30 Jan 2023 06:47:11 +0200 Subject: [PATCH 081/230] Docs correction --- Prometheus/EventCounterAdapter.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/Prometheus/EventCounterAdapter.cs b/Prometheus/EventCounterAdapter.cs index 5052a376..fc9ad0f3 100644 --- a/Prometheus/EventCounterAdapter.cs +++ b/Prometheus/EventCounterAdapter.cs @@ -10,13 +10,6 @@ namespace Prometheus ///

/// /// All .NET event counters are transformed into Prometheus metrics with translated names. - /// - /// There appear to be different types of "incrementing" counters in .NET: - /// * some "incrementing" counters publish the increment value ("+5") - /// * some "incrementing" counters are just gauges that publish a "current" value - /// - /// It is not possible for us to really determine which is which, so for incrementing event counters we publish both a gauge (with latest value) and counter (with total value). - /// Which one you use for which .NET event counter depends on how the authors of the event counter made it - one of them will be wrong! /// public sealed class EventCounterAdapter : IDisposable { From d820de54dec89401db3f62a883986480a12f1267 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Mon, 30 Jan 2023 06:47:45 +0200 Subject: [PATCH 082/230] Tidy --- Prometheus/EventCounterAdapter.cs | 315 +++++++++++++++--------------- 1 file changed, 157 insertions(+), 158 deletions(-) diff --git a/Prometheus/EventCounterAdapter.cs b/Prometheus/EventCounterAdapter.cs index fc9ad0f3..36bcf4fc 100644 --- a/Prometheus/EventCounterAdapter.cs +++ b/Prometheus/EventCounterAdapter.cs @@ -3,222 +3,221 @@ using System.Diagnostics.Tracing; using System.Globalization; -namespace Prometheus +namespace Prometheus; + +/// +/// Monitors .NET EventCounters and exposes them as Prometheus metrics. +/// +/// +/// All observed .NET event counters are transformed into Prometheus metrics with translated names. +/// +public sealed class EventCounterAdapter : IDisposable { - /// - /// Monitors all .NET EventCounters and exposes them as Prometheus metrics. - /// - /// - /// All .NET event counters are transformed into Prometheus metrics with translated names. - /// - public sealed class EventCounterAdapter : IDisposable - { - public static IDisposable StartListening() => new EventCounterAdapter(EventCounterAdapterOptions.Default); + public static IDisposable StartListening() => new EventCounterAdapter(EventCounterAdapterOptions.Default); - public static IDisposable StartListening(EventCounterAdapterOptions options) => new EventCounterAdapter(options); + public static IDisposable StartListening(EventCounterAdapterOptions options) => new EventCounterAdapter(options); - private EventCounterAdapter(EventCounterAdapterOptions options) - { - _options = options; - _metricFactory = Metrics.WithCustomRegistry(_options.Registry); + private EventCounterAdapter(EventCounterAdapterOptions options) + { + _options = options; + _metricFactory = Metrics.WithCustomRegistry(_options.Registry); - _eventSourcesConnected = _metricFactory.CreateGauge("prometheus_net_eventcounteradapter_sources_connected_total", "Number of event sources that are currently connected to the adapter."); + _eventSourcesConnected = _metricFactory.CreateGauge("prometheus_net_eventcounteradapter_sources_connected_total", "Number of event sources that are currently connected to the adapter."); - EventCounterAdapterMemoryWarden.EnsureStarted(); + EventCounterAdapterMemoryWarden.EnsureStarted(); - _listener = new Listener(ShouldUseEventSource, ConfigureEventSource, options.UpdateInterval, OnEventWritten); - } + _listener = new Listener(ShouldUseEventSource, ConfigureEventSource, options.UpdateInterval, OnEventWritten); + } - public void Dispose() - { - // Disposal means we stop listening but we do not remove any published data just to keep things simple. - _listener.Dispose(); - } + public void Dispose() + { + // Disposal means we stop listening but we do not remove any published data just to keep things simple. + _listener.Dispose(); + } - private readonly EventCounterAdapterOptions _options; - private readonly IMetricFactory _metricFactory; + private readonly EventCounterAdapterOptions _options; + private readonly IMetricFactory _metricFactory; - private readonly Listener _listener; + private readonly Listener _listener; - // We never decrease it in the current implementation but perhaps might in a future implementation, so might as well make it a gauge. - private readonly Gauge _eventSourcesConnected; + // We never decrease it in the current implementation but perhaps might in a future implementation, so might as well make it a gauge. + private readonly Gauge _eventSourcesConnected; - private bool ShouldUseEventSource(EventSource source) - { - bool connect = _options.EventSourceFilterPredicate(source.Name); + private bool ShouldUseEventSource(EventSource source) + { + bool connect = _options.EventSourceFilterPredicate(source.Name); - if (connect) - _eventSourcesConnected.Inc(); + if (connect) + _eventSourcesConnected.Inc(); - return connect; - } + return connect; + } - private EventCounterAdapterEventSourceSettings ConfigureEventSource(EventSource source) - { - return _options.EventSourceSettingsProvider(source.Name); - } + private EventCounterAdapterEventSourceSettings ConfigureEventSource(EventSource source) + { + return _options.EventSourceSettingsProvider(source.Name); + } - private const string RateSuffix = "_rate"; + private const string RateSuffix = "_rate"; - private void OnEventWritten(EventWrittenEventArgs args) - { - // This deserialization here is pretty gnarly. - // We just skip anything that makes no sense. + private void OnEventWritten(EventWrittenEventArgs args) + { + // This deserialization here is pretty gnarly. + // We just skip anything that makes no sense. - try - { - if (args.EventName != "EventCounters") - return; // Do not know what it is and do not care. + try + { + if (args.EventName != "EventCounters") + return; // Do not know what it is and do not care. - if (args.Payload == null) - return; // What? Whatever. + if (args.Payload == null) + return; // What? Whatever. - var eventSourceName = args.EventSource.Name; + var eventSourceName = args.EventSource.Name; - foreach (var item in args.Payload) - { - if (item is not IDictionary e) - continue; + foreach (var item in args.Payload) + { + if (item is not IDictionary e) + continue; - if (!e.TryGetValue("Name", out var nameWrapper)) - continue; + if (!e.TryGetValue("Name", out var nameWrapper)) + continue; - var name = nameWrapper as string; + var name = nameWrapper as string; - if (name == null) - continue; // What? Whatever. + if (name == null) + continue; // What? Whatever. - if (!e.TryGetValue("DisplayName", out var displayNameWrapper)) - continue; + if (!e.TryGetValue("DisplayName", out var displayNameWrapper)) + continue; - var displayName = displayNameWrapper as string ?? ""; + var displayName = displayNameWrapper as string ?? ""; - // If there is a DisplayUnits, prefix it to the help text. - if (e.TryGetValue("DisplayUnits", out var displayUnitsWrapper) && !string.IsNullOrWhiteSpace(displayUnitsWrapper as string)) - displayName = $"({(string)displayUnitsWrapper}) {displayName}"; + // If there is a DisplayUnits, prefix it to the help text. + if (e.TryGetValue("DisplayUnits", out var displayUnitsWrapper) && !string.IsNullOrWhiteSpace(displayUnitsWrapper as string)) + displayName = $"({(string)displayUnitsWrapper}) {displayName}"; - var mergedName = $"{eventSourceName}_{name}"; + var mergedName = $"{eventSourceName}_{name}"; - var prometheusName = _counterPrometheusName.GetOrAdd(mergedName, PrometheusNameHelpers.TranslateNameToPrometheusName); + var prometheusName = _counterPrometheusName.GetOrAdd(mergedName, PrometheusNameHelpers.TranslateNameToPrometheusName); - // The event counter can either be - // 1) an aggregating counter (in which case we use the mean); or - // 2) an incrementing counter (in which case we use the delta). + // The event counter can either be + // 1) an aggregating counter (in which case we use the mean); or + // 2) an incrementing counter (in which case we use the delta). - if (e.TryGetValue("Increment", out var increment)) - { - // Looks like an incrementing counter. + if (e.TryGetValue("Increment", out var increment)) + { + // Looks like an incrementing counter. - var value = increment as double?; + var value = increment as double?; - if (value == null) - continue; // What? Whatever. + if (value == null) + continue; // What? Whatever. - // If the underlying metric is exposing a rate then this can result in some strange terminology like "rate_total". - // We will remove the "rate" from the name to be more understandable - you'll get the rate when you apply the Prometheus rate() function, the raw value is not the rate. - if (prometheusName.EndsWith(RateSuffix)) - prometheusName = prometheusName.Remove(prometheusName.Length - RateSuffix.Length); + // If the underlying metric is exposing a rate then this can result in some strange terminology like "rate_total". + // We will remove the "rate" from the name to be more understandable - you'll get the rate when you apply the Prometheus rate() function, the raw value is not the rate. + if (prometheusName.EndsWith(RateSuffix)) + prometheusName = prometheusName.Remove(prometheusName.Length - RateSuffix.Length); - _metricFactory.CreateCounter(prometheusName + "_total", displayName).Inc(value.Value); - } - else if (e.TryGetValue("Mean", out var mean)) - { - // Looks like an aggregating counter. + _metricFactory.CreateCounter(prometheusName + "_total", displayName).Inc(value.Value); + } + else if (e.TryGetValue("Mean", out var mean)) + { + // Looks like an aggregating counter. - var value = mean as double?; + var value = mean as double?; - if (value == null) - continue; // What? Whatever. + if (value == null) + continue; // What? Whatever. - _metricFactory.CreateGauge(prometheusName, displayName).Set(value.Value); - } + _metricFactory.CreateGauge(prometheusName, displayName).Set(value.Value); } } - catch (Exception ex) - { - // We do not want to throw any exceptions if we fail to handle this event because who knows what it messes up upstream. - Trace.WriteLine($"Failed to parse EventCounter event: {ex.Message}"); - } } + catch (Exception ex) + { + // We do not want to throw any exceptions if we fail to handle this event because who knows what it messes up upstream. + Trace.WriteLine($"Failed to parse EventCounter event: {ex.Message}"); + } + } - // Source+Name -> Name - private readonly ConcurrentDictionary _counterPrometheusName = new(); + // Source+Name -> Name + private readonly ConcurrentDictionary _counterPrometheusName = new(); - private sealed class Listener : EventListener + private sealed class Listener : EventListener + { + public Listener( + Func shouldUseEventSource, + Func configureEventSosurce, + TimeSpan updateInterval, + Action onEventWritten) { - public Listener( - Func shouldUseEventSource, - Func configureEventSosurce, - TimeSpan updateInterval, - Action onEventWritten) - { - _shouldUseEventSource = shouldUseEventSource; - _configureEventSosurce = configureEventSosurce; - _updateInterval = updateInterval; - _onEventWritten = onEventWritten; + _shouldUseEventSource = shouldUseEventSource; + _configureEventSosurce = configureEventSosurce; + _updateInterval = updateInterval; + _onEventWritten = onEventWritten; - foreach (var eventSource in _preRegisteredEventSources) - OnEventSourceCreated(eventSource); + foreach (var eventSource in _preRegisteredEventSources) + OnEventSourceCreated(eventSource); - _preRegisteredEventSources.Clear(); - } + _preRegisteredEventSources.Clear(); + } - private readonly List _preRegisteredEventSources = new List(); + private readonly List _preRegisteredEventSources = new List(); - private readonly Func _shouldUseEventSource; - private readonly Func _configureEventSosurce; - private readonly TimeSpan _updateInterval; - private readonly Action _onEventWritten; + private readonly Func _shouldUseEventSource; + private readonly Func _configureEventSosurce; + private readonly TimeSpan _updateInterval; + private readonly Action _onEventWritten; - protected override void OnEventSourceCreated(EventSource eventSource) + protected override void OnEventSourceCreated(EventSource eventSource) + { + if (_shouldUseEventSource == null) { - if (_shouldUseEventSource == null) - { - // The way this EventListener thing works is rather strange. Immediately in the base class constructor, before we - // have even had time to wire up our subclass, it starts calling OnEventSourceCreated for all already-existing event sources... - // We just buffer those calls because CALM DOWN SIR! - _preRegisteredEventSources.Add(eventSource); - return; - } + // The way this EventListener thing works is rather strange. Immediately in the base class constructor, before we + // have even had time to wire up our subclass, it starts calling OnEventSourceCreated for all already-existing event sources... + // We just buffer those calls because CALM DOWN SIR! + _preRegisteredEventSources.Add(eventSource); + return; + } - if (!_shouldUseEventSource(eventSource)) - return; + if (!_shouldUseEventSource(eventSource)) + return; - try - { - var options = _configureEventSosurce(eventSource); + try + { + var options = _configureEventSosurce(eventSource); - EnableEvents(eventSource, options.MinimumLevel, options.MatchKeywords, new Dictionary() - { - ["EventCounterIntervalSec"] = ((int)Math.Max(1, _updateInterval.TotalSeconds)).ToString(CultureInfo.InvariantCulture), - }); - } - catch (Exception ex) + EnableEvents(eventSource, options.MinimumLevel, options.MatchKeywords, new Dictionary() { - // Eat exceptions here to ensure no harm comes of failed enabling. - // The EventCounter infrastructure has proven quite buggy and while it is not certain that this may throw, let's be paranoid. - Trace.WriteLine($"Failed to enable EventCounter listening for {eventSource.Name}: {ex.Message}"); - } + ["EventCounterIntervalSec"] = ((int)Math.Max(1, _updateInterval.TotalSeconds)).ToString(CultureInfo.InvariantCulture), + }); } - - protected override void OnEventWritten(EventWrittenEventArgs eventData) + catch (Exception ex) { - _onEventWritten(eventData); + // Eat exceptions here to ensure no harm comes of failed enabling. + // The EventCounter infrastructure has proven quite buggy and while it is not certain that this may throw, let's be paranoid. + Trace.WriteLine($"Failed to enable EventCounter listening for {eventSource.Name}: {ex.Message}"); } } - /// - /// By default we enable event sources that start with any of these strings. This is a manually curated list to try enable some useful ones - /// without just enabling everything under the sky (because .NET has no way to say "enable only the event counters", you have to enable all diagnostic events). - /// - private static readonly IReadOnlyList DefaultEventSourcePrefixes = new[] + protected override void OnEventWritten(EventWrittenEventArgs eventData) { - "System.Runtime", - "Microsoft-AspNetCore", - "Microsoft.AspNetCore", - "System.Net" - }; - - public static readonly Func DefaultEventSourceFilterPredicate = name => DefaultEventSourcePrefixes.Any(x => name.StartsWith(x, StringComparison.Ordinal)); + _onEventWritten(eventData); + } } + + /// + /// By default we enable event sources that start with any of these strings. This is a manually curated list to try enable some useful ones + /// without just enabling everything under the sky (because .NET has no way to say "enable only the event counters", you have to enable all diagnostic events). + /// + private static readonly IReadOnlyList DefaultEventSourcePrefixes = new[] + { + "System.Runtime", + "Microsoft-AspNetCore", + "Microsoft.AspNetCore", + "System.Net" + }; + + public static readonly Func DefaultEventSourceFilterPredicate = name => DefaultEventSourcePrefixes.Any(x => name.StartsWith(x, StringComparison.Ordinal)); } From dd87cfd22554fa7b6dbf65c3346dd5deb72c1ced Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Mon, 30 Jan 2023 07:05:43 +0200 Subject: [PATCH 083/230] Added support for custom factory in HTTP server metrics --- .../HttpMetrics/HttpMetricsOptionsBase.cs | 12 ++- .../HttpMetrics/HttpRequestMiddlewareBase.cs | 4 +- .../HttpMetricsMiddlewareExtensions.cs | 97 +++++++++---------- .../RouteParameterMappingTests.cs | 46 +++++++++ 4 files changed, 107 insertions(+), 52 deletions(-) diff --git a/Prometheus.AspNetCore/HttpMetrics/HttpMetricsOptionsBase.cs b/Prometheus.AspNetCore/HttpMetrics/HttpMetricsOptionsBase.cs index bfae5120..bd9fb584 100644 --- a/Prometheus.AspNetCore/HttpMetrics/HttpMetricsOptionsBase.cs +++ b/Prometheus.AspNetCore/HttpMetrics/HttpMetricsOptionsBase.cs @@ -32,10 +32,20 @@ public abstract class HttpMetricsOptionsBase /// /// Allows you to override the registry used to create the default metric instance. - /// Value is ignored if you specify a custom metric instance in the options. /// + /// + /// Value is ignored if you specify a custom metric instance or metric factory in the options (instance overrides factory overrides registry). + /// public CollectorRegistry? Registry { get; set; } + /// + /// Allows you to override the metric factory used to create the default metric instance. + /// + /// + /// Value is ignored if you specify a custom metric instance in the options (instance overrides factory overrides registry). + /// + public IMetricFactory? MetricFactory { get; set; } + /// /// If set, the "page" label will be considered one of the built-in default labels. /// This is only enabled if Razor Pages is detected at the middleware setup stage. diff --git a/Prometheus.AspNetCore/HttpMetrics/HttpRequestMiddlewareBase.cs b/Prometheus.AspNetCore/HttpMetrics/HttpRequestMiddlewareBase.cs index 1a8e4f4d..dc15f45e 100644 --- a/Prometheus.AspNetCore/HttpMetrics/HttpRequestMiddlewareBase.cs +++ b/Prometheus.AspNetCore/HttpMetrics/HttpRequestMiddlewareBase.cs @@ -47,7 +47,7 @@ internal abstract class HttpRequestMiddlewareBase /// The factory to use for creating the default metric for this middleware. /// Not used if a custom metric is already provided in options. /// - protected MetricFactory MetricFactory { get; } + protected IMetricFactory MetricFactory { get; } private readonly List _additionalRouteParameters; private readonly List _customLabels; @@ -64,7 +64,7 @@ internal abstract class HttpRequestMiddlewareBase protected HttpRequestMiddlewareBase(HttpMetricsOptionsBase options, TCollector? customMetric) { - MetricFactory = Metrics.WithCustomRegistry(options.Registry ?? Metrics.DefaultRegistry); + MetricFactory = options.MetricFactory ?? Metrics.WithCustomRegistry(options.Registry ?? Metrics.DefaultRegistry); _additionalRouteParameters = options.AdditionalRouteParameters ?? new List(0); _customLabels = options.CustomLabels ?? new List(0); diff --git a/Prometheus.AspNetCore/HttpMetricsMiddlewareExtensions.cs b/Prometheus.AspNetCore/HttpMetricsMiddlewareExtensions.cs index 1016a3b3..8f9987b2 100644 --- a/Prometheus.AspNetCore/HttpMetricsMiddlewareExtensions.cs +++ b/Prometheus.AspNetCore/HttpMetricsMiddlewareExtensions.cs @@ -3,66 +3,65 @@ using Microsoft.Extensions.DependencyInjection; using Prometheus.HttpMetrics; -namespace Prometheus +namespace Prometheus; + +public static class HttpMetricsMiddlewareExtensions { - public static class HttpMetricsMiddlewareExtensions + /// + /// Configures the ASP.NET Core request pipeline to collect Prometheus metrics on processed HTTP requests. + /// + /// Call this after .UseRouting(). + /// + public static IApplicationBuilder UseHttpMetrics(this IApplicationBuilder app, + Action configure) { - /// - /// Configures the ASP.NET Core request pipeline to collect Prometheus metrics on processed HTTP requests. - /// - /// If using ASP.NET Core 3 or newer, call this after .UseRouting(). - /// - public static IApplicationBuilder UseHttpMetrics(this IApplicationBuilder app, - Action configure) - { - var options = new HttpMiddlewareExporterOptions(); + var options = new HttpMiddlewareExporterOptions(); - configure?.Invoke(options); + configure?.Invoke(options); - app.UseHttpMetrics(options); + app.UseHttpMetrics(options); - return app; - } + return app; + } - /// - /// Configures the ASP.NET Core request pipeline to collect Prometheus metrics on processed HTTP requests. - /// - /// If using ASP.NET Core 3 or newer, call this after .UseRouting(). - /// - public static IApplicationBuilder UseHttpMetrics(this IApplicationBuilder app, - HttpMiddlewareExporterOptions? options = null) - { - options = options ?? new HttpMiddlewareExporterOptions(); + /// + /// Configures the ASP.NET Core request pipeline to collect Prometheus metrics on processed HTTP requests. + /// + /// Call this after .UseRouting(). + /// + public static IApplicationBuilder UseHttpMetrics(this IApplicationBuilder app, + HttpMiddlewareExporterOptions? options = null) + { + options = options ?? new HttpMiddlewareExporterOptions(); - if (app.ApplicationServices.GetService() != null) - { - // If Razor Pages is enabled, we will automatically add a "page" route parameter to represent it. We do this only if no custom metric is used. - // If a custom metric is used, we still allow "page" label to be present and automatically fill it with the Razor Pages route parameter - // unless there is a custom label with this name added, in which case the custom label takes priority. + if (app.ApplicationServices.GetService() != null) + { + // If Razor Pages is enabled, we will automatically add a "page" route parameter to represent it. We do this only if no custom metric is used. + // If a custom metric is used, we still allow "page" label to be present and automatically fill it with the Razor Pages route parameter + // unless there is a custom label with this name added, in which case the custom label takes priority. - options.InProgress.IncludePageLabelInDefaultsInternal = true; - options.RequestCount.IncludePageLabelInDefaultsInternal = true; - options.RequestDuration.IncludePageLabelInDefaultsInternal = true; - } + options.InProgress.IncludePageLabelInDefaultsInternal = true; + options.RequestCount.IncludePageLabelInDefaultsInternal = true; + options.RequestDuration.IncludePageLabelInDefaultsInternal = true; + } - void ApplyConfiguration(IApplicationBuilder builder) - { - builder.UseMiddleware(); + void ApplyConfiguration(IApplicationBuilder builder) + { + builder.UseMiddleware(); - if (options.InProgress.Enabled) - builder.UseMiddleware(options.InProgress); - if (options.RequestCount.Enabled) - builder.UseMiddleware(options.RequestCount); - if (options.RequestDuration.Enabled) - builder.UseMiddleware(options.RequestDuration); - } + if (options.InProgress.Enabled) + builder.UseMiddleware(options.InProgress); + if (options.RequestCount.Enabled) + builder.UseMiddleware(options.RequestCount); + if (options.RequestDuration.Enabled) + builder.UseMiddleware(options.RequestDuration); + } - if (options.CaptureMetricsUrl) - ApplyConfiguration(app); - else - app.UseWhen(context => context.Request.Path != "/metrics", ApplyConfiguration); + if (options.CaptureMetricsUrl) + ApplyConfiguration(app); + else + app.UseWhen(context => context.Request.Path != "/metrics", ApplyConfiguration); - return app; - } + return app; } } \ No newline at end of file diff --git a/Tests.NetCore/HttpExporter/RouteParameterMappingTests.cs b/Tests.NetCore/HttpExporter/RouteParameterMappingTests.cs index f54ce074..c911647c 100644 --- a/Tests.NetCore/HttpExporter/RouteParameterMappingTests.cs +++ b/Tests.NetCore/HttpExporter/RouteParameterMappingTests.cs @@ -3,6 +3,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Prometheus.HttpMetrics; using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -63,6 +64,51 @@ public void DefaultMetric_AppliesStandardLabels() }, child.InstanceLabels.Values.ToArray()); } + [TestMethod] + public void DefaultMetric_WithCustomFactory_AppliesStandardLabelsAndFactoryLabels() + { + SetupHttpContext(_context, TestStatusCode, TestMethod, TestAction, TestController); + + var labelName = "static_label_1"; + var labelValue = "static_label_value_1"; + + var factory = Metrics.WithCustomRegistry(_registry) + .WithLabels(new Dictionary + { + { labelName, labelValue } + }); + + var middleware = new HttpRequestCountMiddleware(_next, new HttpRequestCountOptions + { + Registry = _registry, + MetricFactory = factory + }); + var child = (ChildBase)middleware.CreateChild(_context); + + CollectionAssert.AreEquivalent(DefaultLabelNamesPlusEndpoint, child.InstanceLabels.Names.ToArray()); + CollectionAssert.AreEquivalent(new[] + { + TestStatusCode.ToString(), + TestMethod, + TestAction, + TestController, + TestEndpoint + }, child.InstanceLabels.Values.ToArray()); + + var expectedFlattenedLabelNames = new[] { labelName }.Concat(DefaultLabelNamesPlusEndpoint).ToArray(); + var expectedFlattenedLabelValues = new[] { labelValue }.Concat(new[] + { + TestStatusCode.ToString(), + TestMethod, + TestAction, + TestController, + TestEndpoint + }).ToArray(); + + CollectionAssert.AreEquivalent(expectedFlattenedLabelNames, child.FlattenedLabels.Names.ToArray()); + CollectionAssert.AreEquivalent(expectedFlattenedLabelValues, child.FlattenedLabels.Values.ToArray()); + } + [TestMethod] public void CustomMetric_WithNoLabels_AppliesNoLabels() { From ba6cb14ae70da7e6880642bd7148549c557b4600 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Mon, 30 Jan 2023 08:47:08 +0200 Subject: [PATCH 084/230] Add HttpMiddlewareExporterOptions.SetMetricFactory --- .../HttpMiddlewareExporterOptions.cs | 118 ++++++++++-------- README.md | 2 +- 2 files changed, 65 insertions(+), 55 deletions(-) diff --git a/Prometheus.AspNetCore/HttpMetrics/HttpMiddlewareExporterOptions.cs b/Prometheus.AspNetCore/HttpMetrics/HttpMiddlewareExporterOptions.cs index e20f8565..90116f13 100644 --- a/Prometheus.AspNetCore/HttpMetrics/HttpMiddlewareExporterOptions.cs +++ b/Prometheus.AspNetCore/HttpMetrics/HttpMiddlewareExporterOptions.cs @@ -1,65 +1,75 @@ using Microsoft.AspNetCore.Http; -namespace Prometheus.HttpMetrics +namespace Prometheus.HttpMetrics; + +public sealed class HttpMiddlewareExporterOptions { - public sealed class HttpMiddlewareExporterOptions - { - public HttpInProgressOptions InProgress { get; set; } = new HttpInProgressOptions(); - public HttpRequestCountOptions RequestCount { get; set; } = new HttpRequestCountOptions(); - public HttpRequestDurationOptions RequestDuration { get; set; } = new HttpRequestDurationOptions(); + public HttpInProgressOptions InProgress { get; set; } = new HttpInProgressOptions(); + public HttpRequestCountOptions RequestCount { get; set; } = new HttpRequestCountOptions(); + public HttpRequestDurationOptions RequestDuration { get; set; } = new HttpRequestDurationOptions(); + + /// + /// Whether to capture metrics for queries to the /metrics endpoint (where metrics are exported by default). Defaults to false. + /// This matches against URLs starting with the /metrics string specifically - if you use a custom metrics endpoint, this will not match. + /// + public bool CaptureMetricsUrl { get; set; } - /// - /// Whether to capture metrics for queries to the /metrics endpoint (where metrics are exported by default). Defaults to false. - /// This matches against URLs starting with the /metrics string specifically - if you use a custom metrics endpoint, this will not match. - /// - public bool CaptureMetricsUrl { get; set; } + /// + /// Configures all the different types of metrics to use reduced status code cardinality (using 2xx instead of 200, 201 etc). + /// + public void ReduceStatusCodeCardinality() + { + InProgress.ReduceStatusCodeCardinality = true; + RequestCount.ReduceStatusCodeCardinality = true; + RequestDuration.ReduceStatusCodeCardinality = true; + } - /// - /// Configures all the different types of metrics to use reduced status code cardinality (using 2xx instead of 200, 201 etc). - /// - public void ReduceStatusCodeCardinality() - { - InProgress.ReduceStatusCodeCardinality = true; - RequestCount.ReduceStatusCodeCardinality = true; - RequestDuration.ReduceStatusCodeCardinality = true; - } + /// + /// Adds an additional route parameter to all the HTTP metrics. + /// + /// Helper method to avoid manually adding it to each one. + /// + public void AddRouteParameter(HttpRouteParameterMapping mapping) + { + InProgress.AdditionalRouteParameters.Add(mapping); + RequestCount.AdditionalRouteParameters.Add(mapping); + RequestDuration.AdditionalRouteParameters.Add(mapping); + } - /// - /// Adds an additional route parameter to all the HTTP metrics. - /// - /// Helper method to avoid manually adding it to each one. - /// - public void AddRouteParameter(HttpRouteParameterMapping mapping) - { - InProgress.AdditionalRouteParameters.Add(mapping); - RequestCount.AdditionalRouteParameters.Add(mapping); - RequestDuration.AdditionalRouteParameters.Add(mapping); - } + /// + /// Adds a custom label to all the HTTP metrics. + /// + /// Helper method to avoid manually adding it to each one. + /// + public void AddCustomLabel(HttpCustomLabel mapping) + { + InProgress.CustomLabels.Add(mapping); + RequestCount.CustomLabels.Add(mapping); + RequestDuration.CustomLabels.Add(mapping); + } - /// - /// Adds a custom label to all the HTTP metrics. - /// - /// Helper method to avoid manually adding it to each one. - /// - public void AddCustomLabel(HttpCustomLabel mapping) - { - InProgress.CustomLabels.Add(mapping); - RequestCount.CustomLabels.Add(mapping); - RequestDuration.CustomLabels.Add(mapping); - } + /// + /// Adds a custom label to all the HTTP metrics. + /// + /// Helper method to avoid manually adding it to each one. + /// + public void AddCustomLabel(string labelName, Func valueProvider) + { + var mapping = new HttpCustomLabel(labelName, valueProvider); - /// - /// Adds a custom label to all the HTTP metrics. - /// - /// Helper method to avoid manually adding it to each one. - /// - public void AddCustomLabel(string labelName, Func valueProvider) - { - var mapping = new HttpCustomLabel(labelName, valueProvider); + InProgress.CustomLabels.Add(mapping); + RequestCount.CustomLabels.Add(mapping); + RequestDuration.CustomLabels.Add(mapping); + } - InProgress.CustomLabels.Add(mapping); - RequestCount.CustomLabels.Add(mapping); - RequestDuration.CustomLabels.Add(mapping); - } + /// + /// Configures the middleware to use a custom metric factory for creating the metrics. + /// This provides an easy way to add custom static labels and configure exemplar behavior for all created metrics. + /// + public void SetMetricFactory(IMetricFactory metricFactory) + { + InProgress.MetricFactory = metricFactory; + RequestCount.MetricFactory = metricFactory; + RequestDuration.MetricFactory = metricFactory; } } \ No newline at end of file diff --git a/README.md b/README.md index 499f2423..ba66d815 100644 --- a/README.md +++ b/README.md @@ -828,7 +828,7 @@ The level of detail obtained from this is rather low - only the total count for You can configure the integration using `Metrics.ConfigureEventCounterAdapter()`. -By default, prometheus-net will only publish a small predefined set of general-purpose useful event counters to minimize resource consumption in the default configuration. A custom event source filter must now be provided in the configuration to enable publishing of additional event counters. +By default, prometheus-net will only publish [the well-known .NET EventCounters](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/available-counters) to minimize resource consumption in the default configuration. A custom event source filter must be provided in the configuration to enable publishing of additional event counters. See also, [Sample.Console](Sample.Console/Program.cs). From 91c075e0bf505b86ab405e2a932eb62edcd3d8d9 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Mon, 30 Jan 2023 10:42:49 +0200 Subject: [PATCH 085/230] Reduce Exemplar memory allocations from the LabelPair params-array. This cuts memory usage by approx 40% if every measurement uses an exemplar. --- Benchmark.NetCore/MeasurementBenchmarks.cs | 28 +- Benchmark.NetCore/SerializationBenchmarks.cs | 2 +- .../HttpMetrics/HttpInProgressMiddleware.cs | 43 +- .../HttpMetrics/HttpMetricsOptionsBase.cs | 120 ++--- .../HttpMetrics/HttpRequestCountMiddleware.cs | 51 +- .../HttpRequestDurationMiddleware.cs | 65 +-- .../HttpRequestExemplarPredicate.cs | 5 + Prometheus/AutoLeasingCounter.cs | 81 ++- Prometheus/AutoLeasingHistogram.cs | 83 ++-- Prometheus/ChildBase.cs | 9 +- Prometheus/Counter.cs | 14 +- Prometheus/Exemplar.cs | 74 ++- Prometheus/ExemplarLabelSet.cs | 56 +++ Prometheus/ExemplarProvider.cs | 2 +- Prometheus/Histogram.cs | 466 +++++++++--------- Prometheus/ICounter.cs | 44 +- Prometheus/IHistogram.cs | 57 ++- Prometheus/ObservedExemplar.cs | 20 +- Prometheus/TextSerializer.cs | 4 +- Sample.Console.Exemplars/Program.cs | 8 +- Sample.Web/SampleService.cs | 2 +- Tests.NetCore/HistogramTests.cs | 6 +- Tests.NetCore/TextSerializerTests.cs | 12 +- 23 files changed, 707 insertions(+), 545 deletions(-) create mode 100644 Prometheus.AspNetCore/HttpMetrics/HttpRequestExemplarPredicate.cs create mode 100644 Prometheus/ExemplarLabelSet.cs diff --git a/Benchmark.NetCore/MeasurementBenchmarks.cs b/Benchmark.NetCore/MeasurementBenchmarks.cs index 78c39ffb..ea02f104 100644 --- a/Benchmark.NetCore/MeasurementBenchmarks.cs +++ b/Benchmark.NetCore/MeasurementBenchmarks.cs @@ -43,7 +43,16 @@ public enum MetricType private readonly Summary.Child _summary; private readonly Histogram.Child _histogram; - private Exemplar.LabelPair[] _exemplar = Array.Empty(); + private readonly Exemplar.LabelKey _traceIdKey = Exemplar.Key("trace_id"); + private readonly Exemplar.LabelKey _spanIdKey = Exemplar.Key("span_id"); + + // We preallocate the exemplar values to avoid measuring the random()->string serialization as part of the benchmark. + // What we care about measuring is the overhead of processing the exemplar, not of generating/serializing it. + private readonly string _traceIdValue = "7f825eb926a90af6961ace5f9a239945"; + private readonly string _spanIdValue = "a77603af408a13ec"; + + private Exemplar.LabelPair _traceIdLabel; + private Exemplar.LabelPair _spanIdLabel; public MeasurementBenchmarks() { @@ -84,12 +93,11 @@ public MeasurementBenchmarks() [GlobalSetup] public void GlobalSetup() { - if (WithExemplars) - { - // You often do need to allocate new exemplar key-value pairs to measure data from new contexts but this benchmark - // exists to indicate the pure measurement performance independent of this, so we reuse an exemplar. - _exemplar = new[] { Exemplar.Key("traceID").WithValue("bar"), Exemplar.Key("traceID2").WithValue("foo") }; - } + // There is an unavoidable string->bytes encoding overhead from this. + // As it is fixed overhead based on user data size, we pre-encode the strings here to avoid them influencing the benchmark results. + // We only preallocate the strings, however (creating the LabelPairs). We still do as much of the exemplar "processing" inline as feasible, to be realistic. + _traceIdLabel = _traceIdKey.WithValue(_traceIdValue); + _spanIdLabel = _spanIdKey.WithValue(_spanIdValue); } [IterationSetup] @@ -145,7 +153,8 @@ private void MeasurementThreadCounter(object state) for (var i = 0; i < MeasurementCount; i++) { - _counter.Inc(_exemplar); + var exemplar = WithExemplars ? Exemplar.From(_traceIdLabel, _spanIdLabel) : Exemplar.None; + _counter.Inc(exemplar); } } @@ -171,7 +180,8 @@ private void MeasurementThreadHistogram(object state) for (var i = 0; i < MeasurementCount; i++) { - _histogram.Observe(i, _exemplar); + var exemplar = WithExemplars ? Exemplar.From(_traceIdLabel, _spanIdLabel) : Exemplar.None; + _histogram.Observe(i, exemplar); } } diff --git a/Benchmark.NetCore/SerializationBenchmarks.cs b/Benchmark.NetCore/SerializationBenchmarks.cs index a653d55f..3a141f5a 100644 --- a/Benchmark.NetCore/SerializationBenchmarks.cs +++ b/Benchmark.NetCore/SerializationBenchmarks.cs @@ -63,7 +63,7 @@ public SerializationBenchmarks() [GlobalSetup] public void GenerateData() { - var exemplar = Exemplar.Key("traceID").WithValue("bar"); + var exemplar = Exemplar.From(Exemplar.Key("traceID").WithValue("bar")); for (var metricIndex = 0; metricIndex < _metricCount; metricIndex++) for (var variantIndex = 0; variantIndex < _variantCount; variantIndex++) { diff --git a/Prometheus.AspNetCore/HttpMetrics/HttpInProgressMiddleware.cs b/Prometheus.AspNetCore/HttpMetrics/HttpInProgressMiddleware.cs index 5b97f4b0..bff49072 100644 --- a/Prometheus.AspNetCore/HttpMetrics/HttpInProgressMiddleware.cs +++ b/Prometheus.AspNetCore/HttpMetrics/HttpInProgressMiddleware.cs @@ -1,33 +1,30 @@ using Microsoft.AspNetCore.Http; -namespace Prometheus.HttpMetrics +namespace Prometheus.HttpMetrics; + +internal sealed class HttpInProgressMiddleware : HttpRequestMiddlewareBase, IGauge> { - internal sealed class HttpInProgressMiddleware : HttpRequestMiddlewareBase, IGauge> - { - private readonly RequestDelegate _next; + private readonly RequestDelegate _next; - public HttpInProgressMiddleware(RequestDelegate next, HttpInProgressOptions options) - : base(options, options?.Gauge) - { - _next = next ?? throw new ArgumentNullException(nameof(next)); - } + public HttpInProgressMiddleware(RequestDelegate next, HttpInProgressOptions options) + : base(options, options?.Gauge) + { + _next = next ?? throw new ArgumentNullException(nameof(next)); + } - public async Task Invoke(HttpContext context) + public async Task Invoke(HttpContext context) + { + // CreateChild() will take care of applying the right labels, no need to think hard about it here. + using (CreateChild(context).TrackInProgress()) { - // In ASP.NET Core 2, we will not have route data, so we cannot record controller/action labels. - // In ASP.NET Core 3, we will have this data and can record the labels. - // CreateChild() will take care of applying the right labels, no need to think hard about it here. - using (CreateChild(context).TrackInProgress()) - { - await _next(context); - } + await _next(context); } + } - protected override string[] BaselineLabels => HttpRequestLabelNames.DefaultsAvailableBeforeExecutingFinalHandler; + protected override string[] BaselineLabels => HttpRequestLabelNames.DefaultsAvailableBeforeExecutingFinalHandler; - protected override ICollector CreateMetricInstance(string[] labelNames) => MetricFactory.CreateGauge( - "http_requests_in_progress", - "The number of requests currently in progress in the ASP.NET Core pipeline. One series without controller/action label values counts all in-progress requests, with separate series existing for each controller-action pair.", - labelNames); - } + protected override ICollector CreateMetricInstance(string[] labelNames) => MetricFactory.CreateGauge( + "http_requests_in_progress", + "The number of requests currently in progress in the ASP.NET Core pipeline. One series without controller/action label values counts all in-progress requests, with separate series existing for each controller-action pair.", + labelNames); } \ No newline at end of file diff --git a/Prometheus.AspNetCore/HttpMetrics/HttpMetricsOptionsBase.cs b/Prometheus.AspNetCore/HttpMetrics/HttpMetricsOptionsBase.cs index bd9fb584..ab4ca5d7 100644 --- a/Prometheus.AspNetCore/HttpMetrics/HttpMetricsOptionsBase.cs +++ b/Prometheus.AspNetCore/HttpMetrics/HttpMetricsOptionsBase.cs @@ -1,58 +1,66 @@ -namespace Prometheus.HttpMetrics +namespace Prometheus.HttpMetrics; + +public abstract class HttpMetricsOptionsBase { - public abstract class HttpMetricsOptionsBase - { - public bool Enabled { get; set; } = true; - - /// - /// Transforms the label value from it's raw value (e.g. 200, 404) into a compressed - /// alternative (e.g. 2xx, 4xx). Setting this to true can be used to reduce the cardinality of metrics produced while still clearly communicating - /// success and error conditions (client vs server error). Defaults to false. - /// - public bool ReduceStatusCodeCardinality { get; set; } = false; - - /// - /// Additional route parameters to include beyond the defaults (controller/action). - /// This may be useful if you have, for example, a "version" parameter for API versioning. - /// - /// - /// Metric labels are automatically defined for these parameters, unless you provide your - /// own metric instance in the options (in which case you must add the required labels). - /// - public List AdditionalRouteParameters { get; set; } = new List(); - - /// - /// Additional custom labels to add to the metrics, with values extracted from the HttpContext of incoming requests. - /// - /// - /// Metric labels are automatically defined for these, unless you provide your - /// own metric instance in the options (in which case you must add the required labels). - /// - public List CustomLabels { get; set; } = new List(); - - /// - /// Allows you to override the registry used to create the default metric instance. - /// - /// - /// Value is ignored if you specify a custom metric instance or metric factory in the options (instance overrides factory overrides registry). - /// - public CollectorRegistry? Registry { get; set; } - - /// - /// Allows you to override the metric factory used to create the default metric instance. - /// - /// - /// Value is ignored if you specify a custom metric instance in the options (instance overrides factory overrides registry). - /// - public IMetricFactory? MetricFactory { get; set; } - - /// - /// If set, the "page" label will be considered one of the built-in default labels. - /// This is only enabled if Razor Pages is detected at the middleware setup stage. - /// - /// The value is ignored if a custom metric is provided (though the user may still add - /// the "page" label themselves via AdditionalRouteParameters and it will work). - /// - internal bool IncludePageLabelInDefaultsInternal { get; set; } - } + public bool Enabled { get; set; } = true; + + /// + /// Transforms the label value from it's raw value (e.g. 200, 404) into a compressed + /// alternative (e.g. 2xx, 4xx). Setting this to true can be used to reduce the cardinality of metrics produced while still clearly communicating + /// success and error conditions (client vs server error). Defaults to false. + /// + public bool ReduceStatusCodeCardinality { get; set; } = false; + + /// + /// Additional route parameters to include beyond the defaults (controller/action). + /// This may be useful if you have, for example, a "version" parameter for API versioning. + /// + /// + /// Metric labels are automatically defined for these parameters, unless you provide your + /// own metric instance in the options (in which case you must add the required labels). + /// + public List AdditionalRouteParameters { get; set; } = new List(); + + /// + /// Additional custom labels to add to the metrics, with values extracted from the HttpContext of incoming requests. + /// + /// + /// Metric labels are automatically defined for these, unless you provide your + /// own metric instance in the options (in which case you must add the required labels). + /// + public List CustomLabels { get; set; } = new List(); + + /// + /// Allows you to override the registry used to create the default metric instance. + /// + /// + /// Value is ignored if you specify a custom metric instance or metric factory in the options (instance overrides factory overrides registry). + /// + public CollectorRegistry? Registry { get; set; } + + /// + /// Allows you to override the metric factory used to create the default metric instance. + /// + /// + /// Value is ignored if you specify a custom metric instance in the options (instance overrides factory overrides registry). + /// + public IMetricFactory? MetricFactory { get; set; } + + /// + /// Enables custom logic to determine whether an exemplar should be recorded for a specific HTTP request. + /// This will be called after request processing has completed and the response has been filled by inner handlers. + /// + /// + /// By default, we always record an exemplar (if an exemplar is available for the given request/response pair). + /// + public HttpRequestExemplarPredicate ExemplarPredicate { get; set; } = _ => true; + + /// + /// If set, the "page" label will be considered one of the built-in default labels. + /// This is only enabled if Razor Pages is detected at the middleware setup stage. + /// + /// The value is ignored if a custom metric is provided (though the user may still add + /// the "page" label themselves via AdditionalRouteParameters and it will work). + /// + internal bool IncludePageLabelInDefaultsInternal { get; set; } } \ No newline at end of file diff --git a/Prometheus.AspNetCore/HttpMetrics/HttpRequestCountMiddleware.cs b/Prometheus.AspNetCore/HttpMetrics/HttpRequestCountMiddleware.cs index 3ad34cc0..7674ad79 100644 --- a/Prometheus.AspNetCore/HttpMetrics/HttpRequestCountMiddleware.cs +++ b/Prometheus.AspNetCore/HttpMetrics/HttpRequestCountMiddleware.cs @@ -1,37 +1,38 @@ using Microsoft.AspNetCore.Http; -namespace Prometheus.HttpMetrics +namespace Prometheus.HttpMetrics; + +internal sealed class HttpRequestCountMiddleware : HttpRequestMiddlewareBase, ICounter> { - internal sealed class HttpRequestCountMiddleware : HttpRequestMiddlewareBase, ICounter> + private readonly RequestDelegate _next; + private readonly HttpRequestCountOptions _options; + + public HttpRequestCountMiddleware(RequestDelegate next, HttpRequestCountOptions options) + : base(options, options?.Counter) { - private readonly RequestDelegate _next; + _next = next ?? throw new ArgumentNullException(nameof(next)); + _options = options ?? throw new ArgumentNullException(nameof(options)); + } - public HttpRequestCountMiddleware(RequestDelegate next, HttpRequestCountOptions options) - : base(options, options?.Counter) + public async Task Invoke(HttpContext context) + { + try { - _next = next ?? throw new ArgumentNullException(nameof(next)); + await _next(context); } - - public async Task Invoke(HttpContext context) + finally { - try - { - await _next(context); - } - finally - { - // We need to record metrics after inner handler execution because routing data in - // ASP.NET Core 2 is only available *after* executing the next request delegate. - // So we would not have the right labels if we tried to create the child early on. - CreateChild(context).Inc(); - } + // We pass either null (== use default exemplar provider) or None (== do not record exemplar). + ExemplarLabelSet? exemplar = _options.ExemplarPredicate(context) ? null : Exemplar.None; + + CreateChild(context).Inc(exemplar); } + } - protected override string[] BaselineLabels => HttpRequestLabelNames.Default; + protected override string[] BaselineLabels => HttpRequestLabelNames.Default; - protected override ICollector CreateMetricInstance(string[] labelNames) => MetricFactory.CreateCounter( - "http_requests_received_total", - "Provides the count of HTTP requests that have been processed by the ASP.NET Core pipeline.", - labelNames); - } + protected override ICollector CreateMetricInstance(string[] labelNames) => MetricFactory.CreateCounter( + "http_requests_received_total", + "Provides the count of HTTP requests that have been processed by the ASP.NET Core pipeline.", + labelNames); } \ No newline at end of file diff --git a/Prometheus.AspNetCore/HttpMetrics/HttpRequestDurationMiddleware.cs b/Prometheus.AspNetCore/HttpMetrics/HttpRequestDurationMiddleware.cs index e62c8d82..e36f1d72 100644 --- a/Prometheus.AspNetCore/HttpMetrics/HttpRequestDurationMiddleware.cs +++ b/Prometheus.AspNetCore/HttpMetrics/HttpRequestDurationMiddleware.cs @@ -1,44 +1,45 @@ using Microsoft.AspNetCore.Http; -namespace Prometheus.HttpMetrics +namespace Prometheus.HttpMetrics; + +internal sealed class HttpRequestDurationMiddleware : HttpRequestMiddlewareBase, IHistogram> { - internal sealed class HttpRequestDurationMiddleware : HttpRequestMiddlewareBase, IHistogram> + private readonly RequestDelegate _next; + private readonly HttpRequestDurationOptions _options; + + public HttpRequestDurationMiddleware(RequestDelegate next, HttpRequestDurationOptions options) + : base(options, options?.Histogram) + { + _next = next ?? throw new ArgumentNullException(nameof(next)); + _options = options ?? throw new ArgumentNullException(nameof(options)); + } + + public async Task Invoke(HttpContext context) { - private readonly RequestDelegate _next; + var stopWatch = ValueStopwatch.StartNew(); - public HttpRequestDurationMiddleware(RequestDelegate next, HttpRequestDurationOptions options) - : base(options, options?.Histogram) + try { - _next = next ?? throw new ArgumentNullException(nameof(next)); + await _next(context); } - - public async Task Invoke(HttpContext context) + finally { - var stopWatch = ValueStopwatch.StartNew(); - - // We need to write this out in long form instead of using a timer because routing data in - // ASP.NET Core 2 is only available *after* executing the next request delegate. - // So we would not have the right labels if we tried to create the child early on. - try - { - await _next(context); - } - finally - { - CreateChild(context).Observe(stopWatch.GetElapsedTime().TotalSeconds); - } + // We pass either null (== use default exemplar provider) or None (== do not record exemplar). + ExemplarLabelSet? exemplar = _options.ExemplarPredicate(context) ? null : Exemplar.None; + + CreateChild(context).Observe(stopWatch.GetElapsedTime().TotalSeconds, exemplar); } + } - protected override string[] BaselineLabels => HttpRequestLabelNames.Default; + protected override string[] BaselineLabels => HttpRequestLabelNames.Default; - protected override ICollector CreateMetricInstance(string[] labelNames) => MetricFactory.CreateHistogram( - "http_request_duration_seconds", - "The duration of HTTP requests processed by an ASP.NET Core application.", - labelNames, - new HistogramConfiguration - { - // 1 ms to 32K ms buckets - Buckets = Histogram.ExponentialBuckets(0.001, 2, 16), - }); - } + protected override ICollector CreateMetricInstance(string[] labelNames) => MetricFactory.CreateHistogram( + "http_request_duration_seconds", + "The duration of HTTP requests processed by an ASP.NET Core application.", + labelNames, + new HistogramConfiguration + { + // 1 ms to 32K ms buckets + Buckets = Histogram.ExponentialBuckets(0.001, 2, 16), + }); } \ No newline at end of file diff --git a/Prometheus.AspNetCore/HttpMetrics/HttpRequestExemplarPredicate.cs b/Prometheus.AspNetCore/HttpMetrics/HttpRequestExemplarPredicate.cs new file mode 100644 index 00000000..91fce1f9 --- /dev/null +++ b/Prometheus.AspNetCore/HttpMetrics/HttpRequestExemplarPredicate.cs @@ -0,0 +1,5 @@ +using Microsoft.AspNetCore.Http; + +namespace Prometheus.HttpMetrics; + +public delegate bool HttpRequestExemplarPredicate(HttpContext context); diff --git a/Prometheus/AutoLeasingCounter.cs b/Prometheus/AutoLeasingCounter.cs index 7e8c140d..a7c47d76 100644 --- a/Prometheus/AutoLeasingCounter.cs +++ b/Prometheus/AutoLeasingCounter.cs @@ -1,58 +1,57 @@ -namespace Prometheus +namespace Prometheus; + +/// +/// A counter that automatically extends the lifetime of a lease-extended metric whenever it is used. +/// It only supports write operations because we cannot guarantee that the metric is still alive when reading. +/// +internal sealed class AutoLeasingCounter : ICollector { - /// - /// A counter that automatically extends the lifetime of a lease-extended metric whenever it is used. - /// It only supports write operations because we cannot guarantee that the metric is still alive when reading. - /// - internal sealed class AutoLeasingCounter : ICollector + public AutoLeasingCounter(IManagedLifetimeMetricHandle inner, ICollector root) + { + _inner = inner; + _root = root; + } + + private readonly IManagedLifetimeMetricHandle _inner; + private readonly ICollector _root; + + public string Name => _root.Name; + public string Help => _root.Help; + public string[] LabelNames => _root.LabelNames; + + public ICounter Unlabelled => new Instance(_inner, Array.Empty()); + + public ICounter WithLabels(params string[] labelValues) + { + return new Instance(_inner, labelValues); + } + + private sealed class Instance : ICounter { - public AutoLeasingCounter(IManagedLifetimeMetricHandle inner, ICollector root) + public Instance(IManagedLifetimeMetricHandle inner, string[] labelValues) { _inner = inner; - _root = root; + _labelValues = labelValues; } private readonly IManagedLifetimeMetricHandle _inner; - private readonly ICollector _root; + private readonly string[] _labelValues; - public string Name => _root.Name; - public string Help => _root.Help; - public string[] LabelNames => _root.LabelNames; + public double Value => throw new NotSupportedException("Read operations on a lifetime-extending-on-use expiring metric are not supported."); - public ICounter Unlabelled => new Instance(_inner, Array.Empty()); + public void Inc(ExemplarLabelSet? exemplar) + { + Inc(increment:1, exemplar: exemplar); + } - public ICounter WithLabels(params string[] labelValues) + public void Inc(double increment, ExemplarLabelSet? exemplar) { - return new Instance(_inner, labelValues); + _inner.WithLease(x => x.Inc(increment, exemplar), _labelValues); } - private sealed class Instance : ICounter + public void IncTo(double targetValue) { - public Instance(IManagedLifetimeMetricHandle inner, string[] labelValues) - { - _inner = inner; - _labelValues = labelValues; - } - - private readonly IManagedLifetimeMetricHandle _inner; - private readonly string[] _labelValues; - - public double Value => throw new NotSupportedException("Read operations on a lifetime-extending-on-use expiring metric are not supported."); - - public void Inc(params Exemplar.LabelPair[] exemplar) - { - Inc(increment:1, exemplar: exemplar); - } - - public void Inc(double increment = 1, params Exemplar.LabelPair[] exemplar) - { - _inner.WithLease(x => x.Inc(increment, exemplar), _labelValues); - } - - public void IncTo(double targetValue) - { - _inner.WithLease(x => x.IncTo(targetValue), _labelValues); - } + _inner.WithLease(x => x.IncTo(targetValue), _labelValues); } } } diff --git a/Prometheus/AutoLeasingHistogram.cs b/Prometheus/AutoLeasingHistogram.cs index 23787231..0a104b70 100644 --- a/Prometheus/AutoLeasingHistogram.cs +++ b/Prometheus/AutoLeasingHistogram.cs @@ -1,59 +1,58 @@ -namespace Prometheus +namespace Prometheus; + +/// +/// A histogram that automatically extends the lifetime of a lease-extended metric whenever it is used. +/// It only supports write operations because we cannot guarantee that the metric is still alive when reading. +/// +internal sealed class AutoLeasingHistogram : ICollector { - /// - /// A histogram that automatically extends the lifetime of a lease-extended metric whenever it is used. - /// It only supports write operations because we cannot guarantee that the metric is still alive when reading. - /// - internal sealed class AutoLeasingHistogram : ICollector + public AutoLeasingHistogram(IManagedLifetimeMetricHandle inner, ICollector root) + { + _inner = inner; + _root = root; + } + + private readonly IManagedLifetimeMetricHandle _inner; + private readonly ICollector _root; + + public string Name => _root.Name; + public string Help => _root.Help; + public string[] LabelNames => _root.LabelNames; + + public IHistogram Unlabelled => new Instance(_inner, Array.Empty()); + + public IHistogram WithLabels(params string[] labelValues) + { + return new Instance(_inner, labelValues); + } + + private sealed class Instance : IHistogram { - public AutoLeasingHistogram(IManagedLifetimeMetricHandle inner, ICollector root) + public Instance(IManagedLifetimeMetricHandle inner, string[] labelValues) { _inner = inner; - _root = root; + _labelValues = labelValues; } private readonly IManagedLifetimeMetricHandle _inner; - private readonly ICollector _root; + private readonly string[] _labelValues; - public string Name => _root.Name; - public string Help => _root.Help; - public string[] LabelNames => _root.LabelNames; + public double Sum => throw new NotSupportedException("Read operations on a lifetime-extending-on-use expiring metric are not supported."); + public long Count => throw new NotSupportedException("Read operations on a lifetime-extending-on-use expiring metric are not supported."); - public IHistogram Unlabelled => new Instance(_inner, Array.Empty()); + public void Observe(double val, long count) + { + _inner.WithLease(x => x.Observe(val, count), _labelValues); + } - public IHistogram WithLabels(params string[] labelValues) + public void Observe(double val, ExemplarLabelSet? exemplar) { - return new Instance(_inner, labelValues); + _inner.WithLease(x => x.Observe(val, exemplar), _labelValues); } - private sealed class Instance : IHistogram + public void Observe(double val) { - public Instance(IManagedLifetimeMetricHandle inner, string[] labelValues) - { - _inner = inner; - _labelValues = labelValues; - } - - private readonly IManagedLifetimeMetricHandle _inner; - private readonly string[] _labelValues; - - public double Sum => throw new NotSupportedException("Read operations on a lifetime-extending-on-use expiring metric are not supported."); - public long Count => throw new NotSupportedException("Read operations on a lifetime-extending-on-use expiring metric are not supported."); - - public void Observe(double val, long count) - { - _inner.WithLease(x => x.Observe(val, count), _labelValues); - } - - public void Observe(double val, params Exemplar.LabelPair[] exemplar) - { - _inner.WithLease(x => x.Observe(val, exemplar), _labelValues); - } - - public void Observe(double val) - { - Observe(val, Array.Empty()); - } + Observe(val, Exemplar.None); } } } diff --git a/Prometheus/ChildBase.cs b/Prometheus/ChildBase.cs index 7d54e3c8..e15257f2 100644 --- a/Prometheus/ChildBase.cs +++ b/Prometheus/ChildBase.cs @@ -115,11 +115,12 @@ internal void ReturnBorrowedExemplar(ref ObservedExemplar storage, ObservedExemp } } - protected Exemplar.LabelPair[] ExemplarOrDefault(Exemplar.LabelPair[] exemplar, double value) + protected ExemplarLabelSet ExemplarOrDefault(ExemplarLabelSet? exemplar, double value) { - // If a custom exemplar was provided for the observation, just use it. - if (exemplar is { Length: > 0 }) - return exemplar; + // If any non-null value is provided, we use it as exemplar. + // Only a null value causes us to ask the default exemplar provider. + if (exemplar.HasValue) + return exemplar.Value; return _exemplarBehavior.DefaultExemplarProvider?.Invoke(Parent, value) ?? Exemplar.None; } diff --git a/Prometheus/Counter.cs b/Prometheus/Counter.cs index 97249fb8..835a1f8b 100644 --- a/Prometheus/Counter.cs +++ b/Prometheus/Counter.cs @@ -27,17 +27,17 @@ await serializer.WriteMetricPointAsync( ReturnBorrowedExemplar(ref _observedExemplar, exemplar); } - public void Inc(double increment = 1.0) + public void Inc(double increment) { Inc(increment: increment, Exemplar.None); } - public void Inc(params Exemplar.LabelPair[] exemplarLabels) + public void Inc(ExemplarLabelSet? exemplarLabels) { Inc(increment: 1, exemplarLabels: exemplarLabels); } - public void Inc(double increment = 1.0, params Exemplar.LabelPair[] exemplarLabels) + public void Inc(double increment = 1.0, ExemplarLabelSet? exemplarLabels = null) { if (increment < 0.0) throw new ArgumentOutOfRangeException(nameof(increment), "Counter value cannot decrease."); @@ -46,7 +46,7 @@ public void Inc(double increment = 1.0, params Exemplar.LabelPair[] exemplarLabe if (exemplarLabels is { Length: > 0 }) { - var exemplar = ObservedExemplar.CreatePooled(exemplarLabels, increment); + var exemplar = ObservedExemplar.CreatePooled(exemplarLabels.Value, increment); ObservedExemplar.ReturnPooledIfNotEmpty(Interlocked.Exchange(ref _observedExemplar, exemplar)); } @@ -74,19 +74,19 @@ internal Counter(string name, string help, StringSequence instanceLabelNames, La { } - public void Inc(double increment = 1) => Unlabelled.Inc(increment); + public void Inc(double increment) => Unlabelled.Inc(increment); public void IncTo(double targetValue) => Unlabelled.IncTo(targetValue); public double Value => Unlabelled.Value; public void Publish() => Unlabelled.Publish(); public void Unpublish() => Unlabelled.Unpublish(); - public void Inc(params Exemplar.LabelPair[] exemplar) + public void Inc(ExemplarLabelSet? exemplar) { Inc(increment: 1, exemplar: exemplar); } - public void Inc(double increment = 1, params Exemplar.LabelPair[] exemplar) => + public void Inc(double increment = 1, ExemplarLabelSet? exemplar = null) => Unlabelled.Inc(increment, exemplar); internal override MetricType Type => MetricType.Counter; diff --git a/Prometheus/Exemplar.cs b/Prometheus/Exemplar.cs index 719a74a9..6eccf383 100644 --- a/Prometheus/Exemplar.cs +++ b/Prometheus/Exemplar.cs @@ -7,7 +7,10 @@ namespace Prometheus; public static class Exemplar { - public static readonly LabelPair[] None = Array.Empty(); + /// + /// An exemplar value that indicates no exemplar is to be recorded. + /// + public static readonly ExemplarLabelSet None = ExemplarLabelSet.Empty; /// /// An exemplar label key. @@ -77,13 +80,76 @@ public static LabelPair Pair(string key, string value) return Key(key).WithValue(value); } + public static ExemplarLabelSet From(LabelPair labelPair1, LabelPair labelPair2, LabelPair labelPair3, LabelPair labelPair4, LabelPair labelPair5, LabelPair labelPair6) + { + var exemplar = ExemplarLabelSet.AllocateFromPool(length: 6); + exemplar.Buffer[0] = labelPair1; + exemplar.Buffer[1] = labelPair2; + exemplar.Buffer[2] = labelPair3; + exemplar.Buffer[3] = labelPair4; + exemplar.Buffer[4] = labelPair5; + exemplar.Buffer[5] = labelPair6; + + return exemplar; + } + + public static ExemplarLabelSet From(LabelPair labelPair1, LabelPair labelPair2, LabelPair labelPair3, LabelPair labelPair4, LabelPair labelPair5) + { + var exemplar = ExemplarLabelSet.AllocateFromPool(length: 5); + exemplar.Buffer[0] = labelPair1; + exemplar.Buffer[1] = labelPair2; + exemplar.Buffer[2] = labelPair3; + exemplar.Buffer[3] = labelPair4; + exemplar.Buffer[4] = labelPair5; + + return exemplar; + } + + public static ExemplarLabelSet From(LabelPair labelPair1, LabelPair labelPair2, LabelPair labelPair3, LabelPair labelPair4) + { + var exemplar = ExemplarLabelSet.AllocateFromPool(length: 4); + exemplar.Buffer[0] = labelPair1; + exemplar.Buffer[1] = labelPair2; + exemplar.Buffer[2] = labelPair3; + exemplar.Buffer[3] = labelPair4; + + return exemplar; + } + + public static ExemplarLabelSet From(LabelPair labelPair1, LabelPair labelPair2, LabelPair labelPair3) + { + var exemplar = ExemplarLabelSet.AllocateFromPool(length: 3); + exemplar.Buffer[0] = labelPair1; + exemplar.Buffer[1] = labelPair2; + exemplar.Buffer[2] = labelPair3; + + return exemplar; + } + + public static ExemplarLabelSet From(LabelPair labelPair1, LabelPair labelPair2) + { + var exemplar = ExemplarLabelSet.AllocateFromPool(length: 2); + exemplar.Buffer[0] = labelPair1; + exemplar.Buffer[1] = labelPair2; + + return exemplar; + } + + public static ExemplarLabelSet From(LabelPair labelPair1) + { + var exemplar = ExemplarLabelSet.AllocateFromPool(length: 1); + exemplar.Buffer[0] = labelPair1; + + return exemplar; + } + // Based on https://opentelemetry.io/docs/reference/specification/compatibility/prometheus_and_openmetrics/ private static readonly LabelKey DefaultTraceIdKey = Key("trace_id"); private static readonly LabelKey DefaultSpanIdKey = Key("span_id"); - public static LabelPair[] FromTraceContext() => FromTraceContext(DefaultTraceIdKey, DefaultSpanIdKey); + public static ExemplarLabelSet FromTraceContext() => FromTraceContext(DefaultTraceIdKey, DefaultSpanIdKey); - public static LabelPair[] FromTraceContext(LabelKey traceIdKey, LabelKey spanIdKey) + public static ExemplarLabelSet FromTraceContext(LabelKey traceIdKey, LabelKey spanIdKey) { #if NET6_0_OR_GREATER var activity = Activity.Current; @@ -92,7 +158,7 @@ public static LabelPair[] FromTraceContext(LabelKey traceIdKey, LabelKey spanIdK var traceIdLabel = traceIdKey.WithValue(activity.TraceId.ToString()); var spanIdLabel = spanIdKey.WithValue(activity.SpanId.ToString()); - return new[] { traceIdLabel, spanIdLabel }; + return From(traceIdLabel, spanIdLabel); } #endif diff --git a/Prometheus/ExemplarLabelSet.cs b/Prometheus/ExemplarLabelSet.cs new file mode 100644 index 00000000..91ef3d8d --- /dev/null +++ b/Prometheus/ExemplarLabelSet.cs @@ -0,0 +1,56 @@ +using System.Buffers; + +namespace Prometheus; + +/// +/// A fully-formed exemplar, defined from a set of label name-value pairs. Create via Exemplar.From(). +/// +/// One-time use only - when you pass this value to a prometheus-net method, it will consume and destroy the value. +/// +/// +/// The purpose of this is to ensure that any labelpair arrays are allocated from a pool and reused. +/// To facilitate this, we wrap all the arrays in this class, instead of letting the caller provide their own arrays. +/// +public struct ExemplarLabelSet +{ + // We do not return this value to the pool, it is eternal. + internal static readonly ExemplarLabelSet Empty = new ExemplarLabelSet(Array.Empty(), 0); + + internal ExemplarLabelSet(Exemplar.LabelPair[] buffer, int length) + { + Buffer = buffer; + Length = length; + } + + /// + /// The buffer containing the label pairs. Might not be fully filled! + /// + internal Exemplar.LabelPair[] Buffer { get; private set; } + + /// + /// Number of label pairs from the buffer to use. + /// + internal int Length { get; private set; } + + internal static ExemplarLabelSet AllocateFromPool(int length) + { + if (length < 1) + throw new ArgumentOutOfRangeException(nameof(length), $"{nameof(ExemplarLabelSet)} data length must be at least 1."); + + var buffer = ArrayPool.Shared.Rent(length); + + return new ExemplarLabelSet(buffer, length); + } + + internal void ReturnToPoolIfNotEmpty() + { + if (Length == 0) + return; + + ArrayPool.Shared.Return(Buffer); + + // Just for safety, in case it gets accidentally reused. + Buffer = Array.Empty(); + Length = 0; + } +} diff --git a/Prometheus/ExemplarProvider.cs b/Prometheus/ExemplarProvider.cs index 38b448fa..9e9be87e 100644 --- a/Prometheus/ExemplarProvider.cs +++ b/Prometheus/ExemplarProvider.cs @@ -5,4 +5,4 @@ /// /// The metric instance for which an exemplar is being provided. /// Context-dependent - for counters, the increment; for histograms, the observed value. -public delegate Exemplar.LabelPair[] ExemplarProvider(Collector metric, double value); +public delegate ExemplarLabelSet ExemplarProvider(Collector metric, double value); diff --git a/Prometheus/Histogram.cs b/Prometheus/Histogram.cs index 4cb81735..52bfbbc0 100644 --- a/Prometheus/Histogram.cs +++ b/Prometheus/Histogram.cs @@ -1,293 +1,293 @@ -using System.Globalization; +namespace Prometheus; -namespace Prometheus +/// +/// The histogram is thread-safe but not atomic - the sum of values and total count of events +/// may not add up perfectly with bucket contents if new observations are made during a collection. +/// +public sealed class Histogram : Collector, IHistogram { - /// - /// The histogram is thread-safe but not atomic - the sum of values and total count of events - /// may not add up perfectly with bucket contents if new observations are made during a collection. - /// - public sealed class Histogram : Collector, IHistogram - { - private static readonly double[] DefaultBuckets = { .005, .01, .025, .05, .075, .1, .25, .5, .75, 1, 2.5, 5, 7.5, 10 }; - private readonly double[] _buckets; + private static readonly double[] DefaultBuckets = { .005, .01, .025, .05, .075, .1, .25, .5, .75, 1, 2.5, 5, 7.5, 10 }; + private readonly double[] _buckets; - internal Histogram(string name, string help, StringSequence instanceLabelNames, LabelSequence staticLabels, bool suppressInitialValue, double[]? buckets, ExemplarBehavior exemplarBehavior) - : base(name, help, instanceLabelNames, staticLabels, suppressInitialValue, exemplarBehavior) + internal Histogram(string name, string help, StringSequence instanceLabelNames, LabelSequence staticLabels, bool suppressInitialValue, double[]? buckets, ExemplarBehavior exemplarBehavior) + : base(name, help, instanceLabelNames, staticLabels, suppressInitialValue, exemplarBehavior) + { + if (instanceLabelNames.Contains("le")) { - if (instanceLabelNames.Contains("le")) - { - throw new ArgumentException("'le' is a reserved label name"); - } - _buckets = buckets ?? DefaultBuckets; + throw new ArgumentException("'le' is a reserved label name"); + } + _buckets = buckets ?? DefaultBuckets; - if (_buckets.Length == 0) - { - throw new ArgumentException("Histogram must have at least one bucket"); - } + if (_buckets.Length == 0) + { + throw new ArgumentException("Histogram must have at least one bucket"); + } - if (!double.IsPositiveInfinity(_buckets[_buckets.Length - 1])) - { - _buckets = _buckets.Concat(new[] { double.PositiveInfinity }).ToArray(); - } + if (!double.IsPositiveInfinity(_buckets[_buckets.Length - 1])) + { + _buckets = _buckets.Concat(new[] { double.PositiveInfinity }).ToArray(); + } - for (int i = 1; i < _buckets.Length; i++) + for (int i = 1; i < _buckets.Length; i++) + { + if (_buckets[i] <= _buckets[i - 1]) { - if (_buckets[i] <= _buckets[i - 1]) - { - throw new ArgumentException("Bucket values must be increasing"); - } + throw new ArgumentException("Bucket values must be increasing"); } } + } - private protected override Child NewChild(LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish, ExemplarBehavior exemplarBehavior) - { - return new Child(this, instanceLabels, flattenedLabels, publish, exemplarBehavior); - } + private protected override Child NewChild(LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish, ExemplarBehavior exemplarBehavior) + { + return new Child(this, instanceLabels, flattenedLabels, publish, exemplarBehavior); + } - public sealed class Child : ChildBase, IHistogram + public sealed class Child : ChildBase, IHistogram + { + internal Child(Histogram parent, LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish, ExemplarBehavior exemplarBehavior) + : base(parent, instanceLabels, flattenedLabels, publish, exemplarBehavior) { - internal Child(Histogram parent, LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish, ExemplarBehavior exemplarBehavior) - : base(parent, instanceLabels, flattenedLabels, publish, exemplarBehavior) + Parent = parent; + + _upperBounds = Parent._buckets; + _bucketCounts = new ThreadSafeLong[_upperBounds.Length]; + _leLabels = new CanonicalLabel[_upperBounds.Length]; + for (var i = 0; i < Parent._buckets.Length; i++) { - Parent = parent; - - _upperBounds = Parent._buckets; - _bucketCounts = new ThreadSafeLong[_upperBounds.Length]; - _leLabels = new CanonicalLabel[_upperBounds.Length]; - for (var i = 0; i < Parent._buckets.Length; i++) - { - _leLabels[i] = TextSerializer.EncodeValueAsCanonicalLabel(LeLabelName, Parent._buckets[i]); - } - _exemplars = new ObservedExemplar[_upperBounds.Length]; - for (var i = 0; i < _upperBounds.Length; i++) - { - _exemplars[i] = ObservedExemplar.Empty; - } + _leLabels[i] = TextSerializer.EncodeValueAsCanonicalLabel(LeLabelName, Parent._buckets[i]); + } + _exemplars = new ObservedExemplar[_upperBounds.Length]; + for (var i = 0; i < _upperBounds.Length; i++) + { + _exemplars[i] = ObservedExemplar.Empty; } + } - internal new readonly Histogram Parent; + internal new readonly Histogram Parent; - private ThreadSafeDouble _sum = new ThreadSafeDouble(0.0D); - private readonly ThreadSafeLong[] _bucketCounts; - private readonly double[] _upperBounds; - private readonly CanonicalLabel[] _leLabels; - private static readonly byte[] SumSuffix = PrometheusConstants.ExportEncoding.GetBytes("sum"); - private static readonly byte[] CountSuffix = PrometheusConstants.ExportEncoding.GetBytes("count"); - private static readonly byte[] BucketSuffix = PrometheusConstants.ExportEncoding.GetBytes("bucket"); - private static readonly byte[] LeLabelName = PrometheusConstants.ExportEncoding.GetBytes("le"); - private readonly ObservedExemplar[] _exemplars; + private ThreadSafeDouble _sum = new ThreadSafeDouble(0.0D); + private readonly ThreadSafeLong[] _bucketCounts; + private readonly double[] _upperBounds; + private readonly CanonicalLabel[] _leLabels; + private static readonly byte[] SumSuffix = PrometheusConstants.ExportEncoding.GetBytes("sum"); + private static readonly byte[] CountSuffix = PrometheusConstants.ExportEncoding.GetBytes("count"); + private static readonly byte[] BucketSuffix = PrometheusConstants.ExportEncoding.GetBytes("bucket"); + private static readonly byte[] LeLabelName = PrometheusConstants.ExportEncoding.GetBytes("le"); + private readonly ObservedExemplar[] _exemplars; - private protected override async Task CollectAndSerializeImplAsync(IMetricsSerializer serializer, - CancellationToken cancel) + private protected override async Task CollectAndSerializeImplAsync(IMetricsSerializer serializer, + CancellationToken cancel) + { + // We output sum. + // We output count. + // We output each bucket in order of increasing upper bound. + await serializer.WriteMetricPointAsync( + Parent.NameBytes, + FlattenedLabelsBytes, + CanonicalLabel.Empty, + cancel, + _sum.Value, + ObservedExemplar.Empty, + suffix: SumSuffix); + await serializer.WriteMetricPointAsync( + Parent.NameBytes, + FlattenedLabelsBytes, + CanonicalLabel.Empty, + cancel, + _bucketCounts.Sum(b => b.Value), + ObservedExemplar.Empty, + suffix: CountSuffix); + + var cumulativeCount = 0L; + + for (var i = 0; i < _bucketCounts.Length; i++) { - // We output sum. - // We output count. - // We output each bucket in order of increasing upper bound. - await serializer.WriteMetricPointAsync( - Parent.NameBytes, - FlattenedLabelsBytes, - CanonicalLabel.Empty, - cancel, - _sum.Value, - ObservedExemplar.Empty, - suffix: SumSuffix); + var exemplar = BorrowExemplar(ref _exemplars[i]); + + cumulativeCount += _bucketCounts[i].Value; await serializer.WriteMetricPointAsync( Parent.NameBytes, FlattenedLabelsBytes, - CanonicalLabel.Empty, + _leLabels[i], cancel, - _bucketCounts.Sum(b => b.Value), - ObservedExemplar.Empty, - suffix: CountSuffix); + cumulativeCount, + exemplar, + suffix: BucketSuffix); - var cumulativeCount = 0L; - - for (var i = 0; i < _bucketCounts.Length; i++) - { - var exemplar = BorrowExemplar(ref _exemplars[i]); - - cumulativeCount += _bucketCounts[i].Value; - await serializer.WriteMetricPointAsync( - Parent.NameBytes, - FlattenedLabelsBytes, - _leLabels[i], - cancel, - cumulativeCount, - exemplar, - suffix: BucketSuffix); - - ReturnBorrowedExemplar(ref _exemplars[i], exemplar); - } + ReturnBorrowedExemplar(ref _exemplars[i], exemplar); } + } - public double Sum => _sum.Value; - public long Count => _bucketCounts.Sum(b => b.Value); + public double Sum => _sum.Value; + public long Count => _bucketCounts.Sum(b => b.Value); - public void Observe(double val, params Exemplar.LabelPair[] exemplarLabels) => ObserveInternal(val, 1, exemplarLabels); + public void Observe(double val, ExemplarLabelSet? exemplarLabels) => ObserveInternal(val, 1, exemplarLabels); - public void Observe(double val) => Observe(val, 1); + public void Observe(double val) => Observe(val, 1); - public void Observe(double val, long count) => ObserveInternal(val, count); + public void Observe(double val, long count) => ObserveInternal(val, count, null); - private void ObserveInternal(double val, long count, params Exemplar.LabelPair[] exemplarLabels) + private void ObserveInternal(double val, long count, ExemplarLabelSet? exemplarLabels) + { + if (double.IsNaN(val)) { - if (double.IsNaN(val)) - { - return; - } + return; + } - exemplarLabels = ExemplarOrDefault(exemplarLabels, val); + exemplarLabels = ExemplarOrDefault(exemplarLabels, val); - for (int i = 0; i < _upperBounds.Length; i++) + for (int i = 0; i < _upperBounds.Length; i++) + { + if (val <= _upperBounds[i]) { - if (val <= _upperBounds[i]) - { - _bucketCounts[i].Add(count); + _bucketCounts[i].Add(count); - if (exemplarLabels is { Length: > 0 }) - { - var exemplar = ObservedExemplar.CreatePooled(exemplarLabels, val); - ObservedExemplar.ReturnPooledIfNotEmpty(Interlocked.Exchange(ref _exemplars[i], exemplar)); - } - - break; + if (exemplarLabels is { Length: > 0 }) + { + // CreatePooled() takes ownership of the exemplarLabels and will return them to pool when the time is right. + var exemplar = ObservedExemplar.CreatePooled(exemplarLabels.Value, val); + ObservedExemplar.ReturnPooledIfNotEmpty(Interlocked.Exchange(ref _exemplars[i], exemplar)); } + + break; } - _sum.Add(val * count); - Publish(); } - } - internal override MetricType Type => MetricType.Histogram; - - public double Sum => Unlabelled.Sum; - public long Count => Unlabelled.Count; - public void Observe(double val) => Unlabelled.Observe(val, 1); - public void Observe(double val, long count) => Unlabelled.Observe(val, count); - public void Observe(double val, params Exemplar.LabelPair[] exemplar) => Unlabelled.Observe(val, exemplar); - public void Publish() => Unlabelled.Publish(); - public void Unpublish() => Unlabelled.Unpublish(); - - // From https://github.com/prometheus/client_golang/blob/master/prometheus/histogram.go - /// - /// Creates '' buckets, where the lowest bucket has an - /// upper bound of '' and each following bucket's upper bound is '' - /// times the previous bucket's upper bound. - /// - /// The function throws if '' is 0 or negative, if '' is 0 or negative, - /// or if '' is less than or equal 1. - /// - /// The upper bound of the lowest bucket. Must be positive. - /// The factor to increase the upper bound of subsequent buckets. Must be greater than 1. - /// The number of buckets to create. Must be positive. - public static double[] ExponentialBuckets(double start, double factor, int count) - { - if (count <= 0) throw new ArgumentException($"{nameof(ExponentialBuckets)} needs a positive {nameof(count)}"); - if (start <= 0) throw new ArgumentException($"{nameof(ExponentialBuckets)} needs a positive {nameof(start)}"); - if (factor <= 1) throw new ArgumentException($"{nameof(ExponentialBuckets)} needs a {nameof(factor)} greater than 1"); + _sum.Add(val * count); - // The math we do can make it incur some tiny avoidable error due to floating point gremlins. - // We use decimal for the path to preserve as much accuracy as we can, before finally converting to double. - // It will not fix 100% of the cases where we end up with 0.0000000000000000000000000000001 offset but it helps a lot. + Publish(); + } + } - var next = (decimal)start; - var buckets = new double[count]; + internal override MetricType Type => MetricType.Histogram; + + public double Sum => Unlabelled.Sum; + public long Count => Unlabelled.Count; + public void Observe(double val) => Unlabelled.Observe(val, 1); + public void Observe(double val, long count) => Unlabelled.Observe(val, count); + public void Observe(double val, ExemplarLabelSet? exemplar) => Unlabelled.Observe(val, exemplar); + public void Publish() => Unlabelled.Publish(); + public void Unpublish() => Unlabelled.Unpublish(); + + // From https://github.com/prometheus/client_golang/blob/master/prometheus/histogram.go + /// + /// Creates '' buckets, where the lowest bucket has an + /// upper bound of '' and each following bucket's upper bound is '' + /// times the previous bucket's upper bound. + /// + /// The function throws if '' is 0 or negative, if '' is 0 or negative, + /// or if '' is less than or equal 1. + /// + /// The upper bound of the lowest bucket. Must be positive. + /// The factor to increase the upper bound of subsequent buckets. Must be greater than 1. + /// The number of buckets to create. Must be positive. + public static double[] ExponentialBuckets(double start, double factor, int count) + { + if (count <= 0) throw new ArgumentException($"{nameof(ExponentialBuckets)} needs a positive {nameof(count)}"); + if (start <= 0) throw new ArgumentException($"{nameof(ExponentialBuckets)} needs a positive {nameof(start)}"); + if (factor <= 1) throw new ArgumentException($"{nameof(ExponentialBuckets)} needs a {nameof(factor)} greater than 1"); - for (var i = 0; i < buckets.Length; i++) - { - buckets[i] = (double)next; - next *= (decimal)factor; - } + // The math we do can make it incur some tiny avoidable error due to floating point gremlins. + // We use decimal for the path to preserve as much accuracy as we can, before finally converting to double. + // It will not fix 100% of the cases where we end up with 0.0000000000000000000000000000001 offset but it helps a lot. - return buckets; - } + var next = (decimal)start; + var buckets = new double[count]; - // From https://github.com/prometheus/client_golang/blob/master/prometheus/histogram.go - /// - /// Creates '' buckets, where the lowest bucket has an - /// upper bound of '' and each following bucket's upper bound is the upper bound of the - /// previous bucket, incremented by '' - /// - /// The function throws if '' is 0 or negative. - /// - /// The upper bound of the lowest bucket. - /// The width of each bucket (distance between lower and upper bound). - /// The number of buckets to create. Must be positive. - public static double[] LinearBuckets(double start, double width, int count) + for (var i = 0; i < buckets.Length; i++) { - if (count <= 0) throw new ArgumentException($"{nameof(LinearBuckets)} needs a positive {nameof(count)}"); + buckets[i] = (double)next; + next *= (decimal)factor; + } - // The math we do can make it incur some tiny avoidable error due to floating point gremlins. - // We use decimal for the path to preserve as much accuracy as we can, before finally converting to double. - // It will not fix 100% of the cases where we end up with 0.0000000000000000000000000000001 offset but it helps a lot. + return buckets; + } - var next = (decimal)start; - var buckets = new double[count]; + // From https://github.com/prometheus/client_golang/blob/master/prometheus/histogram.go + /// + /// Creates '' buckets, where the lowest bucket has an + /// upper bound of '' and each following bucket's upper bound is the upper bound of the + /// previous bucket, incremented by '' + /// + /// The function throws if '' is 0 or negative. + /// + /// The upper bound of the lowest bucket. + /// The width of each bucket (distance between lower and upper bound). + /// The number of buckets to create. Must be positive. + public static double[] LinearBuckets(double start, double width, int count) + { + if (count <= 0) throw new ArgumentException($"{nameof(LinearBuckets)} needs a positive {nameof(count)}"); - for (var i = 0; i < buckets.Length; i++) - { - buckets[i] = (double)next; - next += (decimal)width; - } + // The math we do can make it incur some tiny avoidable error due to floating point gremlins. + // We use decimal for the path to preserve as much accuracy as we can, before finally converting to double. + // It will not fix 100% of the cases where we end up with 0.0000000000000000000000000000001 offset but it helps a lot. - return buckets; - } + var next = (decimal)start; + var buckets = new double[count]; - /// - /// Divides each power of 10 into N divisions. - /// - /// The starting range includes 10 raised to this power. - /// The ranges end with 10 raised to this power (this no longer starts a new range). - /// How many divisions to divide each range into. - /// - /// For example, with startPower=-1, endPower=2, divisions=4 we would get: - /// 10^-1 == 0.1 which defines our starting range, giving buckets: 0.25, 0.5, 0.75, 1.0 - /// 10^0 == 1 which is the next range, giving buckets: 2.5, 5, 7.5, 10 - /// 10^1 == 10 which is the next range, giving buckets: 25, 50, 75, 100 - /// 10^2 == 100 which is the end and the top level of the preceding range. - /// Giving total buckets: 0.25, 0.5, 0.75, 1.0, 2.5, 5, 7.5, 10, 25, 50, 75, 100 - /// - public static double[] PowersOfTenDividedBuckets(int startPower, int endPower, int divisions) + for (var i = 0; i < buckets.Length; i++) { - if (startPower >= endPower) - throw new ArgumentException($"{nameof(startPower)} must be less than {nameof(endPower)}.", nameof(startPower)); + buckets[i] = (double)next; + next += (decimal)width; + } - if (divisions <= 0) - throw new ArgumentOutOfRangeException($"{nameof(divisions)} must be a positive integer.", nameof(divisions)); + return buckets; + } - var buckets = new List(); + /// + /// Divides each power of 10 into N divisions. + /// + /// The starting range includes 10 raised to this power. + /// The ranges end with 10 raised to this power (this no longer starts a new range). + /// How many divisions to divide each range into. + /// + /// For example, with startPower=-1, endPower=2, divisions=4 we would get: + /// 10^-1 == 0.1 which defines our starting range, giving buckets: 0.25, 0.5, 0.75, 1.0 + /// 10^0 == 1 which is the next range, giving buckets: 2.5, 5, 7.5, 10 + /// 10^1 == 10 which is the next range, giving buckets: 25, 50, 75, 100 + /// 10^2 == 100 which is the end and the top level of the preceding range. + /// Giving total buckets: 0.25, 0.5, 0.75, 1.0, 2.5, 5, 7.5, 10, 25, 50, 75, 100 + /// + public static double[] PowersOfTenDividedBuckets(int startPower, int endPower, int divisions) + { + if (startPower >= endPower) + throw new ArgumentException($"{nameof(startPower)} must be less than {nameof(endPower)}.", nameof(startPower)); - for (var powerOfTen = startPower; powerOfTen < endPower; powerOfTen++) - { - // This gives us the upper bound (the start of the next range). - var max = (decimal)Math.Pow(10, powerOfTen + 1); + if (divisions <= 0) + throw new ArgumentOutOfRangeException($"{nameof(divisions)} must be a positive integer.", nameof(divisions)); - // Then we just divide it into N divisions and we are done! - for (var division = 0; division < divisions; division++) - { - var bucket = max / divisions * (division + 1); - - // The math we do can make it incur some tiny avoidable error due to floating point gremlins. - // We use decimal for the path to preserve as much accuracy as we can, before finally converting to double. - // It will not fix 100% of the cases where we end up with 0.0000000000000000000000000000001 offset but it helps a lot. - var candidate = (double)bucket; - - // Depending on the number of divisions, it may be that divisions from different powers overlap. - // For example, a division into 20 would include: - // 19th value in the 0th power: 9.5 (10/20*19=9.5) - // 1st value in the 1st power: 5 (100/20*1 = 5) - // To avoid this being a problem, we simply constrain all values to be increasing. - if (buckets.Any() && buckets.Last() >= candidate) - continue; // Skip this one, it is not greater. - - buckets.Add(candidate); - } - } + var buckets = new List(); - return buckets.ToArray(); + for (var powerOfTen = startPower; powerOfTen < endPower; powerOfTen++) + { + // This gives us the upper bound (the start of the next range). + var max = (decimal)Math.Pow(10, powerOfTen + 1); + + // Then we just divide it into N divisions and we are done! + for (var division = 0; division < divisions; division++) + { + var bucket = max / divisions * (division + 1); + + // The math we do can make it incur some tiny avoidable error due to floating point gremlins. + // We use decimal for the path to preserve as much accuracy as we can, before finally converting to double. + // It will not fix 100% of the cases where we end up with 0.0000000000000000000000000000001 offset but it helps a lot. + var candidate = (double)bucket; + + // Depending on the number of divisions, it may be that divisions from different powers overlap. + // For example, a division into 20 would include: + // 19th value in the 0th power: 9.5 (10/20*19=9.5) + // 1st value in the 1st power: 5 (100/20*1 = 5) + // To avoid this being a problem, we simply constrain all values to be increasing. + if (buckets.Any() && buckets.Last() >= candidate) + continue; // Skip this one, it is not greater. + + buckets.Add(candidate); + } } - // sum + count + buckets - internal override int TimeseriesCount => ChildCount * (2 + _buckets.Length); + return buckets.ToArray(); } + + // sum + count + buckets + internal override int TimeseriesCount => ChildCount * (2 + _buckets.Length); } \ No newline at end of file diff --git a/Prometheus/ICounter.cs b/Prometheus/ICounter.cs index 41d66d75..86c0e9c1 100644 --- a/Prometheus/ICounter.cs +++ b/Prometheus/ICounter.cs @@ -1,19 +1,29 @@ -namespace Prometheus +namespace Prometheus; + +public interface ICounter : ICollectorChild { - public interface ICounter : ICollectorChild - { - /// - /// Increment a counter by 1. - /// - /// A set of labels representing an exemplar. - void Inc(params Exemplar.LabelPair[] exemplar); - /// - /// Increment a counter - /// - /// The increment. - /// A set of labels representing an exemplar. - void Inc(double increment = 1, params Exemplar.LabelPair[] exemplar); - void IncTo(double targetValue); - double Value { get; } - } + /// + /// Increment a counter by 1. + /// + /// + /// A set of labels representing an exemplar, created using Exemplar.From(). + /// If null, the default exemplar provider associated with the metric is asked to provide an exemplar. + /// Pass Exemplar.None to explicitly record an observation without an exemplar. + /// + void Inc(ExemplarLabelSet? exemplar); + + /// + /// Increment a counter + /// + /// The increment. + /// + /// A set of labels representing an exemplar, created using Exemplar.From(). + /// If null, the default exemplar provider associated with the metric is asked to provide an exemplar. + /// Pass Exemplar.None to explicitly record an observation without an exemplar. + /// + void Inc(double increment = 1, ExemplarLabelSet? exemplar = null); + + void IncTo(double targetValue); + + double Value { get; } } diff --git a/Prometheus/IHistogram.cs b/Prometheus/IHistogram.cs index 5abe5521..08e3636a 100644 --- a/Prometheus/IHistogram.cs +++ b/Prometheus/IHistogram.cs @@ -1,31 +1,34 @@ -namespace Prometheus +namespace Prometheus; + +public interface IHistogram : IObserver { - public interface IHistogram : IObserver - { - /// - /// Observe multiple events with a given value. - /// - /// Intended to support high frequency or batch processing use cases utilizing pre-aggregation. - /// - /// Measured value. - /// Number of observations with this value. - void Observe(double val, long count); + /// + /// Observe multiple events with a given value. + /// + /// Intended to support high frequency or batch processing use cases utilizing pre-aggregation. + /// + /// Measured value. + /// Number of observations with this value. + void Observe(double val, long count); - /// - /// Observe an event with an exemplar - /// - /// Measured value. - /// A set of labels representing an exemplar - void Observe(double val, params Exemplar.LabelPair[] exemplar); - - /// - /// Gets the sum of all observed events. - /// - double Sum { get; } + /// + /// Observe an event with an exemplar + /// + /// Measured value. + /// + /// A set of labels representing an exemplar, created using Exemplar.From(). + /// If null, the default exemplar provider associated with the metric is asked to provide an exemplar. + /// Pass Exemplar.None to explicitly record an observation without an exemplar. + /// + void Observe(double val, ExemplarLabelSet? exemplar = null); + + /// + /// Gets the sum of all observed events. + /// + double Sum { get; } - /// - /// Gets the count of all observed events. - /// - long Count { get; } - } + /// + /// Gets the count of all observed events. + /// + long Count { get; } } diff --git a/Prometheus/ObservedExemplar.cs b/Prometheus/ObservedExemplar.cs index 3b1c018c..441cfdc1 100644 --- a/Prometheus/ObservedExemplar.cs +++ b/Prometheus/ObservedExemplar.cs @@ -6,7 +6,7 @@ namespace Prometheus; /// /// Internal representation of an Exemplar ready to be serialized. /// -internal class ObservedExemplar +internal sealed class ObservedExemplar { /// /// OpenMetrics places a length limit of 128 runes on the exemplar (sum of all key value pairs). @@ -23,7 +23,7 @@ internal class ObservedExemplar internal static INowProvider NowProvider = new RealNowProvider(); - public Exemplar.LabelPair[]? Labels { get; private set; } + public ExemplarLabelSet? Labels { get; private set; } public double Value { get; private set; } public double Timestamp { get; private set; } @@ -49,18 +49,18 @@ public double Now() public bool IsValid => Labels != null; - private void Update(Exemplar.LabelPair[] labels, double value) + private void Update(ExemplarLabelSet labels, double value) { Debug.Assert(this != Empty, "Do not mutate the sentinel"); var totalRuneCount = 0; for (var i = 0; i < labels.Length; i++) { - totalRuneCount += labels[i].RuneCount; + totalRuneCount += labels.Buffer[i].RuneCount; for (var j = 0; j < labels.Length; j++) { if (i == j) continue; - if (Equal(labels[i].KeyBytes, labels[j].KeyBytes)) + if (Equal(labels.Buffer[i].KeyBytes, labels.Buffer[j].KeyBytes)) throw new ArgumentException("Exemplar contains duplicate keys."); } } @@ -81,10 +81,13 @@ private static bool Equal(byte[] a, byte[] b) return x == 0; } - public static ObservedExemplar CreatePooled(Exemplar.LabelPair[] labelPairs, double value) + /// + /// Takes ownership of the labels and will destroy them when the instance is returned to the pool. + /// + public static ObservedExemplar CreatePooled(ExemplarLabelSet labels, double value) { var instance = Pool.Get(); - instance.Update(labelPairs, value); + instance.Update(labels, value); return instance; } @@ -94,5 +97,8 @@ public static void ReturnPooledIfNotEmpty(ObservedExemplar instance) return; // We never put the "Empty" instance into the pool. Do the check here to avoid repeating it any time we return instances to the pool. Pool.Return(instance); + + if (instance.Labels.HasValue) + instance.Labels.Value.ReturnToPoolIfNotEmpty(); } } \ No newline at end of file diff --git a/Prometheus/TextSerializer.cs b/Prometheus/TextSerializer.cs index 1dd06be5..9c0bf9e6 100644 --- a/Prometheus/TextSerializer.cs +++ b/Prometheus/TextSerializer.cs @@ -126,11 +126,11 @@ public async Task WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, Can private async Task WriteExemplarAsync(CancellationToken cancel, ObservedExemplar exemplar) { await _stream.Value.WriteAsync(SpaceHashSpaceLeftBrace, 0, SpaceHashSpaceLeftBrace.Length, cancel); - for (var i = 0; i < exemplar.Labels!.Length; i++) + for (var i = 0; i < exemplar.Labels!.Value.Length; i++) { if (i > 0) await _stream.Value.WriteAsync(Comma, 0, Comma.Length, cancel); - await WriteLabel(exemplar.Labels[i].KeyBytes, exemplar.Labels[i].ValueBytes, cancel); + await WriteLabel(exemplar.Labels!.Value.Buffer[i].KeyBytes, exemplar.Labels!.Value.Buffer[i].ValueBytes, cancel); } await _stream.Value.WriteAsync(RightBraceSpace, 0, RightBraceSpace.Length, cancel); diff --git a/Sample.Console.Exemplars/Program.cs b/Sample.Console.Exemplars/Program.cs index 29f05050..9bd048e4 100644 --- a/Sample.Console.Exemplars/Program.cs +++ b/Sample.Console.Exemplars/Program.cs @@ -26,7 +26,7 @@ }); // SAMPLED EXEMPLAR: For the next histogram we only want to record exemplars for values larger than 0.1 (i.e. when record processing goes slowly). -static Exemplar.LabelPair[] RecordExemplarForSlowRecordProcessingDuration(Collector metric, double value) +static ExemplarLabelSet RecordExemplarForSlowRecordProcessingDuration(Collector metric, double value) { if (value < 0.1) return Exemplar.None; @@ -73,10 +73,10 @@ static Exemplar.LabelPair[] RecordExemplarForSlowRecordProcessingDuration(Collec // CUSTOM EXEMPLAR: We pass the record ID key-value pair when we increment the metric. // When the metric data is published to Prometheus, the most recent record ID will be attached to it. - var recordIdKeyValuePair = recordIdKey.WithValue(recordId.ToString()); + var exemplar = Exemplar.From(recordIdKey.WithValue(recordId.ToString())); - recordsProcessed.Inc(recordIdKeyValuePair); - recordSizeInPages.Observe(recordPageCount, recordIdKeyValuePair); + recordsProcessed.Inc(exemplar); + recordSizeInPages.Observe(recordPageCount, exemplar); } }); diff --git a/Sample.Web/SampleService.cs b/Sample.Web/SampleService.cs index 859f7663..14249097 100644 --- a/Sample.Web/SampleService.cs +++ b/Sample.Web/SampleService.cs @@ -68,7 +68,7 @@ private async Task ReadySetGoAsync(CancellationToken cancel) await Task.WhenAll(googleTask, microsoftTask); - var exemplar = Exemplar.Pair("traceID", "1234"); + var exemplar = Exemplar.From(Exemplar.Pair("traceID", "1234")); // Determine the winner and report the change in score. if (googleStopwatch.Elapsed < microsoftStopwatch.Elapsed) diff --git a/Tests.NetCore/HistogramTests.cs b/Tests.NetCore/HistogramTests.cs index 466da915..94878a55 100644 --- a/Tests.NetCore/HistogramTests.cs +++ b/Tests.NetCore/HistogramTests.cs @@ -17,7 +17,7 @@ public void ObserveExemplarDuplicateKeys() var factory = Metrics.WithCustomRegistry(registry); var histogram = factory.CreateHistogram("xxx", ""); - histogram.Observe(1, Exemplar.Pair("traceID", "123"), Exemplar.Pair("traceID", "1")); + histogram.Observe(1, Exemplar.From(Exemplar.Pair("traceID", "123"), Exemplar.Pair("traceID", "1"))); } [TestMethod] @@ -33,7 +33,7 @@ public void ObserveExemplarTooManyRunes() var val2 = "012345678901234"; // 15 (= 129) var histogram = factory.CreateHistogram("xxx", ""); - histogram.Observe(1, Exemplar.Pair(key1, val1), Exemplar.Pair(key2, val2)); + histogram.Observe(1, Exemplar.From(Exemplar.Pair(key1, val1), Exemplar.Pair(key2, val2))); } [TestMethod] @@ -48,7 +48,7 @@ public async Task ObserveExemplar_OnlyAddsExemplarToSingleBucket() }); var canary = "my_value_354867398"; - var exemplar = Exemplar.Pair("my_key", canary); + var exemplar = Exemplar.From(Exemplar.Pair("my_key", canary)); // We expect the exemplar to be added to the specific bucket that the value falls into, not every bucket that gets incremented. // In this case, it would be the 2.0 bucket that the exemplar belongs to (the lowest-valued bucket that gets incremented). diff --git a/Tests.NetCore/TextSerializerTests.cs b/Tests.NetCore/TextSerializerTests.cs index 8575fc80..64399543 100644 --- a/Tests.NetCore/TextSerializerTests.cs +++ b/Tests.NetCore/TextSerializerTests.cs @@ -203,10 +203,10 @@ public async Task ValidateOpenMetricsFmtHistogram_WithExemplar() Buckets = new[] { 1, 2.5, 3, Math.Pow(10, 45) } }); - counter.Observe(1, Exemplar.Pair("traceID", "1")); - counter.Observe(1.5, Exemplar.Pair("traceID", "2")); - counter.Observe(4, Exemplar.Pair("traceID", "3")); - counter.Observe(Math.Pow(10, 44), Exemplar.Pair("traceID", "4")); + counter.Observe(1, Exemplar.From(Exemplar.Pair("traceID", "1"))); + counter.Observe(1.5, Exemplar.From(Exemplar.Pair("traceID", "2"))); + counter.Observe(4, Exemplar.From(Exemplar.Pair("traceID", "3"))); + counter.Observe(Math.Pow(10, 44), Exemplar.From(Exemplar.Pair("traceID", "4"))); }); // This asserts histogram OpenMetrics form with exemplars and also using numbers which are large enough for @@ -235,7 +235,7 @@ public async Task ValidateOpenMetricsFmtCounter_MultiItemExemplar() }); counter.WithLabels("foo").Inc(1, - Exemplar.Pair("traceID", "1234"), Exemplar.Pair("yaay", "4321")); + Exemplar.From(Exemplar.Pair("traceID", "1234"), Exemplar.Pair("yaay", "4321"))); }); // This asserts that multi-labeled exemplars work as well not supplying a _total suffix in the counter name. result.ShouldBe(@"# HELP boom_bam @@ -256,7 +256,7 @@ public async Task ValidateOpenMetricsFmtCounter_TotalInNameSuffix() }); counter.WithLabels("foo").Inc(1, - Exemplar.Pair("traceID", "1234"), Exemplar.Pair("yaay", "4321")); + Exemplar.From(Exemplar.Pair("traceID", "1234"), Exemplar.Pair("yaay", "4321"))); }); // This tests the shape of OpenMetrics when _total suffix is supplied result.ShouldBe(@"# HELP boom_bam From 8cde05fc5504df127b598942eb7ba4c72c53e306 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Mon, 30 Jan 2023 11:14:29 +0200 Subject: [PATCH 086/230] Merge ExemplarLabelSet into Exemplar --- .../HttpMetrics/HttpRequestCountMiddleware.cs | 2 +- .../HttpRequestDurationMiddleware.cs | 2 +- Prometheus/AutoLeasingCounter.cs | 4 +- Prometheus/AutoLeasingHistogram.cs | 2 +- Prometheus/ChildBase.cs | 2 +- Prometheus/Counter.cs | 8 +- Prometheus/Exemplar.cs | 101 ++++++++++++++---- Prometheus/ExemplarLabelSet.cs | 56 ---------- Prometheus/ExemplarProvider.cs | 2 +- Prometheus/Histogram.cs | 6 +- Prometheus/ICounter.cs | 4 +- Prometheus/IHistogram.cs | 2 +- Prometheus/ObservedExemplar.cs | 6 +- Sample.Console.Exemplars/Program.cs | 2 +- 14 files changed, 102 insertions(+), 97 deletions(-) delete mode 100644 Prometheus/ExemplarLabelSet.cs diff --git a/Prometheus.AspNetCore/HttpMetrics/HttpRequestCountMiddleware.cs b/Prometheus.AspNetCore/HttpMetrics/HttpRequestCountMiddleware.cs index 7674ad79..faba435e 100644 --- a/Prometheus.AspNetCore/HttpMetrics/HttpRequestCountMiddleware.cs +++ b/Prometheus.AspNetCore/HttpMetrics/HttpRequestCountMiddleware.cs @@ -23,7 +23,7 @@ public async Task Invoke(HttpContext context) finally { // We pass either null (== use default exemplar provider) or None (== do not record exemplar). - ExemplarLabelSet? exemplar = _options.ExemplarPredicate(context) ? null : Exemplar.None; + Exemplar? exemplar = _options.ExemplarPredicate(context) ? null : Exemplar.None; CreateChild(context).Inc(exemplar); } diff --git a/Prometheus.AspNetCore/HttpMetrics/HttpRequestDurationMiddleware.cs b/Prometheus.AspNetCore/HttpMetrics/HttpRequestDurationMiddleware.cs index e36f1d72..820f8da4 100644 --- a/Prometheus.AspNetCore/HttpMetrics/HttpRequestDurationMiddleware.cs +++ b/Prometheus.AspNetCore/HttpMetrics/HttpRequestDurationMiddleware.cs @@ -25,7 +25,7 @@ public async Task Invoke(HttpContext context) finally { // We pass either null (== use default exemplar provider) or None (== do not record exemplar). - ExemplarLabelSet? exemplar = _options.ExemplarPredicate(context) ? null : Exemplar.None; + Exemplar? exemplar = _options.ExemplarPredicate(context) ? null : Exemplar.None; CreateChild(context).Observe(stopWatch.GetElapsedTime().TotalSeconds, exemplar); } diff --git a/Prometheus/AutoLeasingCounter.cs b/Prometheus/AutoLeasingCounter.cs index a7c47d76..c20ff8ab 100644 --- a/Prometheus/AutoLeasingCounter.cs +++ b/Prometheus/AutoLeasingCounter.cs @@ -39,12 +39,12 @@ public Instance(IManagedLifetimeMetricHandle inner, string[] labelValu public double Value => throw new NotSupportedException("Read operations on a lifetime-extending-on-use expiring metric are not supported."); - public void Inc(ExemplarLabelSet? exemplar) + public void Inc(Exemplar? exemplar) { Inc(increment:1, exemplar: exemplar); } - public void Inc(double increment, ExemplarLabelSet? exemplar) + public void Inc(double increment, Exemplar? exemplar) { _inner.WithLease(x => x.Inc(increment, exemplar), _labelValues); } diff --git a/Prometheus/AutoLeasingHistogram.cs b/Prometheus/AutoLeasingHistogram.cs index 0a104b70..2ef44dcf 100644 --- a/Prometheus/AutoLeasingHistogram.cs +++ b/Prometheus/AutoLeasingHistogram.cs @@ -45,7 +45,7 @@ public void Observe(double val, long count) _inner.WithLease(x => x.Observe(val, count), _labelValues); } - public void Observe(double val, ExemplarLabelSet? exemplar) + public void Observe(double val, Exemplar? exemplar) { _inner.WithLease(x => x.Observe(val, exemplar), _labelValues); } diff --git a/Prometheus/ChildBase.cs b/Prometheus/ChildBase.cs index e15257f2..6d78379e 100644 --- a/Prometheus/ChildBase.cs +++ b/Prometheus/ChildBase.cs @@ -115,7 +115,7 @@ internal void ReturnBorrowedExemplar(ref ObservedExemplar storage, ObservedExemp } } - protected ExemplarLabelSet ExemplarOrDefault(ExemplarLabelSet? exemplar, double value) + protected Exemplar ExemplarOrDefault(Exemplar? exemplar, double value) { // If any non-null value is provided, we use it as exemplar. // Only a null value causes us to ask the default exemplar provider. diff --git a/Prometheus/Counter.cs b/Prometheus/Counter.cs index 835a1f8b..70e4a18c 100644 --- a/Prometheus/Counter.cs +++ b/Prometheus/Counter.cs @@ -32,12 +32,12 @@ public void Inc(double increment) Inc(increment: increment, Exemplar.None); } - public void Inc(ExemplarLabelSet? exemplarLabels) + public void Inc(Exemplar? exemplarLabels) { Inc(increment: 1, exemplarLabels: exemplarLabels); } - public void Inc(double increment = 1.0, ExemplarLabelSet? exemplarLabels = null) + public void Inc(double increment = 1.0, Exemplar? exemplarLabels = null) { if (increment < 0.0) throw new ArgumentOutOfRangeException(nameof(increment), "Counter value cannot decrease."); @@ -81,12 +81,12 @@ internal Counter(string name, string help, StringSequence instanceLabelNames, La public void Publish() => Unlabelled.Publish(); public void Unpublish() => Unlabelled.Unpublish(); - public void Inc(ExemplarLabelSet? exemplar) + public void Inc(Exemplar? exemplar) { Inc(increment: 1, exemplar: exemplar); } - public void Inc(double increment = 1, ExemplarLabelSet? exemplar = null) => + public void Inc(double increment = 1, Exemplar? exemplar = null) => Unlabelled.Inc(increment, exemplar); internal override MetricType Type => MetricType.Counter; diff --git a/Prometheus/Exemplar.cs b/Prometheus/Exemplar.cs index 6eccf383..596b6bf6 100644 --- a/Prometheus/Exemplar.cs +++ b/Prometheus/Exemplar.cs @@ -1,19 +1,33 @@ #if NET6_0_OR_GREATER +using System.Buffers; +using System.Collections.Concurrent; using System.Diagnostics; #endif using System.Text; +using Microsoft.Extensions.ObjectPool; namespace Prometheus; -public static class Exemplar +/// +/// A fully-formed exemplar, describing a set of label name-value pairs. +/// +/// One-time use only - when you pass this value to a prometheus-net method, it will consume and destroy the value. +/// +/// You should preallocate and cache: +/// 1. The exemplar keys created via Exemplar.Key(). +/// 2. Exemplar key-value pairs created vvia key.WithValue() or Exemplar.Pair(). +/// +/// From the key-value pairs you can create one-use Exemplar values using Exemplar.From(). +/// +public struct Exemplar { /// - /// An exemplar value that indicates no exemplar is to be recorded. + /// An exemplar value that indicates no exemplar is to be recorded for a given observation. /// - public static readonly ExemplarLabelSet None = ExemplarLabelSet.Empty; + public static readonly Exemplar None = new Exemplar(Array.Empty(), 0); /// - /// An exemplar label key. + /// An exemplar label key. For optimal performance, create it once and reuse it forever. /// public readonly struct LabelKey { @@ -42,6 +56,7 @@ public LabelPair WithValue(string value) /// /// A single exemplar label pair in a form suitable for efficient serialization. + /// If you wish to reuse the same key-value pair, you should reuse this object as much as possible. /// public readonly struct LabelPair { @@ -59,6 +74,7 @@ internal LabelPair(byte[] keyBytes, byte[] valueBytes, int runeCount) /// /// Return an exemplar label key, this may be curried with a value to produce a LabelPair. + /// Reuse this for optimal performance. /// public static LabelKey Key(string key) { @@ -73,16 +89,16 @@ public static LabelKey Key(string key) /// /// Pair constructs a LabelPair, it is advisable to memoize a "Key" (eg: "traceID") and then to derive "LabelPair"s - /// from these. + /// from these. You may (should) reuse a LabelPair for recording multiple observations that use the same exemplar. /// public static LabelPair Pair(string key, string value) { return Key(key).WithValue(value); } - public static ExemplarLabelSet From(LabelPair labelPair1, LabelPair labelPair2, LabelPair labelPair3, LabelPair labelPair4, LabelPair labelPair5, LabelPair labelPair6) + public static Exemplar From(LabelPair labelPair1, LabelPair labelPair2, LabelPair labelPair3, LabelPair labelPair4, LabelPair labelPair5, LabelPair labelPair6) { - var exemplar = ExemplarLabelSet.AllocateFromPool(length: 6); + var exemplar = Exemplar.AllocateFromPool(length: 6); exemplar.Buffer[0] = labelPair1; exemplar.Buffer[1] = labelPair2; exemplar.Buffer[2] = labelPair3; @@ -93,9 +109,9 @@ public static ExemplarLabelSet From(LabelPair labelPair1, LabelPair labelPair2, return exemplar; } - public static ExemplarLabelSet From(LabelPair labelPair1, LabelPair labelPair2, LabelPair labelPair3, LabelPair labelPair4, LabelPair labelPair5) + public static Exemplar From(LabelPair labelPair1, LabelPair labelPair2, LabelPair labelPair3, LabelPair labelPair4, LabelPair labelPair5) { - var exemplar = ExemplarLabelSet.AllocateFromPool(length: 5); + var exemplar = Exemplar.AllocateFromPool(length: 5); exemplar.Buffer[0] = labelPair1; exemplar.Buffer[1] = labelPair2; exemplar.Buffer[2] = labelPair3; @@ -104,10 +120,10 @@ public static ExemplarLabelSet From(LabelPair labelPair1, LabelPair labelPair2, return exemplar; } - - public static ExemplarLabelSet From(LabelPair labelPair1, LabelPair labelPair2, LabelPair labelPair3, LabelPair labelPair4) + + public static Exemplar From(LabelPair labelPair1, LabelPair labelPair2, LabelPair labelPair3, LabelPair labelPair4) { - var exemplar = ExemplarLabelSet.AllocateFromPool(length: 4); + var exemplar = Exemplar.AllocateFromPool(length: 4); exemplar.Buffer[0] = labelPair1; exemplar.Buffer[1] = labelPair2; exemplar.Buffer[2] = labelPair3; @@ -116,9 +132,9 @@ public static ExemplarLabelSet From(LabelPair labelPair1, LabelPair labelPair2, return exemplar; } - public static ExemplarLabelSet From(LabelPair labelPair1, LabelPair labelPair2, LabelPair labelPair3) + public static Exemplar From(LabelPair labelPair1, LabelPair labelPair2, LabelPair labelPair3) { - var exemplar = ExemplarLabelSet.AllocateFromPool(length: 3); + var exemplar = Exemplar.AllocateFromPool(length: 3); exemplar.Buffer[0] = labelPair1; exemplar.Buffer[1] = labelPair2; exemplar.Buffer[2] = labelPair3; @@ -126,18 +142,18 @@ public static ExemplarLabelSet From(LabelPair labelPair1, LabelPair labelPair2, return exemplar; } - public static ExemplarLabelSet From(LabelPair labelPair1, LabelPair labelPair2) + public static Exemplar From(LabelPair labelPair1, LabelPair labelPair2) { - var exemplar = ExemplarLabelSet.AllocateFromPool(length: 2); + var exemplar = Exemplar.AllocateFromPool(length: 2); exemplar.Buffer[0] = labelPair1; exemplar.Buffer[1] = labelPair2; return exemplar; } - public static ExemplarLabelSet From(LabelPair labelPair1) + public static Exemplar From(LabelPair labelPair1) { - var exemplar = ExemplarLabelSet.AllocateFromPool(length: 1); + var exemplar = Exemplar.AllocateFromPool(length: 1); exemplar.Buffer[0] = labelPair1; return exemplar; @@ -147,9 +163,9 @@ public static ExemplarLabelSet From(LabelPair labelPair1) private static readonly LabelKey DefaultTraceIdKey = Key("trace_id"); private static readonly LabelKey DefaultSpanIdKey = Key("span_id"); - public static ExemplarLabelSet FromTraceContext() => FromTraceContext(DefaultTraceIdKey, DefaultSpanIdKey); + public static Exemplar FromTraceContext() => FromTraceContext(DefaultTraceIdKey, DefaultSpanIdKey); - public static ExemplarLabelSet FromTraceContext(LabelKey traceIdKey, LabelKey spanIdKey) + public static Exemplar FromTraceContext(LabelKey traceIdKey, LabelKey spanIdKey) { #if NET6_0_OR_GREATER var activity = Activity.Current; @@ -165,4 +181,49 @@ public static ExemplarLabelSet FromTraceContext(LabelKey traceIdKey, LabelKey sp // Trace context based exemplars are only supported in .NET Core, not .NET Framework. return None; } + + internal Exemplar(LabelPair[] buffer, int length) + { + Buffer = buffer; + Length = length; + } + + /// + /// The buffer containing the label pairs. Might not be fully filled! + /// + internal LabelPair[] Buffer { get; private set; } + + /// + /// Number of label pairs from the buffer to use. + /// + internal int Length { get; private set; } + + internal static Exemplar AllocateFromPool(int length) + { + if (length < 1) + throw new ArgumentOutOfRangeException(nameof(length), $"{nameof(Exemplar)} key-value pair length must be at least 1 when constructing a pool-backed value."); + +#if NET + var buffer = ArrayPool.Shared.Rent(length); +#else + // .NET Framework does not support ArrayPool, so we just allocate explicit arrays to keep it simple. Migrate to .NET Core to get better performance. + var buffer = new LabelPair[length]; +#endif + + return new Exemplar(buffer, length); + } + + internal void ReturnToPoolIfNotEmpty() + { + if (Length == 0) + return; + +#if NET + ArrayPool.Shared.Return(Buffer); +#endif + + // Just for safety, in case it gets accidentally reused. + Buffer = Array.Empty(); + Length = 0; + } } \ No newline at end of file diff --git a/Prometheus/ExemplarLabelSet.cs b/Prometheus/ExemplarLabelSet.cs deleted file mode 100644 index 91ef3d8d..00000000 --- a/Prometheus/ExemplarLabelSet.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.Buffers; - -namespace Prometheus; - -/// -/// A fully-formed exemplar, defined from a set of label name-value pairs. Create via Exemplar.From(). -/// -/// One-time use only - when you pass this value to a prometheus-net method, it will consume and destroy the value. -/// -/// -/// The purpose of this is to ensure that any labelpair arrays are allocated from a pool and reused. -/// To facilitate this, we wrap all the arrays in this class, instead of letting the caller provide their own arrays. -/// -public struct ExemplarLabelSet -{ - // We do not return this value to the pool, it is eternal. - internal static readonly ExemplarLabelSet Empty = new ExemplarLabelSet(Array.Empty(), 0); - - internal ExemplarLabelSet(Exemplar.LabelPair[] buffer, int length) - { - Buffer = buffer; - Length = length; - } - - /// - /// The buffer containing the label pairs. Might not be fully filled! - /// - internal Exemplar.LabelPair[] Buffer { get; private set; } - - /// - /// Number of label pairs from the buffer to use. - /// - internal int Length { get; private set; } - - internal static ExemplarLabelSet AllocateFromPool(int length) - { - if (length < 1) - throw new ArgumentOutOfRangeException(nameof(length), $"{nameof(ExemplarLabelSet)} data length must be at least 1."); - - var buffer = ArrayPool.Shared.Rent(length); - - return new ExemplarLabelSet(buffer, length); - } - - internal void ReturnToPoolIfNotEmpty() - { - if (Length == 0) - return; - - ArrayPool.Shared.Return(Buffer); - - // Just for safety, in case it gets accidentally reused. - Buffer = Array.Empty(); - Length = 0; - } -} diff --git a/Prometheus/ExemplarProvider.cs b/Prometheus/ExemplarProvider.cs index 9e9be87e..bd20af04 100644 --- a/Prometheus/ExemplarProvider.cs +++ b/Prometheus/ExemplarProvider.cs @@ -5,4 +5,4 @@ /// /// The metric instance for which an exemplar is being provided. /// Context-dependent - for counters, the increment; for histograms, the observed value. -public delegate ExemplarLabelSet ExemplarProvider(Collector metric, double value); +public delegate Exemplar ExemplarProvider(Collector metric, double value); diff --git a/Prometheus/Histogram.cs b/Prometheus/Histogram.cs index 52bfbbc0..599fb9a9 100644 --- a/Prometheus/Histogram.cs +++ b/Prometheus/Histogram.cs @@ -121,13 +121,13 @@ await serializer.WriteMetricPointAsync( public double Sum => _sum.Value; public long Count => _bucketCounts.Sum(b => b.Value); - public void Observe(double val, ExemplarLabelSet? exemplarLabels) => ObserveInternal(val, 1, exemplarLabels); + public void Observe(double val, Exemplar? exemplarLabels) => ObserveInternal(val, 1, exemplarLabels); public void Observe(double val) => Observe(val, 1); public void Observe(double val, long count) => ObserveInternal(val, count, null); - private void ObserveInternal(double val, long count, ExemplarLabelSet? exemplarLabels) + private void ObserveInternal(double val, long count, Exemplar? exemplarLabels) { if (double.IsNaN(val)) { @@ -165,7 +165,7 @@ private void ObserveInternal(double val, long count, ExemplarLabelSet? exemplarL public long Count => Unlabelled.Count; public void Observe(double val) => Unlabelled.Observe(val, 1); public void Observe(double val, long count) => Unlabelled.Observe(val, count); - public void Observe(double val, ExemplarLabelSet? exemplar) => Unlabelled.Observe(val, exemplar); + public void Observe(double val, Exemplar? exemplar) => Unlabelled.Observe(val, exemplar); public void Publish() => Unlabelled.Publish(); public void Unpublish() => Unlabelled.Unpublish(); diff --git a/Prometheus/ICounter.cs b/Prometheus/ICounter.cs index 86c0e9c1..7f33dda9 100644 --- a/Prometheus/ICounter.cs +++ b/Prometheus/ICounter.cs @@ -10,7 +10,7 @@ public interface ICounter : ICollectorChild /// If null, the default exemplar provider associated with the metric is asked to provide an exemplar. /// Pass Exemplar.None to explicitly record an observation without an exemplar. /// - void Inc(ExemplarLabelSet? exemplar); + void Inc(Exemplar? exemplar); /// /// Increment a counter @@ -21,7 +21,7 @@ public interface ICounter : ICollectorChild /// If null, the default exemplar provider associated with the metric is asked to provide an exemplar. /// Pass Exemplar.None to explicitly record an observation without an exemplar. /// - void Inc(double increment = 1, ExemplarLabelSet? exemplar = null); + void Inc(double increment = 1, Exemplar? exemplar = null); void IncTo(double targetValue); diff --git a/Prometheus/IHistogram.cs b/Prometheus/IHistogram.cs index 08e3636a..6dd7f58e 100644 --- a/Prometheus/IHistogram.cs +++ b/Prometheus/IHistogram.cs @@ -20,7 +20,7 @@ public interface IHistogram : IObserver /// If null, the default exemplar provider associated with the metric is asked to provide an exemplar. /// Pass Exemplar.None to explicitly record an observation without an exemplar. /// - void Observe(double val, ExemplarLabelSet? exemplar = null); + void Observe(double val, Exemplar? exemplar = null); /// /// Gets the sum of all observed events. diff --git a/Prometheus/ObservedExemplar.cs b/Prometheus/ObservedExemplar.cs index 441cfdc1..009cba63 100644 --- a/Prometheus/ObservedExemplar.cs +++ b/Prometheus/ObservedExemplar.cs @@ -23,7 +23,7 @@ internal sealed class ObservedExemplar internal static INowProvider NowProvider = new RealNowProvider(); - public ExemplarLabelSet? Labels { get; private set; } + public Exemplar? Labels { get; private set; } public double Value { get; private set; } public double Timestamp { get; private set; } @@ -49,7 +49,7 @@ public double Now() public bool IsValid => Labels != null; - private void Update(ExemplarLabelSet labels, double value) + private void Update(Exemplar labels, double value) { Debug.Assert(this != Empty, "Do not mutate the sentinel"); @@ -84,7 +84,7 @@ private static bool Equal(byte[] a, byte[] b) /// /// Takes ownership of the labels and will destroy them when the instance is returned to the pool. /// - public static ObservedExemplar CreatePooled(ExemplarLabelSet labels, double value) + public static ObservedExemplar CreatePooled(Exemplar labels, double value) { var instance = Pool.Get(); instance.Update(labels, value); diff --git a/Sample.Console.Exemplars/Program.cs b/Sample.Console.Exemplars/Program.cs index 9bd048e4..03f53915 100644 --- a/Sample.Console.Exemplars/Program.cs +++ b/Sample.Console.Exemplars/Program.cs @@ -26,7 +26,7 @@ }); // SAMPLED EXEMPLAR: For the next histogram we only want to record exemplars for values larger than 0.1 (i.e. when record processing goes slowly). -static ExemplarLabelSet RecordExemplarForSlowRecordProcessingDuration(Collector metric, double value) +static Exemplar RecordExemplarForSlowRecordProcessingDuration(Collector metric, double value) { if (value < 0.1) return Exemplar.None; From 59f5e0a95eb9b4ebfa76603919f38bc24cf95dad Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Mon, 30 Jan 2023 11:18:11 +0200 Subject: [PATCH 087/230] Update exemplar exeamples in readme --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ba66d815..75a45141 100644 --- a/README.md +++ b/README.md @@ -356,7 +356,7 @@ You can customize the default exemplar provider via `IMetricFactory.ExemplarBeha ```csharp // For the next histogram we only want to record exemplars for values larger than 0.1 (i.e. when record processing goes slowly). -static Exemplar.LabelPair[] RecordExemplarForSlowRecordProcessingDuration(Collector metric, double value) +static Exemplar RecordExemplarForSlowRecordProcessingDuration(Collector metric, double value) { if (value < 0.1) return Exemplar.None; @@ -388,8 +388,8 @@ private static readonly Exemplar.LabelKey RecordIdKey = Exemplar.Key("record_id" foreach (var record in recordsToProcess) { - var recordIdKeyValuePair = RecordIdKey.WithValue(record.Id.ToString()); - RecordsProcessed.Inc(recordIdKeyValuePair); + var exemplar = Exemplar.From(RecordIdKey.WithValue(record.Id.ToString())); + RecordsProcessed.Inc(exemplar); } ``` From 81a1595526f29d61d603ce1c48d7315a878f33f2 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Wed, 1 Feb 2023 08:07:21 +0200 Subject: [PATCH 088/230] Docs --- Sample.Console.NoAspNetCore/Program.cs | 2 +- Sample.Console/Program.cs | 2 +- Sample.Grpc/Program.cs | 2 +- Sample.Web.DifferentPort/Program.cs | 2 +- Sample.Web/Program.cs | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sample.Console.NoAspNetCore/Program.cs b/Sample.Console.NoAspNetCore/Program.cs index ce21bbbc..cd2ae775 100644 --- a/Sample.Console.NoAspNetCore/Program.cs +++ b/Sample.Console.NoAspNetCore/Program.cs @@ -38,7 +38,7 @@ // Metrics published in this sample: // * built-in process metrics giving basic information about the .NET runtime (enabled by default) -// * metrics from .NET Event Counters (enabled by default) +// * metrics from .NET Event Counters (enabled by default, updated every 10 seconds) // * metrics from .NET Meters (enabled by default) // * the custom sample counter defined above Console.WriteLine("Open http://localhost:1234/metrics in a web browser."); diff --git a/Sample.Console/Program.cs b/Sample.Console/Program.cs index 3c0d6ffe..1ea76f0a 100644 --- a/Sample.Console/Program.cs +++ b/Sample.Console/Program.cs @@ -25,7 +25,7 @@ // Metrics published in this sample: // * built-in process metrics giving basic information about the .NET runtime (enabled by default) -// * metrics from .NET Event Counters (enabled by default) +// * metrics from .NET Event Counters (enabled by default, updated every 10 seconds) // * metrics from .NET Meters (enabled by default) // * the custom sample counter defined above Console.WriteLine("Open http://localhost:1234/metrics in a web browser."); diff --git a/Sample.Grpc/Program.cs b/Sample.Grpc/Program.cs index a093dee6..7a2b94c1 100644 --- a/Sample.Grpc/Program.cs +++ b/Sample.Grpc/Program.cs @@ -28,7 +28,7 @@ // // Metrics published in this sample: // * built-in process metrics giving basic information about the .NET runtime (enabled by default) - // * metrics from .NET Event Counters (enabled by default) + // * metrics from .NET Event Counters (enabled by default, updated every 10 seconds) // * metrics from .NET Meters (enabled by default) // * metrics about HTTP requests handled by the web app (configured above) // * metrics about gRPC requests handled by the web app (configured above) diff --git a/Sample.Web.DifferentPort/Program.cs b/Sample.Web.DifferentPort/Program.cs index ebe86f8c..794fee35 100644 --- a/Sample.Web.DifferentPort/Program.cs +++ b/Sample.Web.DifferentPort/Program.cs @@ -16,7 +16,7 @@ // // Metrics published: // * built-in process metrics giving basic information about the .NET runtime (enabled by default) -// * metrics from .NET Event Counters (enabled by default) +// * metrics from .NET Event Counters (enabled by default, updated every 10 seconds) // * metrics from .NET Meters (enabled by default) // * metrics about requests handled by the web app (configured below) builder.Services.AddMetricServer(options => diff --git a/Sample.Web/Program.cs b/Sample.Web/Program.cs index 451322cc..8668aacb 100644 --- a/Sample.Web/Program.cs +++ b/Sample.Web/Program.cs @@ -47,7 +47,7 @@ // // Metrics published in this sample: // * built-in process metrics giving basic information about the .NET runtime (enabled by default) - // * metrics from .NET Event Counters (enabled by default) + // * metrics from .NET Event Counters (enabled by default, updated every 10 seconds) // * metrics from .NET Meters (enabled by default) // * metrics about requests made by registered HTTP clients used in SampleService (configured above) // * metrics about requests handled by the web app (configured above) From bb67e9c58067cbddf0987a92513902d8749ce9ce Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Wed, 1 Feb 2023 08:07:38 +0200 Subject: [PATCH 089/230] Default metrics should actually use the default metric factory now that it is user-configurable --- Prometheus/DotNetStats.cs | 141 +++++++++++---------- Prometheus/EventCounterAdapter.cs | 2 +- Prometheus/EventCounterAdapterOptions.cs | 5 + Prometheus/MeterAdapter.cs | 6 +- Prometheus/MeterAdapterOptions.cs | 5 + Prometheus/Metrics.cs | 4 +- Prometheus/SuppressDefaultMetricOptions.cs | 10 +- 7 files changed, 94 insertions(+), 79 deletions(-) diff --git a/Prometheus/DotNetStats.cs b/Prometheus/DotNetStats.cs index dd61f0dd..1de75eab 100644 --- a/Prometheus/DotNetStats.cs +++ b/Prometheus/DotNetStats.cs @@ -1,91 +1,98 @@ using System.Diagnostics; -namespace Prometheus +namespace Prometheus; + +/// +/// Collects basic .NET metrics about the current process. This is not meant to be an especially serious collector, +/// more of a producer of sample data so users of the library see something when they install it. +/// +public sealed class DotNetStats { /// - /// Collects basic .NET metrics about the current process. This is not meant to be an especially serious collector, - /// more of a producer of sample data so users of the library see something when they install it. + /// Registers the .NET metrics in the specified registry. /// - public sealed class DotNetStats + public static void Register(CollectorRegistry registry) { - /// - /// Registers the .NET metrics in the specified registry. - /// - public static void Register(CollectorRegistry registry) - { - var instance = new DotNetStats(registry); - registry.AddBeforeCollectCallback(instance.UpdateMetrics); - } + var instance = new DotNetStats(Metrics.WithCustomRegistry(registry)); + registry.AddBeforeCollectCallback(instance.UpdateMetrics); + } - private readonly Process _process; - private readonly List _collectionCounts = new List(); - private Gauge _totalMemory; - private Gauge _virtualMemorySize; - private Gauge _workingSet; - private Gauge _privateMemorySize; - private Counter _cpuTotal; - private Gauge _openHandles; - private Gauge _startTime; - private Gauge _numThreads; + /// + /// Registers the .NET metrics in the default metrics factory and registry. + /// + internal static void RegisterDefault() + { + var instance = new DotNetStats(Metrics.DefaultFactory); + Metrics.DefaultRegistry.AddBeforeCollectCallback(instance.UpdateMetrics); + } - private DotNetStats(CollectorRegistry registry) - { - _process = Process.GetCurrentProcess(); - var metrics = Metrics.WithCustomRegistry(registry); + private readonly Process _process; + private readonly List _collectionCounts = new List(); + private Gauge _totalMemory; + private Gauge _virtualMemorySize; + private Gauge _workingSet; + private Gauge _privateMemorySize; + private Counter _cpuTotal; + private Gauge _openHandles; + private Gauge _startTime; + private Gauge _numThreads; - var collectionCountsParent = metrics.CreateCounter("dotnet_collection_count_total", "GC collection count", new[] { "generation" }); + private DotNetStats(IMetricFactory metricFactory) + { + _process = Process.GetCurrentProcess(); - for (var gen = 0; gen <= GC.MaxGeneration; gen++) - { - _collectionCounts.Add(collectionCountsParent.Labels(gen.ToString())); - } + var collectionCountsParent = metricFactory.CreateCounter("dotnet_collection_count_total", "GC collection count", new[] { "generation" }); + + for (var gen = 0; gen <= GC.MaxGeneration; gen++) + { + _collectionCounts.Add(collectionCountsParent.Labels(gen.ToString())); + } - // Metrics that make sense to compare between all operating systems - // Note that old versions of pushgateway errored out if different metrics had same name but different help string. - // This is fixed in newer versions but keep the help text synchronized with the Go implementation just in case. - // See https://github.com/prometheus/pushgateway/issues/194 - // and https://github.com/prometheus-net/prometheus-net/issues/89 - _startTime = metrics.CreateGauge("process_start_time_seconds", "Start time of the process since unix epoch in seconds."); - _cpuTotal = metrics.CreateCounter("process_cpu_seconds_total", "Total user and system CPU time spent in seconds."); + // Metrics that make sense to compare between all operating systems + // Note that old versions of pushgateway errored out if different metrics had same name but different help string. + // This is fixed in newer versions but keep the help text synchronized with the Go implementation just in case. + // See https://github.com/prometheus/pushgateway/issues/194 + // and https://github.com/prometheus-net/prometheus-net/issues/89 + _startTime = metricFactory.CreateGauge("process_start_time_seconds", "Start time of the process since unix epoch in seconds."); + _cpuTotal = metricFactory.CreateCounter("process_cpu_seconds_total", "Total user and system CPU time spent in seconds."); - _virtualMemorySize = metrics.CreateGauge("process_virtual_memory_bytes", "Virtual memory size in bytes."); - _workingSet = metrics.CreateGauge("process_working_set_bytes", "Process working set"); - _privateMemorySize = metrics.CreateGauge("process_private_memory_bytes", "Process private memory size"); - _openHandles = metrics.CreateGauge("process_open_handles", "Number of open handles"); - _numThreads = metrics.CreateGauge("process_num_threads", "Total number of threads"); + _virtualMemorySize = metricFactory.CreateGauge("process_virtual_memory_bytes", "Virtual memory size in bytes."); + _workingSet = metricFactory.CreateGauge("process_working_set_bytes", "Process working set"); + _privateMemorySize = metricFactory.CreateGauge("process_private_memory_bytes", "Process private memory size"); + _openHandles = metricFactory.CreateGauge("process_open_handles", "Number of open handles"); + _numThreads = metricFactory.CreateGauge("process_num_threads", "Total number of threads"); - // .net specific metrics - _totalMemory = metrics.CreateGauge("dotnet_total_memory_bytes", "Total known allocated memory"); + // .net specific metrics + _totalMemory = metricFactory.CreateGauge("dotnet_total_memory_bytes", "Total known allocated memory"); - _startTime.SetToTimeUtc(_process.StartTime); - } + _startTime.SetToTimeUtc(_process.StartTime); + } - // The Process class is not thread-safe so let's synchronize the updates to avoid data tearing. - private readonly object _updateLock = new object(); + // The Process class is not thread-safe so let's synchronize the updates to avoid data tearing. + private readonly object _updateLock = new object(); - private void UpdateMetrics() + private void UpdateMetrics() + { + try { - try + lock (_updateLock) { - lock (_updateLock) - { - _process.Refresh(); + _process.Refresh(); - for (var gen = 0; gen <= GC.MaxGeneration; gen++) - _collectionCounts[gen].IncTo(GC.CollectionCount(gen)); + for (var gen = 0; gen <= GC.MaxGeneration; gen++) + _collectionCounts[gen].IncTo(GC.CollectionCount(gen)); - _totalMemory.Set(GC.GetTotalMemory(false)); - _virtualMemorySize.Set(_process.VirtualMemorySize64); - _workingSet.Set(_process.WorkingSet64); - _privateMemorySize.Set(_process.PrivateMemorySize64); - _cpuTotal.IncTo(_process.TotalProcessorTime.TotalSeconds); - _openHandles.Set(_process.HandleCount); - _numThreads.Set(_process.Threads.Count); - } - } - catch (Exception) - { + _totalMemory.Set(GC.GetTotalMemory(false)); + _virtualMemorySize.Set(_process.VirtualMemorySize64); + _workingSet.Set(_process.WorkingSet64); + _privateMemorySize.Set(_process.PrivateMemorySize64); + _cpuTotal.IncTo(_process.TotalProcessorTime.TotalSeconds); + _openHandles.Set(_process.HandleCount); + _numThreads.Set(_process.Threads.Count); } } + catch (Exception) + { + } } } \ No newline at end of file diff --git a/Prometheus/EventCounterAdapter.cs b/Prometheus/EventCounterAdapter.cs index 36bcf4fc..f63141a3 100644 --- a/Prometheus/EventCounterAdapter.cs +++ b/Prometheus/EventCounterAdapter.cs @@ -20,7 +20,7 @@ public sealed class EventCounterAdapter : IDisposable private EventCounterAdapter(EventCounterAdapterOptions options) { _options = options; - _metricFactory = Metrics.WithCustomRegistry(_options.Registry); + _metricFactory = _options.MetricFactory ?? Metrics.WithCustomRegistry(_options.Registry); _eventSourcesConnected = _metricFactory.CreateGauge("prometheus_net_eventcounteradapter_sources_connected_total", "Number of event sources that are currently connected to the adapter."); diff --git a/Prometheus/EventCounterAdapterOptions.cs b/Prometheus/EventCounterAdapterOptions.cs index 856d0bbc..2a14e248 100644 --- a/Prometheus/EventCounterAdapterOptions.cs +++ b/Prometheus/EventCounterAdapterOptions.cs @@ -25,4 +25,9 @@ public sealed class EventCounterAdapterOptions public TimeSpan UpdateInterval { get; set; } = TimeSpan.FromSeconds(10); public CollectorRegistry Registry { get; set; } = Metrics.DefaultRegistry; + + /// + /// If set, the value in Registry is ignored and this factory is instead used to create all the metrics. + /// + public IMetricFactory? MetricFactory { get; set; } = Metrics.DefaultFactory; } diff --git a/Prometheus/MeterAdapter.cs b/Prometheus/MeterAdapter.cs index 745ee1ff..bc1decd9 100644 --- a/Prometheus/MeterAdapter.cs +++ b/Prometheus/MeterAdapter.cs @@ -2,7 +2,6 @@ using System.Collections.Concurrent; using System.Diagnostics; using System.Diagnostics.Metrics; -using System.Diagnostics.Tracing; using System.Text; namespace Prometheus; @@ -21,8 +20,9 @@ private MeterAdapter(MeterAdapterOptions options) _options = options; _registry = options.Registry; - _factory = (ManagedLifetimeMetricFactory)Metrics.WithCustomRegistry(_options.Registry) - .WithManagedLifetime(expiresAfter: options.MetricsExpireAfter); + + var baseFactory = options.MetricFactory ?? Metrics.WithCustomRegistry(_options.Registry); + _factory = (ManagedLifetimeMetricFactory)baseFactory.WithManagedLifetime(expiresAfter: options.MetricsExpireAfter); _inheritedStaticLabelNames = ((ManagedLifetimeMetricFactory)_factory).GetAllStaticLabelNames().ToArray(); diff --git a/Prometheus/MeterAdapterOptions.cs b/Prometheus/MeterAdapterOptions.cs index 372f097b..5b782d01 100644 --- a/Prometheus/MeterAdapterOptions.cs +++ b/Prometheus/MeterAdapterOptions.cs @@ -27,6 +27,11 @@ public sealed class MeterAdapterOptions /// public CollectorRegistry Registry { get; set; } = Metrics.DefaultRegistry; + /// + /// If set, the value in Registry is ignored and this factory is instead used to create all the metrics. + /// + public IMetricFactory? MetricFactory { get; set; } = Metrics.DefaultFactory; + /// /// Enables you to define custom buckets for histogram-typed metrics. /// diff --git a/Prometheus/Metrics.cs b/Prometheus/Metrics.cs index 4a952c29..96acc858 100644 --- a/Prometheus/Metrics.cs +++ b/Prometheus/Metrics.cs @@ -14,7 +14,7 @@ public static class Metrics public static CollectorRegistry DefaultRegistry { get; private set; } /// - /// The default metric factory used to create collectors. + /// The default metric factory used to create collectors in the default registry. /// public static MetricFactory DefaultFactory { get; private set; } @@ -155,7 +155,7 @@ public static void SuppressDefaultMetrics(SuppressDefaultMetricOptions options) #endif }; - options.Configure(DefaultRegistry, configureCallbacks); + options.ApplyToDefaultRegistry(configureCallbacks); }); } diff --git a/Prometheus/SuppressDefaultMetricOptions.cs b/Prometheus/SuppressDefaultMetricOptions.cs index 6b2b19cb..26d55d5b 100644 --- a/Prometheus/SuppressDefaultMetricOptions.cs +++ b/Prometheus/SuppressDefaultMetricOptions.cs @@ -64,17 +64,15 @@ internal sealed class ConfigurationCallbacks } /// - /// Configures the target registry based on the requested defaults behavior. + /// Configures the default metrics registry based on the requested defaults behavior. /// - internal void Configure(CollectorRegistry registry, ConfigurationCallbacks configurationCallbacks) + internal void ApplyToDefaultRegistry(ConfigurationCallbacks configurationCallbacks) { - // We include some metrics by default, just to give some output when a user first uses the library. - // These are not designed to be super meaningful/useful metrics. if (!SuppressProcessMetrics) - DotNetStats.Register(registry); + DotNetStats.RegisterDefault(); if (!SuppressDebugMetrics) - registry.StartCollectingRegistryMetrics(); + Metrics.DefaultRegistry.StartCollectingRegistryMetrics(); #if NET if (!SuppressEventCounters) From 011617ce732e8eebfc3cc087b3a563fb05e57349 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Wed, 1 Feb 2023 08:17:46 +0200 Subject: [PATCH 090/230] Docs & options --- .../HttpMiddlewareExporterOptions.cs | 10 +++ README.md | 66 ++++++++++++------- 2 files changed, 52 insertions(+), 24 deletions(-) diff --git a/Prometheus.AspNetCore/HttpMetrics/HttpMiddlewareExporterOptions.cs b/Prometheus.AspNetCore/HttpMetrics/HttpMiddlewareExporterOptions.cs index 90116f13..f133f8d0 100644 --- a/Prometheus.AspNetCore/HttpMetrics/HttpMiddlewareExporterOptions.cs +++ b/Prometheus.AspNetCore/HttpMetrics/HttpMiddlewareExporterOptions.cs @@ -72,4 +72,14 @@ public void SetMetricFactory(IMetricFactory metricFactory) RequestCount.MetricFactory = metricFactory; RequestDuration.MetricFactory = metricFactory; } + + /// + /// Configures the options that are shared between all metrics exposed by the HTTP server exporter. + /// + public void ConfigureMeasurements(Action configure) + { + configure(InProgress); + configure(RequestCount); + configure(RequestDuration); + } } \ No newline at end of file diff --git a/README.md b/README.md index 75a45141..fbf2fe73 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ The library targets the following runtimes (and newer): * [Labels](#labels) * [Static labels](#static-labels) * [Exemplars](#exemplars) +* [Limiting exemplar volume](#limiting-exemplar-volume) * [When are metrics published?](#when-are-metrics-published) * [Deleting metrics](#deleting-metrics) * [ASP.NET Core exporter middleware](#aspnet-core-exporter-middleware) @@ -352,7 +353,38 @@ This will be published as the following metric point: sample_sleep_seconds_total 251.03833569999986 # {trace_id="08ad1c8cec52bf5284538abae7e6d26a",span_id="4761a4918922879b"} 1.0010688 1672634812.125 ``` -You can customize the default exemplar provider via `IMetricFactory.ExemplarBehavior` or `CounterConfiguration.ExemplarBehavior` and `HistogramConfiguration.ExemplarBehavior`, which allow you to provide your own method to generate exemplars: +You can override any default exemplar logic by providing your own exemplar when updating the value of the metric: + +```csharp +private static readonly Counter RecordsProcessed = Metrics + .CreateCounter("sample_records_processed_total", "Total number of records processed."); + +// The key from an exemplar key-value pair should be created once and reused to minimize memory allocations. +private static readonly Exemplar.LabelKey RecordIdKey = Exemplar.Key("record_id"); +... + +foreach (var record in recordsToProcess) +{ + var exemplar = Exemplar.From(RecordIdKey.WithValue(record.Id.ToString())); + RecordsProcessed.Inc(exemplar); +} +``` + +> **Warning** +> Exemplars are limited to 128 ASCII characters (counting both keys and values) - they are meant to contain IDs for cross-referencing with trace databases, not as a replacement for trace databases. + +Exemplars are only published if the metrics are being scraped by an OpenMetrics-capable client. For development purposes, you can force the library to use the OpenMetrics exposition format by adding `?accept=application/openmetrics-text` to the `/metrics` URL. + +> **Note** +> The Prometheus database automatically negotiates OpenMetrics support when scraping metrics - you do not need to apply any special scraping configuration in production scenarios. You may need to [enable exemplar storage](https://prometheus.io/docs/prometheus/latest/feature_flags/#exemplars-storage), though. + +See also, [Sample.Console.Exemplars](Sample.Console.Exemplars/Program.cs). + +# Limiting exemplar volume + +Exemplars can be expensive to store in the metrics database. For this reason, it can be useful to only record exemplars for "interesting" metric values. + +You can customize the default exemplar provider via `IMetricFactory.ExemplarBehavior` or `CounterConfiguration.ExemplarBehavior` and `HistogramConfiguration.ExemplarBehavior`, which allow you to provide your own method to generate exemplars and to filter which values/metrics exemplars are recorded for: ```csharp // For the next histogram we only want to record exemplars for values larger than 0.1 (i.e. when record processing goes slowly). @@ -376,33 +408,19 @@ var recordProcessingDuration = Metrics }); ``` -You can also override any default exemplar logic by providing your own exemplar when updating the value of the metric: +For the ASP.NET Core HTTP server metrics, you can further fine-tune exemplar recording by inspecting the HTTP request and response: ```csharp -private static readonly Counter RecordsProcessed = Metrics - .CreateCounter("sample_records_processed_total", "Total number of records processed."); - -// The key from an exemplar key-value pair should be created once and reused to minimize memory allocations. -private static readonly Exemplar.LabelKey RecordIdKey = Exemplar.Key("record_id"); -... - -foreach (var record in recordsToProcess) +app.UseHttpMetrics(options => { - var exemplar = Exemplar.From(RecordIdKey.WithValue(record.Id.ToString())); - RecordsProcessed.Inc(exemplar); -} + options.ConfigureMeasurements(measurementOptions => + { + // Only measure exemplar if the HTTP response status code is not "OK". + measurementOptions.ExemplarPredicate = context => context.Response.StatusCode != HttpStatusCode.Ok; + }); +}); ``` -> **Warning** -> Exemplars are limited to 128 ASCII characters (counting both keys and values) - they are meant to contain IDs for cross-referencing with trace databases, not as a replacement for trace databases. - -Exemplars are only published if the metrics are being scraped by an OpenMetrics-capable client. For development purposes, you can force the library to use the OpenMetrics exposition format by adding `?accept=application/openmetrics-text` to the `/metrics` URL. - -> **Note** -> The Prometheus database automatically negotiates OpenMetrics support when scraping metrics - you do not need to apply any special scraping configuration in production scenarios. You may need to [enable exemplar storage](https://prometheus.io/docs/prometheus/latest/feature_flags/#exemplars-storage), though. - -See also, [Sample.Console.Exemplars](Sample.Console.Exemplars/Program.cs). - # When are metrics published? Metrics without labels are published immediately after the `Metrics.CreateX()` call. Metrics that use labels are published when you provide the label values for the first time. @@ -549,7 +567,7 @@ public void Configure(IApplicationBuilder app, ...) By default, metrics are collected separately for each response status code (200, 201, 202, 203, ...). You can considerably reduce the size of the data set by only preserving information about the first digit of the status code: -``` +```csharp app.UseHttpMetrics(options => { // This will preserve only the first digit of the status code. From 983caf3ac74bd6086ec74b37368f7995fa42c8c8 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Wed, 1 Feb 2023 08:24:41 +0200 Subject: [PATCH 091/230] Attempt to fix flaky test --- Tests.NetCore/MetricPusherTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests.NetCore/MetricPusherTests.cs b/Tests.NetCore/MetricPusherTests.cs index 6669bb78..832756ea 100644 --- a/Tests.NetCore/MetricPusherTests.cs +++ b/Tests.NetCore/MetricPusherTests.cs @@ -27,7 +27,7 @@ void OnError(Exception ex) // Small interval to ensure that we exit fast. IntervalMilliseconds = 100, // Nothing listening there, should throw error right away. - Endpoint = "https://127.0.0.1:1", + Endpoint = "https://127.0.0.1:0", OnError = OnError }); From ece2743a8a1199f9ab81bb067a7b84a005e20603 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Wed, 1 Feb 2023 08:24:58 +0200 Subject: [PATCH 092/230] Attempt to fix flaky test --- Tests.NetCore/MetricPusherTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests.NetCore/MetricPusherTests.cs b/Tests.NetCore/MetricPusherTests.cs index 832756ea..772952d6 100644 --- a/Tests.NetCore/MetricPusherTests.cs +++ b/Tests.NetCore/MetricPusherTests.cs @@ -33,7 +33,7 @@ void OnError(Exception ex) pusher.Start(); - var onErrorWasCalled = onErrorCalled.Wait(TimeSpan.FromSeconds(5)); + var onErrorWasCalled = onErrorCalled.Wait(TimeSpan.FromSeconds(10)); Assert.IsTrue(onErrorWasCalled, "OnError was not called even though at least one failed push should have happened already."); Assert.IsNotNull(lastError); From 1da5eecf959ef2f12cc7d80164cde864dad81b1e Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Fri, 3 Feb 2023 08:36:29 +0200 Subject: [PATCH 093/230] Implement exemplar recording rate control --- Prometheus/ChildBase.cs | 48 ++++++++++++-- Prometheus/Counter.cs | 17 +++-- Prometheus/ExemplarBehavior.cs | 6 ++ Prometheus/Histogram.cs | 15 ++--- Tests.NetCore/CounterTests.cs | 112 +++++++++++++++++++++++++++------ 5 files changed, 156 insertions(+), 42 deletions(-) diff --git a/Prometheus/ChildBase.cs b/Prometheus/ChildBase.cs index 6d78379e..bf3d258b 100644 --- a/Prometheus/ChildBase.cs +++ b/Prometheus/ChildBase.cs @@ -1,3 +1,5 @@ +using System.Diagnostics; + namespace Prometheus; /// @@ -115,13 +117,49 @@ internal void ReturnBorrowedExemplar(ref ObservedExemplar storage, ObservedExemp } } - protected Exemplar ExemplarOrDefault(Exemplar? exemplar, double value) + internal void RecordExemplar(Exemplar exemplar, ref ObservedExemplar storage, double observedValue) { - // If any non-null value is provided, we use it as exemplar. - // Only a null value causes us to ask the default exemplar provider. - if (exemplar.HasValue) - return exemplar.Value; + if (exemplar.Length == 0) + return; + // We do the "is allowed" check only if we really have an exemplar to record, to minimize the performance impact on users who do not use exemplars. + // If you are using exemplars, you are already paying for a lot of value serialization overhead, so this is insignificant. + // Whereas if you are not using exemplars, the difference from this simple check can be substantial. + if (!IsRecordingNewExemplarAllowed()) + { + // We will not record the exemplar but must still release the resources to the pool. + exemplar.ReturnToPoolIfNotEmpty(); + return; + } + + // ObservedExemplar takes ownership of the Exemplar and will return its resources to the pool when the time is right. + var observedExemplar = ObservedExemplar.CreatePooled(exemplar, observedValue); + ObservedExemplar.ReturnPooledIfNotEmpty(Interlocked.Exchange(ref storage, observedExemplar)); + MarkNewExemplarHasBeenRecorded(); + } + + protected Exemplar GetDefaultExemplar(double value) + { return _exemplarBehavior.DefaultExemplarProvider?.Invoke(Parent, value) ?? Exemplar.None; } + + // May be replaced in test code. + internal static Func ExemplarRecordingTimestampProvider = DefaultExemplarRecordingTimestampProvider; + internal static long DefaultExemplarRecordingTimestampProvider() => Stopwatch.GetTimestamp(); + + // Stopwatch timetamp of when we last recorded an exemplar. We do not use ObservedExemplar.Timestamp because we do not want to + // read from an existing ObservedExemplar when we are writing to our metrics (to avoid the synchronization overhead). + // We start at a deep enough negative value to not cause funny behavior near zero point (only likely in tests, really). + private ThreadSafeLong _exemplarLastRecordedTimestamp = new(TimeSpan.FromDays(-10).Ticks); + + protected bool IsRecordingNewExemplarAllowed() + { + return _exemplarBehavior.NewExemplarMinInterval <= TimeSpan.Zero + || TimeSpan.FromTicks(ExemplarRecordingTimestampProvider() - _exemplarLastRecordedTimestamp.Value) >= _exemplarBehavior.NewExemplarMinInterval; + } + + protected void MarkNewExemplarHasBeenRecorded() + { + _exemplarLastRecordedTimestamp.Value = ExemplarRecordingTimestampProvider(); + } } \ No newline at end of file diff --git a/Prometheus/Counter.cs b/Prometheus/Counter.cs index 70e4a18c..9c35387a 100644 --- a/Prometheus/Counter.cs +++ b/Prometheus/Counter.cs @@ -32,25 +32,24 @@ public void Inc(double increment) Inc(increment: increment, Exemplar.None); } - public void Inc(Exemplar? exemplarLabels) + public void Inc(Exemplar? exemplar) { - Inc(increment: 1, exemplarLabels: exemplarLabels); + Inc(increment: 1, exemplar: exemplar); } - public void Inc(double increment = 1.0, Exemplar? exemplarLabels = null) + public void Inc(double increment = 1.0, Exemplar? exemplar = null) { if (increment < 0.0) throw new ArgumentOutOfRangeException(nameof(increment), "Counter value cannot decrease."); - exemplarLabels = ExemplarOrDefault(exemplarLabels, increment); + if (!exemplar.HasValue) + exemplar = GetDefaultExemplar(increment); - if (exemplarLabels is { Length: > 0 }) - { - var exemplar = ObservedExemplar.CreatePooled(exemplarLabels.Value, increment); - ObservedExemplar.ReturnPooledIfNotEmpty(Interlocked.Exchange(ref _observedExemplar, exemplar)); - } + if (exemplar.HasValue) + RecordExemplar(exemplar.Value, ref _observedExemplar, increment); _value.Add(increment); + Publish(); } diff --git a/Prometheus/ExemplarBehavior.cs b/Prometheus/ExemplarBehavior.cs index 136eedb8..250926cb 100644 --- a/Prometheus/ExemplarBehavior.cs +++ b/Prometheus/ExemplarBehavior.cs @@ -12,6 +12,12 @@ public sealed class ExemplarBehavior /// public ExemplarProvider? DefaultExemplarProvider { get; set; } + /// + /// A new exemplar will only be recorded for a timeseries if at least this much time has passed since the previous exemplar was recorded. + /// This can be used to limit the rate of publishing unique exemplars. By default we do not have any limit - a new exemplar always overwrites the old one. + /// + public TimeSpan NewExemplarMinInterval { get; set; } = TimeSpan.Zero; + internal static readonly ExemplarBehavior Default = new ExemplarBehavior { DefaultExemplarProvider = (_, _) => Exemplar.FromTraceContext() diff --git a/Prometheus/Histogram.cs b/Prometheus/Histogram.cs index 599fb9a9..baf4832b 100644 --- a/Prometheus/Histogram.cs +++ b/Prometheus/Histogram.cs @@ -127,14 +127,15 @@ await serializer.WriteMetricPointAsync( public void Observe(double val, long count) => ObserveInternal(val, count, null); - private void ObserveInternal(double val, long count, Exemplar? exemplarLabels) + private void ObserveInternal(double val, long count, Exemplar? exemplar) { if (double.IsNaN(val)) { return; } - exemplarLabels = ExemplarOrDefault(exemplarLabels, val); + if (!exemplar.HasValue) + exemplar = GetDefaultExemplar(val); for (int i = 0; i < _upperBounds.Length; i++) { @@ -142,13 +143,9 @@ private void ObserveInternal(double val, long count, Exemplar? exemplarLabels) { _bucketCounts[i].Add(count); - if (exemplarLabels is { Length: > 0 }) - { - // CreatePooled() takes ownership of the exemplarLabels and will return them to pool when the time is right. - var exemplar = ObservedExemplar.CreatePooled(exemplarLabels.Value, val); - ObservedExemplar.ReturnPooledIfNotEmpty(Interlocked.Exchange(ref _exemplars[i], exemplar)); - } - + if (exemplar.HasValue) + RecordExemplar(exemplar.Value, ref _exemplars[i], val); + break; } } diff --git a/Tests.NetCore/CounterTests.cs b/Tests.NetCore/CounterTests.cs index f3a7fbb3..fae49b3d 100644 --- a/Tests.NetCore/CounterTests.cs +++ b/Tests.NetCore/CounterTests.cs @@ -1,32 +1,106 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; -namespace Prometheus.Tests +namespace Prometheus.Tests; + +[TestClass] +public class CounterTests { - [TestClass] - public class CounterTests + private CollectorRegistry _registry; + private MetricFactory _metrics; + + public CounterTests() + { + _registry = Metrics.NewCustomRegistry(); + _metrics = Metrics.WithCustomRegistry(_registry); + } + + [TestMethod] + public void IncTo_IncrementsButDoesNotDecrement() + { + var counter = _metrics.CreateCounter("xxx", "xxx"); + + counter.IncTo(100); + Assert.AreEqual(100, counter.Value); + + counter.IncTo(100); + Assert.AreEqual(100, counter.Value); + + counter.IncTo(10); + Assert.AreEqual(100, counter.Value); + } + + [TestMethod] + public async Task ObserveExemplar_WithDefaultExemplarProvider_UsesDefaultOnlyWhenNoExplicitExemplarProvided() { - private CollectorRegistry _registry; - private MetricFactory _metrics; + const string defaultExemplarData = "this_is_the_default_exemplar"; + const string explicitExemplarData = "this_is_the_explicit_exemplar"; - public CounterTests() + var counter = _metrics.CreateCounter("xxx", "", new CounterConfiguration { - _registry = Metrics.NewCustomRegistry(); - _metrics = Metrics.WithCustomRegistry(_registry); - } + ExemplarBehavior = new ExemplarBehavior + { + DefaultExemplarProvider = (_, _) => Exemplar.From(Exemplar.Pair(defaultExemplarData, defaultExemplarData)) + } + }); + + // No exemplar provided, expect to see default. + counter.Inc(); + + var serialized = await _registry.CollectAndSerializeToStringAsync(ExpositionFormat.OpenMetricsText); + StringAssert.Contains(serialized, defaultExemplarData); - [TestMethod] - public void IncTo_IncrementsButDoesNotDecrement() + counter.Inc(Exemplar.From(Exemplar.Pair(explicitExemplarData, explicitExemplarData))); + + serialized = await _registry.CollectAndSerializeToStringAsync(ExpositionFormat.OpenMetricsText); + StringAssert.Contains(serialized, explicitExemplarData); + } + + [TestMethod] + public async Task ObserveExemplar_WithLimitedRecordingInterval_RecordsOnlyAfterIntervalElapses() + { + const string firstData = "this_is_the_first_exemplar"; + const string secondData = "this_is_the_second_exemplar"; + const string thirdData = "this_is_the_third_exemplar"; + + var interval = TimeSpan.FromMinutes(5); + + var counter = _metrics.CreateCounter("xxx", "", new CounterConfiguration { - var counter = _metrics.CreateCounter("xxx", "xxx"); + ExemplarBehavior = new ExemplarBehavior + { + NewExemplarMinInterval = interval + } + }); - counter.IncTo(100); - Assert.AreEqual(100, counter.Value); + long timestamp = 0; + ChildBase.ExemplarRecordingTimestampProvider = () => timestamp; + + try + { + counter.Inc(Exemplar.From(Exemplar.Pair(firstData, firstData))); - counter.IncTo(100); - Assert.AreEqual(100, counter.Value); + var serialized = await _registry.CollectAndSerializeToStringAsync(ExpositionFormat.OpenMetricsText); + StringAssert.Contains(serialized, firstData); - counter.IncTo(10); - Assert.AreEqual(100, counter.Value); + // Attempt to record a new exemplar immediately - should fail because interval has not elapsed. + counter.Inc(Exemplar.From(Exemplar.Pair(secondData, secondData))); + + serialized = await _registry.CollectAndSerializeToStringAsync(ExpositionFormat.OpenMetricsText); + StringAssert.Contains(serialized, firstData); + + // Wait for enough time to elapse - now it should work. + timestamp = interval.Ticks; + + counter.Inc(Exemplar.From(Exemplar.Pair(thirdData, thirdData))); + + serialized = await _registry.CollectAndSerializeToStringAsync(ExpositionFormat.OpenMetricsText); + StringAssert.Contains(serialized, thirdData); + } + finally + { + ChildBase.ExemplarRecordingTimestampProvider = ChildBase.DefaultExemplarRecordingTimestampProvider; } } } From 9b2b8431e922b07f2af847484f027f52f372cd69 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Fri, 3 Feb 2023 09:09:12 +0200 Subject: [PATCH 094/230] Make Exemplar a reference type for better accident avoidance --- Benchmark.NetCore/SerializationBenchmarks.cs | 133 +++++++++---------- Prometheus/ChildBase.cs | 2 + Prometheus/Counter.cs | 7 +- Prometheus/Exemplar.cs | 44 ++++-- Prometheus/Histogram.cs | 7 +- Prometheus/ObservedExemplar.cs | 4 +- Prometheus/TextSerializer.cs | 4 +- Tests.NetCore/CounterTests.cs | 12 ++ 8 files changed, 124 insertions(+), 89 deletions(-) diff --git a/Benchmark.NetCore/SerializationBenchmarks.cs b/Benchmark.NetCore/SerializationBenchmarks.cs index 3a141f5a..9aeb2021 100644 --- a/Benchmark.NetCore/SerializationBenchmarks.cs +++ b/Benchmark.NetCore/SerializationBenchmarks.cs @@ -1,89 +1,88 @@ using BenchmarkDotNet.Attributes; using Prometheus; -namespace Benchmark.NetCore +namespace Benchmark.NetCore; + +[MemoryDiagnoser] +public class SerializationBenchmarks { - [MemoryDiagnoser] - public class SerializationBenchmarks - { - // Metric -> Variant -> Label values - private static readonly string[][][] _labelValueRows; + // Metric -> Variant -> Label values + private static readonly string[][][] _labelValueRows; - private const int _metricCount = 100; - private const int _variantCount = 100; - private const int _labelCount = 5; + private const int _metricCount = 100; + private const int _variantCount = 100; + private const int _labelCount = 5; - private const string _help = "arbitrary help message for metric, not relevant for benchmarking"; + private const string _help = "arbitrary help message for metric, not relevant for benchmarking"; + + static SerializationBenchmarks() + { + _labelValueRows = new string[_metricCount][][]; - static SerializationBenchmarks() + for (var metricIndex = 0; metricIndex < _metricCount; metricIndex++) { - _labelValueRows = new string[_metricCount][][]; + var variants = new string[_variantCount][]; + _labelValueRows[metricIndex] = variants; - for (var metricIndex = 0; metricIndex < _metricCount; metricIndex++) + for (var variantIndex = 0; variantIndex < _variantCount; variantIndex++) { - var variants = new string[_variantCount][]; - _labelValueRows[metricIndex] = variants; + var values = new string[_labelCount]; + _labelValueRows[metricIndex][variantIndex] = values; - for (var variantIndex = 0; variantIndex < _variantCount; variantIndex++) - { - var values = new string[_labelCount]; - _labelValueRows[metricIndex][variantIndex] = values; - - for (var labelIndex = 0; labelIndex < _labelCount; labelIndex++) - values[labelIndex] = $"metric{metricIndex:D2}_label{labelIndex:D2}_variant{variantIndex:D2}"; - } + for (var labelIndex = 0; labelIndex < _labelCount; labelIndex++) + values[labelIndex] = $"metric{metricIndex:D2}_label{labelIndex:D2}_variant{variantIndex:D2}"; } } + } - private readonly CollectorRegistry _registry = Metrics.NewCustomRegistry(); - private readonly Counter[] _counters; - private readonly Gauge[] _gauges; - private readonly Summary[] _summaries; - private readonly Histogram[] _histograms; + private readonly CollectorRegistry _registry = Metrics.NewCustomRegistry(); + private readonly Counter[] _counters; + private readonly Gauge[] _gauges; + private readonly Summary[] _summaries; + private readonly Histogram[] _histograms; - public SerializationBenchmarks() - { - _counters = new Counter[_metricCount]; - _gauges = new Gauge[_metricCount]; - _summaries = new Summary[_metricCount]; - _histograms = new Histogram[_metricCount]; - - var factory = Metrics.WithCustomRegistry(_registry); + public SerializationBenchmarks() + { + _counters = new Counter[_metricCount]; + _gauges = new Gauge[_metricCount]; + _summaries = new Summary[_metricCount]; + _histograms = new Histogram[_metricCount]; - // Just use 1st variant for the keys (all we care about are that there is some name-like value in there). - for (var metricIndex = 0; metricIndex < _metricCount; metricIndex++) - { - _counters[metricIndex] = factory.CreateCounter($"counter{metricIndex:D2}", _help, _labelValueRows[metricIndex][0]); - _gauges[metricIndex] = factory.CreateGauge($"gauge{metricIndex:D2}", _help, _labelValueRows[metricIndex][0]); - _summaries[metricIndex] = factory.CreateSummary($"summary{metricIndex:D2}", _help, _labelValueRows[metricIndex][0]); - _histograms[metricIndex] = factory.CreateHistogram($"histogram{metricIndex:D2}", _help, _labelValueRows[metricIndex][0]); - } - } + var factory = Metrics.WithCustomRegistry(_registry); - [GlobalSetup] - public void GenerateData() + // Just use 1st variant for the keys (all we care about are that there is some name-like value in there). + for (var metricIndex = 0; metricIndex < _metricCount; metricIndex++) { - var exemplar = Exemplar.From(Exemplar.Key("traceID").WithValue("bar")); - for (var metricIndex = 0; metricIndex < _metricCount; metricIndex++) - for (var variantIndex = 0; variantIndex < _variantCount; variantIndex++) - { - _counters[metricIndex].Labels(_labelValueRows[metricIndex][variantIndex]).Inc(exemplar); - _gauges[metricIndex].Labels(_labelValueRows[metricIndex][variantIndex]).Inc(); - _summaries[metricIndex].Labels(_labelValueRows[metricIndex][variantIndex]).Observe(variantIndex); - _histograms[metricIndex].Labels(_labelValueRows[metricIndex][variantIndex]).Observe(variantIndex, exemplar); - } + _counters[metricIndex] = factory.CreateCounter($"counter{metricIndex:D2}", _help, _labelValueRows[metricIndex][0]); + _gauges[metricIndex] = factory.CreateGauge($"gauge{metricIndex:D2}", _help, _labelValueRows[metricIndex][0]); + _summaries[metricIndex] = factory.CreateSummary($"summary{metricIndex:D2}", _help, _labelValueRows[metricIndex][0]); + _histograms[metricIndex] = factory.CreateHistogram($"histogram{metricIndex:D2}", _help, _labelValueRows[metricIndex][0]); } + } - [Benchmark] - public async Task CollectAndSerialize() - { - await _registry.CollectAndSerializeAsync(new TextSerializer(Stream.Null), default); - } - - [Benchmark] - public async Task CollectAndSerializeOpenMetrics() - { - await _registry.CollectAndSerializeAsync(new TextSerializer(Stream.Null, ExpositionFormat.OpenMetricsText), default); - } + [GlobalSetup] + public void GenerateData() + { + var exemplarLabelPair = Exemplar.Key("traceID").WithValue("bar"); + for (var metricIndex = 0; metricIndex < _metricCount; metricIndex++) + for (var variantIndex = 0; variantIndex < _variantCount; variantIndex++) + { + _counters[metricIndex].Labels(_labelValueRows[metricIndex][variantIndex]).Inc(Exemplar.From(exemplarLabelPair)); + _gauges[metricIndex].Labels(_labelValueRows[metricIndex][variantIndex]).Inc(); + _summaries[metricIndex].Labels(_labelValueRows[metricIndex][variantIndex]).Observe(variantIndex); + _histograms[metricIndex].Labels(_labelValueRows[metricIndex][variantIndex]).Observe(variantIndex, Exemplar.From(exemplarLabelPair)); + } + } + + [Benchmark] + public async Task CollectAndSerialize() + { + await _registry.CollectAndSerializeAsync(new TextSerializer(Stream.Null), default); + } + + [Benchmark] + public async Task CollectAndSerializeOpenMetrics() + { + await _registry.CollectAndSerializeAsync(new TextSerializer(Stream.Null, ExpositionFormat.OpenMetricsText), default); } } diff --git a/Prometheus/ChildBase.cs b/Prometheus/ChildBase.cs index bf3d258b..e0c30be9 100644 --- a/Prometheus/ChildBase.cs +++ b/Prometheus/ChildBase.cs @@ -122,6 +122,8 @@ internal void RecordExemplar(Exemplar exemplar, ref ObservedExemplar storage, do if (exemplar.Length == 0) return; + exemplar.MarkAsConsumed(); + // We do the "is allowed" check only if we really have an exemplar to record, to minimize the performance impact on users who do not use exemplars. // If you are using exemplars, you are already paying for a lot of value serialization overhead, so this is insignificant. // Whereas if you are not using exemplars, the difference from this simple check can be substantial. diff --git a/Prometheus/Counter.cs b/Prometheus/Counter.cs index 9c35387a..97666cdc 100644 --- a/Prometheus/Counter.cs +++ b/Prometheus/Counter.cs @@ -42,11 +42,10 @@ public void Inc(double increment = 1.0, Exemplar? exemplar = null) if (increment < 0.0) throw new ArgumentOutOfRangeException(nameof(increment), "Counter value cannot decrease."); - if (!exemplar.HasValue) - exemplar = GetDefaultExemplar(increment); + exemplar ??= GetDefaultExemplar(increment); - if (exemplar.HasValue) - RecordExemplar(exemplar.Value, ref _observedExemplar, increment); + if (exemplar != null) + RecordExemplar(exemplar, ref _observedExemplar, increment); _value.Add(increment); diff --git a/Prometheus/Exemplar.cs b/Prometheus/Exemplar.cs index 596b6bf6..58fec0fd 100644 --- a/Prometheus/Exemplar.cs +++ b/Prometheus/Exemplar.cs @@ -1,6 +1,5 @@ #if NET6_0_OR_GREATER using System.Buffers; -using System.Collections.Concurrent; using System.Diagnostics; #endif using System.Text; @@ -11,7 +10,7 @@ namespace Prometheus; /// /// A fully-formed exemplar, describing a set of label name-value pairs. /// -/// One-time use only - when you pass this value to a prometheus-net method, it will consume and destroy the value. +/// One-time use only - when you pass an instance to a prometheus-net method, it will take ownership of it. /// /// You should preallocate and cache: /// 1. The exemplar keys created via Exemplar.Key(). @@ -19,10 +18,10 @@ namespace Prometheus; /// /// From the key-value pairs you can create one-use Exemplar values using Exemplar.From(). /// -public struct Exemplar +public sealed class Exemplar { /// - /// An exemplar value that indicates no exemplar is to be recorded for a given observation. + /// Indicates that no exemplar is to be recorded for a given observation. /// public static readonly Exemplar None = new Exemplar(Array.Empty(), 0); @@ -182,10 +181,22 @@ public static Exemplar FromTraceContext(LabelKey traceIdKey, LabelKey spanIdKey) return None; } - internal Exemplar(LabelPair[] buffer, int length) + public Exemplar() + { + Buffer = Array.Empty(); + } + + private Exemplar(LabelPair[] buffer, int length) + { + Buffer = buffer; + Length = length; + } + + internal void Update(LabelPair[] buffer, int length) { Buffer = buffer; Length = length; + _consumed = false; } /// @@ -198,6 +209,8 @@ internal Exemplar(LabelPair[] buffer, int length) /// internal int Length { get; private set; } + private static readonly ObjectPool ExemplarPool = ObjectPool.Create(); + internal static Exemplar AllocateFromPool(int length) { if (length < 1) @@ -210,20 +223,33 @@ internal static Exemplar AllocateFromPool(int length) var buffer = new LabelPair[length]; #endif - return new Exemplar(buffer, length); + var instance = ExemplarPool.Get(); + instance.Update(buffer, length); + return instance; } internal void ReturnToPoolIfNotEmpty() { if (Length == 0) - return; + return; // Only the None instance can have a length of 0. #if NET ArrayPool.Shared.Return(Buffer); #endif - // Just for safety, in case it gets accidentally reused. - Buffer = Array.Empty(); Length = 0; + Buffer = Array.Empty(); + + ExemplarPool.Return(this); + } + + private volatile bool _consumed; + + internal void MarkAsConsumed() + { + if (_consumed) + throw new InvalidOperationException($"An instance of {nameof(Exemplar)} was reused. You must obtain a new instance via Exemplar.From() for each observation."); + + _consumed = true; } } \ No newline at end of file diff --git a/Prometheus/Histogram.cs b/Prometheus/Histogram.cs index baf4832b..363b6ec3 100644 --- a/Prometheus/Histogram.cs +++ b/Prometheus/Histogram.cs @@ -134,8 +134,7 @@ private void ObserveInternal(double val, long count, Exemplar? exemplar) return; } - if (!exemplar.HasValue) - exemplar = GetDefaultExemplar(val); + exemplar ??= GetDefaultExemplar(val); for (int i = 0; i < _upperBounds.Length; i++) { @@ -143,8 +142,8 @@ private void ObserveInternal(double val, long count, Exemplar? exemplar) { _bucketCounts[i].Add(count); - if (exemplar.HasValue) - RecordExemplar(exemplar.Value, ref _exemplars[i], val); + if (exemplar != null) + RecordExemplar(exemplar, ref _exemplars[i], val); break; } diff --git a/Prometheus/ObservedExemplar.cs b/Prometheus/ObservedExemplar.cs index 009cba63..a589b9c2 100644 --- a/Prometheus/ObservedExemplar.cs +++ b/Prometheus/ObservedExemplar.cs @@ -96,9 +96,7 @@ public static void ReturnPooledIfNotEmpty(ObservedExemplar instance) if (object.ReferenceEquals(instance, Empty)) return; // We never put the "Empty" instance into the pool. Do the check here to avoid repeating it any time we return instances to the pool. + instance.Labels?.ReturnToPoolIfNotEmpty(); Pool.Return(instance); - - if (instance.Labels.HasValue) - instance.Labels.Value.ReturnToPoolIfNotEmpty(); } } \ No newline at end of file diff --git a/Prometheus/TextSerializer.cs b/Prometheus/TextSerializer.cs index 9c0bf9e6..77663274 100644 --- a/Prometheus/TextSerializer.cs +++ b/Prometheus/TextSerializer.cs @@ -126,11 +126,11 @@ public async Task WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, Can private async Task WriteExemplarAsync(CancellationToken cancel, ObservedExemplar exemplar) { await _stream.Value.WriteAsync(SpaceHashSpaceLeftBrace, 0, SpaceHashSpaceLeftBrace.Length, cancel); - for (var i = 0; i < exemplar.Labels!.Value.Length; i++) + for (var i = 0; i < exemplar.Labels!.Length; i++) { if (i > 0) await _stream.Value.WriteAsync(Comma, 0, Comma.Length, cancel); - await WriteLabel(exemplar.Labels!.Value.Buffer[i].KeyBytes, exemplar.Labels!.Value.Buffer[i].ValueBytes, cancel); + await WriteLabel(exemplar.Labels!.Buffer[i].KeyBytes, exemplar.Labels!.Buffer[i].ValueBytes, cancel); } await _stream.Value.WriteAsync(RightBraceSpace, 0, RightBraceSpace.Length, cancel); diff --git a/Tests.NetCore/CounterTests.cs b/Tests.NetCore/CounterTests.cs index fae49b3d..fb8fbfd5 100644 --- a/Tests.NetCore/CounterTests.cs +++ b/Tests.NetCore/CounterTests.cs @@ -103,4 +103,16 @@ public async Task ObserveExemplar_WithLimitedRecordingInterval_RecordsOnlyAfterI ChildBase.ExemplarRecordingTimestampProvider = ChildBase.DefaultExemplarRecordingTimestampProvider; } } + + [TestMethod] + public void ObserveExemplar_ReusingInstance_Throws() + { + var counter = _metrics.CreateCounter("xxx", ""); + + var exemplar = Exemplar.From(Exemplar.Pair("foo", "bar")); + + counter.Inc(exemplar); + + Assert.ThrowsException(() => counter.Inc(exemplar)); + } } From 0d54cb86c52824a7ecb99b787351e4bf9cf6e585 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Fri, 3 Feb 2023 09:31:03 +0200 Subject: [PATCH 095/230] Readme --- README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index fbf2fe73..cbf0faaf 100644 --- a/README.md +++ b/README.md @@ -384,7 +384,11 @@ See also, [Sample.Console.Exemplars](Sample.Console.Exemplars/Program.cs). Exemplars can be expensive to store in the metrics database. For this reason, it can be useful to only record exemplars for "interesting" metric values. -You can customize the default exemplar provider via `IMetricFactory.ExemplarBehavior` or `CounterConfiguration.ExemplarBehavior` and `HistogramConfiguration.ExemplarBehavior`, which allow you to provide your own method to generate exemplars and to filter which values/metrics exemplars are recorded for: +You can use `ExemplarBehavior.NewExemplarMinInterval` to define a minimum interval between exemplars - a new exemplar will only be recorded if this much time has passed. This can be useful to limit the rate of publishing unique exemplars. + +You can customize the default exemplar provider via `IMetricFactory.ExemplarBehavior` or `CounterConfiguration.ExemplarBehavior` and `HistogramConfiguration.ExemplarBehavior`, which allows you to provide your own method to generate exemplars and to filter which values/metrics exemplars are recorded for: + +Example of a custom exemplar provider used together with exemplar rate limiting: ```csharp // For the next histogram we only want to record exemplars for values larger than 0.1 (i.e. when record processing goes slowly). @@ -403,7 +407,9 @@ var recordProcessingDuration = Metrics Buckets = Histogram.PowersOfTenDividedBuckets(-4, 1, 5), ExemplarBehavior = new() { - DefaultExemplarProvider = RecordExemplarForSlowRecordProcessingDuration + DefaultExemplarProvider = RecordExemplarForSlowRecordProcessingDuration, + // Even if we have interesting data more often, do not record it to conserve exemplar storage. + NewExemplarMinInterval = TimeSpan.FromMinutes(5) } }); ``` From 06c2e2f19e22e80cc59e6c11008cb74b83d9f9a4 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Fri, 3 Feb 2023 09:34:31 +0200 Subject: [PATCH 096/230] Remove dead code --- Prometheus/Collector.cs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/Prometheus/Collector.cs b/Prometheus/Collector.cs index f6cb2394..decd30c1 100644 --- a/Prometheus/Collector.cs +++ b/Prometheus/Collector.cs @@ -233,14 +233,6 @@ internal Collector(string name, string help, StringSequence instanceLabelNames, _exemplarBehavior = exemplarBehavior; _unlabelledLazy = GetUnlabelledLazyInitializer(); - - _familyHeaderLines = new byte[][] - { - string.IsNullOrWhiteSpace(help) - ? PrometheusConstants.ExportEncoding.GetBytes($"# HELP {name}") - : PrometheusConstants.ExportEncoding.GetBytes($"# HELP {name} {help}"), - PrometheusConstants.ExportEncoding.GetBytes($"# TYPE {name} {Type.ToString().ToLowerInvariant()}") - }; } /// @@ -248,8 +240,6 @@ internal Collector(string name, string help, StringSequence instanceLabelNames, /// private protected abstract TChild NewChild(LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish, ExemplarBehavior exemplarBehavior); - private readonly byte[][] _familyHeaderLines; - internal override async Task CollectAndSerializeAsync(IMetricsSerializer serializer, bool writeFamilyDeclaration, CancellationToken cancel) { EnsureUnlabelledMetricCreatedIfNoLabels(); From 140d222cf6252c7890758f11165a355927233df1 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Fri, 3 Feb 2023 14:54:59 +0200 Subject: [PATCH 097/230] Stopwatch.GetTimestamp() is not guaranteed to use the same "tick" units as DateTime/TimeSpan. --- Prometheus/ChildBase.cs | 13 +++++++++++-- Sample.Console/Program.cs | 9 ++++++++- Tests.NetCore/CounterTests.cs | 2 +- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/Prometheus/ChildBase.cs b/Prometheus/ChildBase.cs index e0c30be9..8f5b8ebb 100644 --- a/Prometheus/ChildBase.cs +++ b/Prometheus/ChildBase.cs @@ -154,10 +154,19 @@ protected Exemplar GetDefaultExemplar(double value) // We start at a deep enough negative value to not cause funny behavior near zero point (only likely in tests, really). private ThreadSafeLong _exemplarLastRecordedTimestamp = new(TimeSpan.FromDays(-10).Ticks); + // Internal for use in tests. + internal static readonly double StopwatchTicksToDateTimeTicksFactor = TimeSpan.TicksPerSecond * 1.0 / Stopwatch.Frequency; + protected bool IsRecordingNewExemplarAllowed() { - return _exemplarBehavior.NewExemplarMinInterval <= TimeSpan.Zero - || TimeSpan.FromTicks(ExemplarRecordingTimestampProvider() - _exemplarLastRecordedTimestamp.Value) >= _exemplarBehavior.NewExemplarMinInterval; + if (_exemplarBehavior.NewExemplarMinInterval <= TimeSpan.Zero) + return true; + + // Stopwatch.GetTimestamp() is not guaranteed to use the same "tick" units as DateTime/TimeSpan. + var elapsedStopwatchTicks = ExemplarRecordingTimestampProvider() - _exemplarLastRecordedTimestamp.Value; + var elapsedDateTimeTicks = (long)(elapsedStopwatchTicks * StopwatchTicksToDateTimeTicksFactor); + + return TimeSpan.FromTicks(elapsedDateTimeTicks) >= _exemplarBehavior.NewExemplarMinInterval; } protected void MarkNewExemplarHasBeenRecorded() diff --git a/Sample.Console/Program.cs b/Sample.Console/Program.cs index 1ea76f0a..cdf1629e 100644 --- a/Sample.Console/Program.cs +++ b/Sample.Console/Program.cs @@ -5,6 +5,11 @@ // NuGet packages required: // * prometheus-net.AspNetCore +Metrics.DefaultFactory.ExemplarBehavior = new ExemplarBehavior +{ + NewExemplarMinInterval = TimeSpan.FromSeconds(30) +}; + // Start the metrics server on your preferred port number. using var server = new KestrelMetricServer(port: 1234); server.Start(); @@ -14,10 +19,12 @@ _ = Task.Run(async delegate { + var index = 0; + while (true) { // Pretend to process a record approximately every second, just for changing sample data. - recordsProcessed.Inc(); + recordsProcessed.Inc(Exemplar.From(Exemplar.Pair("foo", (++index).ToString()))); await Task.Delay(TimeSpan.FromSeconds(1)); } diff --git a/Tests.NetCore/CounterTests.cs b/Tests.NetCore/CounterTests.cs index fb8fbfd5..cac84cc3 100644 --- a/Tests.NetCore/CounterTests.cs +++ b/Tests.NetCore/CounterTests.cs @@ -91,7 +91,7 @@ public async Task ObserveExemplar_WithLimitedRecordingInterval_RecordsOnlyAfterI StringAssert.Contains(serialized, firstData); // Wait for enough time to elapse - now it should work. - timestamp = interval.Ticks; + timestamp = (long)(interval.Ticks / ChildBase.StopwatchTicksToDateTimeTicksFactor); counter.Inc(Exemplar.From(Exemplar.Pair(thirdData, thirdData))); From 1813839c745f9547f165c3cc2f1480ddc5c8a16a Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Fri, 3 Feb 2023 14:56:09 +0200 Subject: [PATCH 098/230] Revert accidental change to sample --- Sample.Console/Program.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/Sample.Console/Program.cs b/Sample.Console/Program.cs index cdf1629e..1ea76f0a 100644 --- a/Sample.Console/Program.cs +++ b/Sample.Console/Program.cs @@ -5,11 +5,6 @@ // NuGet packages required: // * prometheus-net.AspNetCore -Metrics.DefaultFactory.ExemplarBehavior = new ExemplarBehavior -{ - NewExemplarMinInterval = TimeSpan.FromSeconds(30) -}; - // Start the metrics server on your preferred port number. using var server = new KestrelMetricServer(port: 1234); server.Start(); @@ -19,12 +14,10 @@ _ = Task.Run(async delegate { - var index = 0; - while (true) { // Pretend to process a record approximately every second, just for changing sample data. - recordsProcessed.Inc(Exemplar.From(Exemplar.Pair("foo", (++index).ToString()))); + recordsProcessed.Inc(); await Task.Delay(TimeSpan.FromSeconds(1)); } From 4bf76fb4427c4cafa84c0542dd0b7cb0958d2ffb Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Fri, 3 Feb 2023 17:47:46 +0200 Subject: [PATCH 099/230] Add prometheus_net_exemplars_recorded_total --- Prometheus/ChildBase.cs | 14 ++++++++++++++ Prometheus/CollectorRegistry.cs | 17 +++++++++++++++++ Sample.Console.Exemplars/Program.cs | 6 +++--- 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/Prometheus/ChildBase.cs b/Prometheus/ChildBase.cs index 8f5b8ebb..a07003a2 100644 --- a/Prometheus/ChildBase.cs +++ b/Prometheus/ChildBase.cs @@ -138,6 +138,8 @@ internal void RecordExemplar(Exemplar exemplar, ref ObservedExemplar storage, do var observedExemplar = ObservedExemplar.CreatePooled(exemplar, observedValue); ObservedExemplar.ReturnPooledIfNotEmpty(Interlocked.Exchange(ref storage, observedExemplar)); MarkNewExemplarHasBeenRecorded(); + + ExemplarsRecorded?.Inc(); } protected Exemplar GetDefaultExemplar(double value) @@ -173,4 +175,16 @@ protected void MarkNewExemplarHasBeenRecorded() { _exemplarLastRecordedTimestamp.Value = ExemplarRecordingTimestampProvider(); } + + + // This is only set if and when debug metrics are enabled in the default registry. + private static volatile Counter? ExemplarsRecorded; + + static ChildBase() + { + Metrics.DefaultRegistry.OnStartCollectingRegistryMetrics(delegate + { + ExemplarsRecorded = Metrics.CreateCounter("prometheus_net_exemplars_recorded_total", "Number of exemplars that were accepted into in-memory storage in the prometheus-net SDK."); + }); + } } \ No newline at end of file diff --git a/Prometheus/CollectorRegistry.cs b/Prometheus/CollectorRegistry.cs index 6a476750..7c5439f5 100644 --- a/Prometheus/CollectorRegistry.cs +++ b/Prometheus/CollectorRegistry.cs @@ -318,8 +318,25 @@ internal void StartCollectingRegistryMetrics() _metricInstancesPerType[type] = _metricInstances.WithLabels(typeName); _metricTimeseriesPerType[type] = _metricTimeseries.WithLabels(typeName); } + + _startedCollectingRegistryMetrics.SetResult(true); + } + + /// + /// Registers a callback to be called when registry debug metrics are enabled. + /// If the debug metrics have already been enabled, the callback is called immediately. + /// + internal void OnStartCollectingRegistryMetrics(Action callback) + { + _startedCollectingRegistryMetrics.Task.ContinueWith(delegate + { + callback(); + return Task.CompletedTask; + }); } + private readonly TaskCompletionSource _startedCollectingRegistryMetrics = new(); + private const string MetricTypeDebugLabel = "metric_type"; private Gauge? _metricFamilies; diff --git a/Sample.Console.Exemplars/Program.cs b/Sample.Console.Exemplars/Program.cs index 03f53915..fa3eb79d 100644 --- a/Sample.Console.Exemplars/Program.cs +++ b/Sample.Console.Exemplars/Program.cs @@ -73,10 +73,10 @@ static Exemplar RecordExemplarForSlowRecordProcessingDuration(Collector metric, // CUSTOM EXEMPLAR: We pass the record ID key-value pair when we increment the metric. // When the metric data is published to Prometheus, the most recent record ID will be attached to it. - var exemplar = Exemplar.From(recordIdKey.WithValue(recordId.ToString())); + var exemplarLabelPair = recordIdKey.WithValue(recordId.ToString()); - recordsProcessed.Inc(exemplar); - recordSizeInPages.Observe(recordPageCount, exemplar); + recordsProcessed.Inc(Exemplar.From(exemplarLabelPair)); + recordSizeInPages.Observe(recordPageCount, Exemplar.From(exemplarLabelPair)); } }); From d2fa750945aa5e53f3a6c74de775059797a04f73 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 9 Feb 2023 06:30:13 +0200 Subject: [PATCH 100/230] Adjust interfaces for overload disambiguation --- Prometheus/AutoLeasingCounter.cs | 7 ++++++- Prometheus/Counter.cs | 15 +++++---------- Prometheus/ICounter.cs | 9 +++++++-- Prometheus/IHistogram.cs | 2 +- 4 files changed, 19 insertions(+), 14 deletions(-) diff --git a/Prometheus/AutoLeasingCounter.cs b/Prometheus/AutoLeasingCounter.cs index c20ff8ab..df31c265 100644 --- a/Prometheus/AutoLeasingCounter.cs +++ b/Prometheus/AutoLeasingCounter.cs @@ -39,9 +39,14 @@ public Instance(IManagedLifetimeMetricHandle inner, string[] labelValu public double Value => throw new NotSupportedException("Read operations on a lifetime-extending-on-use expiring metric are not supported."); + public void Inc(double increment) + { + Inc(increment, null); + } + public void Inc(Exemplar? exemplar) { - Inc(increment:1, exemplar: exemplar); + Inc(increment: 1, exemplar: exemplar); } public void Inc(double increment, Exemplar? exemplar) diff --git a/Prometheus/Counter.cs b/Prometheus/Counter.cs index 97666cdc..dd36db99 100644 --- a/Prometheus/Counter.cs +++ b/Prometheus/Counter.cs @@ -27,7 +27,7 @@ await serializer.WriteMetricPointAsync( ReturnBorrowedExemplar(ref _observedExemplar, exemplar); } - public void Inc(double increment) + public void Inc(double increment = 1.0) { Inc(increment: increment, Exemplar.None); } @@ -37,7 +37,7 @@ public void Inc(Exemplar? exemplar) Inc(increment: 1, exemplar: exemplar); } - public void Inc(double increment = 1.0, Exemplar? exemplar = null) + public void Inc(double increment, Exemplar? exemplar) { if (increment < 0.0) throw new ArgumentOutOfRangeException(nameof(increment), "Counter value cannot decrease."); @@ -72,20 +72,15 @@ internal Counter(string name, string help, StringSequence instanceLabelNames, La { } - public void Inc(double increment) => Unlabelled.Inc(increment); + public void Inc(double increment = 1.0) => Unlabelled.Inc(increment); public void IncTo(double targetValue) => Unlabelled.IncTo(targetValue); public double Value => Unlabelled.Value; public void Publish() => Unlabelled.Publish(); public void Unpublish() => Unlabelled.Unpublish(); - public void Inc(Exemplar? exemplar) - { - Inc(increment: 1, exemplar: exemplar); - } - - public void Inc(double increment = 1, Exemplar? exemplar = null) => - Unlabelled.Inc(increment, exemplar); + public void Inc(Exemplar? exemplar) => Inc(increment: 1, exemplar: exemplar); + public void Inc(double increment, Exemplar? exemplar) => Unlabelled.Inc(increment, exemplar); internal override MetricType Type => MetricType.Counter; diff --git a/Prometheus/ICounter.cs b/Prometheus/ICounter.cs index 7f33dda9..658e0033 100644 --- a/Prometheus/ICounter.cs +++ b/Prometheus/ICounter.cs @@ -2,6 +2,11 @@ public interface ICounter : ICollectorChild { + /// + /// Increment a counter by 1. + /// + void Inc(double increment = 1.0); + /// /// Increment a counter by 1. /// @@ -13,7 +18,7 @@ public interface ICounter : ICollectorChild void Inc(Exemplar? exemplar); /// - /// Increment a counter + /// Increment a counter. /// /// The increment. /// @@ -21,7 +26,7 @@ public interface ICounter : ICollectorChild /// If null, the default exemplar provider associated with the metric is asked to provide an exemplar. /// Pass Exemplar.None to explicitly record an observation without an exemplar. /// - void Inc(double increment = 1, Exemplar? exemplar = null); + void Inc(double increment, Exemplar? exemplar); void IncTo(double targetValue); diff --git a/Prometheus/IHistogram.cs b/Prometheus/IHistogram.cs index 6dd7f58e..72508f39 100644 --- a/Prometheus/IHistogram.cs +++ b/Prometheus/IHistogram.cs @@ -20,7 +20,7 @@ public interface IHistogram : IObserver /// If null, the default exemplar provider associated with the metric is asked to provide an exemplar. /// Pass Exemplar.None to explicitly record an observation without an exemplar. /// - void Observe(double val, Exemplar? exemplar = null); + void Observe(double val, Exemplar? exemplar); /// /// Gets the sum of all observed events. From 2f1b7119276c218db05987a27f9c2dc496cc7e2b Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 9 Feb 2023 06:40:42 +0200 Subject: [PATCH 101/230] Benchmark expansion --- Benchmark.NetCore/MeasurementBenchmarks.cs | 48 ++++++++++++++++++---- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/Benchmark.NetCore/MeasurementBenchmarks.cs b/Benchmark.NetCore/MeasurementBenchmarks.cs index ea02f104..82583823 100644 --- a/Benchmark.NetCore/MeasurementBenchmarks.cs +++ b/Benchmark.NetCore/MeasurementBenchmarks.cs @@ -29,11 +29,29 @@ public enum MetricType [Params(1, 16)] public int ThreadCount { get; set; } - [Params(MetricType.Counter, MetricType.Gauge, MetricType.Histogram, MetricType.Summary)] + [Params(MetricType.Counter, /*MetricType.Gauge,*/ MetricType.Histogram/*, MetricType.Summary*/)] public MetricType TargetMetricType { get; set; } - [Params(true, false)] - public bool WithExemplars { get; set; } + [Params(ExemplarMode.Auto, ExemplarMode.None, ExemplarMode.Provided)] + public ExemplarMode Exemplars { get; set; } + + public enum ExemplarMode + { + /// + /// No user-supplied exemplar but the default behavior is allowed to execute (and fail to provide an exemplar). + /// + Auto, + + /// + /// Explicitly indicating that no exemplar is to be used. + /// + None, + + /// + /// Explicitly providing an exemplar. + /// + Provided + } private readonly CollectorRegistry _registry; private readonly MetricFactory _factory; @@ -103,6 +121,14 @@ public void GlobalSetup() [IterationSetup] public void Setup() { + _getExemplar = Exemplars switch + { + ExemplarMode.Auto => () => null, + ExemplarMode.None => () => Exemplar.None, + ExemplarMode.Provided => () =>Exemplar.From(_traceIdLabel, _spanIdLabel), + _ => throw new NotImplementedException(), + }; + // We reuse the same registry for each iteration, as this represents typical (warmed up) usage. _threadReadyToStart = new ManualResetEventSlim[ThreadCount]; @@ -153,8 +179,7 @@ private void MeasurementThreadCounter(object state) for (var i = 0; i < MeasurementCount; i++) { - var exemplar = WithExemplars ? Exemplar.From(_traceIdLabel, _spanIdLabel) : Exemplar.None; - _counter.Inc(exemplar); + _counter.Inc(_getExemplar()); } } @@ -180,8 +205,7 @@ private void MeasurementThreadHistogram(object state) for (var i = 0; i < MeasurementCount; i++) { - var exemplar = WithExemplars ? Exemplar.From(_traceIdLabel, _spanIdLabel) : Exemplar.None; - _histogram.Observe(i, exemplar); + _histogram.Observe(i, _getExemplar()); } } @@ -206,4 +230,14 @@ public void MeasurementPerformance() for (var i = 0; i < _threads.Length; i++) _threads[i].Join(); } + + private Exemplar GetExemplar() => Exemplars switch + { + ExemplarMode.Auto => null, + ExemplarMode.None => Exemplar.None, + ExemplarMode.Provided => Exemplar.From(_traceIdLabel, _spanIdLabel), + _ => throw new NotImplementedException(), + }; + + private Func _getExemplar; } From afcc03cb8da08bc0fb4edce740e994cf9235afe6 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 9 Feb 2023 07:13:16 +0200 Subject: [PATCH 102/230] Minor speedup by using non-constant-time comparison for exemplar labels --- Benchmark.NetCore/MeasurementBenchmarks.cs | 26 +++++++++------------- Prometheus/ObservedExemplar.cs | 14 +++++++----- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/Benchmark.NetCore/MeasurementBenchmarks.cs b/Benchmark.NetCore/MeasurementBenchmarks.cs index 82583823..cbef34b1 100644 --- a/Benchmark.NetCore/MeasurementBenchmarks.cs +++ b/Benchmark.NetCore/MeasurementBenchmarks.cs @@ -121,14 +121,6 @@ public void GlobalSetup() [IterationSetup] public void Setup() { - _getExemplar = Exemplars switch - { - ExemplarMode.Auto => () => null, - ExemplarMode.None => () => Exemplar.None, - ExemplarMode.Provided => () =>Exemplar.From(_traceIdLabel, _spanIdLabel), - _ => throw new NotImplementedException(), - }; - // We reuse the same registry for each iteration, as this represents typical (warmed up) usage. _threadReadyToStart = new ManualResetEventSlim[ThreadCount]; @@ -172,6 +164,8 @@ public void Cleanup() private void MeasurementThreadCounter(object state) { + var exemplarProvider = GetExemplarProvider(); + var threadIndex = (int)state; _threadReadyToStart[threadIndex].Set(); @@ -179,7 +173,7 @@ private void MeasurementThreadCounter(object state) for (var i = 0; i < MeasurementCount; i++) { - _counter.Inc(_getExemplar()); + _counter.Inc(exemplarProvider()); } } @@ -198,6 +192,8 @@ private void MeasurementThreadGauge(object state) private void MeasurementThreadHistogram(object state) { + var exemplarProvider = GetExemplarProvider(); + var threadIndex = (int)state; _threadReadyToStart[threadIndex].Set(); @@ -205,7 +201,7 @@ private void MeasurementThreadHistogram(object state) for (var i = 0; i < MeasurementCount; i++) { - _histogram.Observe(i, _getExemplar()); + _histogram.Observe(i, exemplarProvider()); } } @@ -231,13 +227,11 @@ public void MeasurementPerformance() _threads[i].Join(); } - private Exemplar GetExemplar() => Exemplars switch + private Func GetExemplarProvider() => Exemplars switch { - ExemplarMode.Auto => null, - ExemplarMode.None => Exemplar.None, - ExemplarMode.Provided => Exemplar.From(_traceIdLabel, _spanIdLabel), + ExemplarMode.Auto => () => null, + ExemplarMode.None => () => Exemplar.None, + ExemplarMode.Provided => () => Exemplar.From(_traceIdLabel, _spanIdLabel), _ => throw new NotImplementedException(), }; - - private Func _getExemplar; } diff --git a/Prometheus/ObservedExemplar.cs b/Prometheus/ObservedExemplar.cs index a589b9c2..08f1d3ed 100644 --- a/Prometheus/ObservedExemplar.cs +++ b/Prometheus/ObservedExemplar.cs @@ -60,7 +60,7 @@ private void Update(Exemplar labels, double value) for (var j = 0; j < labels.Length; j++) { if (i == j) continue; - if (Equal(labels.Buffer[i].KeyBytes, labels.Buffer[j].KeyBytes)) + if (ByteArraysEqual(labels.Buffer[i].KeyBytes, labels.Buffer[j].KeyBytes)) throw new ArgumentException("Exemplar contains duplicate keys."); } } @@ -73,12 +73,14 @@ private void Update(Exemplar labels, double value) Timestamp = NowProvider.Now(); } - private static bool Equal(byte[] a, byte[] b) + private static bool ByteArraysEqual(byte[] a, byte[] b) { - // see https://www.syncfusion.com/succinctly-free-ebooks/application-security-in-net-succinctly/comparing-byte-arrays - var x = a.Length ^ b.Length; - for (var i = 0; i < a.Length && i < b.Length; ++i) x |= a[i] ^ b[i]; - return x == 0; + if (a.Length != b.Length) return false; + + for (var i = 0; i < a.Length; i++) + if (a[i] != b[i]) return false; + + return true; } /// From 0424d27393ece13b3744b0a72cef7e7ce9c4ea98 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 9 Feb 2023 07:35:54 +0200 Subject: [PATCH 103/230] Mark in-parameters as such No significant perf difference but just for semantics --- Prometheus/Exemplar.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Prometheus/Exemplar.cs b/Prometheus/Exemplar.cs index 58fec0fd..9d2a5b1c 100644 --- a/Prometheus/Exemplar.cs +++ b/Prometheus/Exemplar.cs @@ -48,8 +48,8 @@ internal LabelKey(byte[] key, int runeCount) /// public LabelPair WithValue(string value) { - var asciiBytes = Encoding.ASCII.GetBytes(value); - return new LabelPair(Bytes, asciiBytes, RuneCount + asciiBytes.Length); + var valueBytes = Encoding.ASCII.GetBytes(value); + return new LabelPair(Bytes, valueBytes, RuneCount + valueBytes.Length); } } @@ -95,7 +95,7 @@ public static LabelPair Pair(string key, string value) return Key(key).WithValue(value); } - public static Exemplar From(LabelPair labelPair1, LabelPair labelPair2, LabelPair labelPair3, LabelPair labelPair4, LabelPair labelPair5, LabelPair labelPair6) + public static Exemplar From(in LabelPair labelPair1, in LabelPair labelPair2, in LabelPair labelPair3, in LabelPair labelPair4, in LabelPair labelPair5, in LabelPair labelPair6) { var exemplar = Exemplar.AllocateFromPool(length: 6); exemplar.Buffer[0] = labelPair1; @@ -108,7 +108,7 @@ public static Exemplar From(LabelPair labelPair1, LabelPair labelPair2, LabelPai return exemplar; } - public static Exemplar From(LabelPair labelPair1, LabelPair labelPair2, LabelPair labelPair3, LabelPair labelPair4, LabelPair labelPair5) + public static Exemplar From(in LabelPair labelPair1, in LabelPair labelPair2, in LabelPair labelPair3, in LabelPair labelPair4, in LabelPair labelPair5) { var exemplar = Exemplar.AllocateFromPool(length: 5); exemplar.Buffer[0] = labelPair1; @@ -120,7 +120,7 @@ public static Exemplar From(LabelPair labelPair1, LabelPair labelPair2, LabelPai return exemplar; } - public static Exemplar From(LabelPair labelPair1, LabelPair labelPair2, LabelPair labelPair3, LabelPair labelPair4) + public static Exemplar From(in LabelPair labelPair1, in LabelPair labelPair2, in LabelPair labelPair3, in LabelPair labelPair4) { var exemplar = Exemplar.AllocateFromPool(length: 4); exemplar.Buffer[0] = labelPair1; @@ -131,7 +131,7 @@ public static Exemplar From(LabelPair labelPair1, LabelPair labelPair2, LabelPai return exemplar; } - public static Exemplar From(LabelPair labelPair1, LabelPair labelPair2, LabelPair labelPair3) + public static Exemplar From(in LabelPair labelPair1, in LabelPair labelPair2, in LabelPair labelPair3) { var exemplar = Exemplar.AllocateFromPool(length: 3); exemplar.Buffer[0] = labelPair1; @@ -141,7 +141,7 @@ public static Exemplar From(LabelPair labelPair1, LabelPair labelPair2, LabelPai return exemplar; } - public static Exemplar From(LabelPair labelPair1, LabelPair labelPair2) + public static Exemplar From(in LabelPair labelPair1, in LabelPair labelPair2) { var exemplar = Exemplar.AllocateFromPool(length: 2); exemplar.Buffer[0] = labelPair1; @@ -150,7 +150,7 @@ public static Exemplar From(LabelPair labelPair1, LabelPair labelPair2) return exemplar; } - public static Exemplar From(LabelPair labelPair1) + public static Exemplar From(in LabelPair labelPair1) { var exemplar = Exemplar.AllocateFromPool(length: 1); exemplar.Buffer[0] = labelPair1; @@ -164,7 +164,7 @@ public static Exemplar From(LabelPair labelPair1) public static Exemplar FromTraceContext() => FromTraceContext(DefaultTraceIdKey, DefaultSpanIdKey); - public static Exemplar FromTraceContext(LabelKey traceIdKey, LabelKey spanIdKey) + public static Exemplar FromTraceContext(in LabelKey traceIdKey, in LabelKey spanIdKey) { #if NET6_0_OR_GREATER var activity = Activity.Current; From f61e5d4221cd3f3174d95c3adfa15ba1e241baa9 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 9 Feb 2023 08:26:29 +0200 Subject: [PATCH 104/230] User low granularity time source for faster timestamping peformance --- Prometheus/ChildBase.cs | 17 ++++++---------- Prometheus/LowGranularityTimeSource.cs | 27 ++++++++++++++++++++++++++ Prometheus/ObservedExemplar.cs | 18 +++-------------- Tests.NetCore/CounterTests.cs | 6 +++--- Tests.NetCore/TextSerializerTests.cs | 17 ++++++++-------- 5 files changed, 47 insertions(+), 38 deletions(-) create mode 100644 Prometheus/LowGranularityTimeSource.cs diff --git a/Prometheus/ChildBase.cs b/Prometheus/ChildBase.cs index a07003a2..31daa352 100644 --- a/Prometheus/ChildBase.cs +++ b/Prometheus/ChildBase.cs @@ -148,27 +148,22 @@ protected Exemplar GetDefaultExemplar(double value) } // May be replaced in test code. - internal static Func ExemplarRecordingTimestampProvider = DefaultExemplarRecordingTimestampProvider; - internal static long DefaultExemplarRecordingTimestampProvider() => Stopwatch.GetTimestamp(); + internal static Func ExemplarRecordingTimestampProvider = DefaultExemplarRecordingTimestampProvider; + internal static double DefaultExemplarRecordingTimestampProvider() => LowGranularityTimeSource.GetSecondsFromUnixEpoch(); - // Stopwatch timetamp of when we last recorded an exemplar. We do not use ObservedExemplar.Timestamp because we do not want to + // Timetamp of when we last recorded an exemplar. We do not use ObservedExemplar.Timestamp because we do not want to // read from an existing ObservedExemplar when we are writing to our metrics (to avoid the synchronization overhead). // We start at a deep enough negative value to not cause funny behavior near zero point (only likely in tests, really). - private ThreadSafeLong _exemplarLastRecordedTimestamp = new(TimeSpan.FromDays(-10).Ticks); - - // Internal for use in tests. - internal static readonly double StopwatchTicksToDateTimeTicksFactor = TimeSpan.TicksPerSecond * 1.0 / Stopwatch.Frequency; + private ThreadSafeDouble _exemplarLastRecordedTimestamp = new(-100_000_000); protected bool IsRecordingNewExemplarAllowed() { if (_exemplarBehavior.NewExemplarMinInterval <= TimeSpan.Zero) return true; - // Stopwatch.GetTimestamp() is not guaranteed to use the same "tick" units as DateTime/TimeSpan. - var elapsedStopwatchTicks = ExemplarRecordingTimestampProvider() - _exemplarLastRecordedTimestamp.Value; - var elapsedDateTimeTicks = (long)(elapsedStopwatchTicks * StopwatchTicksToDateTimeTicksFactor); + var elapsedSeconds = ExemplarRecordingTimestampProvider() - _exemplarLastRecordedTimestamp.Value; - return TimeSpan.FromTicks(elapsedDateTimeTicks) >= _exemplarBehavior.NewExemplarMinInterval; + return elapsedSeconds >= _exemplarBehavior.NewExemplarMinInterval.TotalSeconds; } protected void MarkNewExemplarHasBeenRecorded() diff --git a/Prometheus/LowGranularityTimeSource.cs b/Prometheus/LowGranularityTimeSource.cs new file mode 100644 index 00000000..0cde9eb2 --- /dev/null +++ b/Prometheus/LowGranularityTimeSource.cs @@ -0,0 +1,27 @@ +namespace Prometheus; + +/// +/// We occasionally need timestamps to attach to metrics metadata. In high-performance code, calling the standard get-timestamp functions can be a nontrivial cost. +/// This class does some caching to avoid calling the expensive timestamp functions too often, giving an accurate but slightly lower granularity clock as one might otherwise get. +/// +internal static class LowGranularityTimeSource +{ + [ThreadStatic] + private static double LastUnixSeconds; + + [ThreadStatic] + private static int LastTickCount; + + public static double GetSecondsFromUnixEpoch() + { + var currentTickCount = Environment.TickCount; + + if (LastTickCount != currentTickCount) + { + LastUnixSeconds = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() / 1e3; + LastTickCount = currentTickCount; + } + + return LastUnixSeconds; + } +} diff --git a/Prometheus/ObservedExemplar.cs b/Prometheus/ObservedExemplar.cs index 08f1d3ed..ddd71047 100644 --- a/Prometheus/ObservedExemplar.cs +++ b/Prometheus/ObservedExemplar.cs @@ -21,7 +21,8 @@ internal sealed class ObservedExemplar public static readonly ObservedExemplar Empty = new(); - internal static INowProvider NowProvider = new RealNowProvider(); + internal static Func NowProvider = DefaultNowProvider; + internal static double DefaultNowProvider() => LowGranularityTimeSource.GetSecondsFromUnixEpoch(); public Exemplar? Labels { get; private set; } public double Value { get; private set; } @@ -34,19 +35,6 @@ public ObservedExemplar() Timestamp = 0; } - internal interface INowProvider - { - double Now(); - } - - private sealed class RealNowProvider : INowProvider - { - public double Now() - { - return DateTimeOffset.Now.ToUnixTimeMilliseconds() / 1e3; - } - } - public bool IsValid => Labels != null; private void Update(Exemplar labels, double value) @@ -70,7 +58,7 @@ private void Update(Exemplar labels, double value) Labels = labels; Value = value; - Timestamp = NowProvider.Now(); + Timestamp = NowProvider(); } private static bool ByteArraysEqual(byte[] a, byte[] b) diff --git a/Tests.NetCore/CounterTests.cs b/Tests.NetCore/CounterTests.cs index cac84cc3..a6eb79c9 100644 --- a/Tests.NetCore/CounterTests.cs +++ b/Tests.NetCore/CounterTests.cs @@ -74,8 +74,8 @@ public async Task ObserveExemplar_WithLimitedRecordingInterval_RecordsOnlyAfterI } }); - long timestamp = 0; - ChildBase.ExemplarRecordingTimestampProvider = () => timestamp; + double timestampSeconds = 0; + ChildBase.ExemplarRecordingTimestampProvider = () => timestampSeconds; try { @@ -91,7 +91,7 @@ public async Task ObserveExemplar_WithLimitedRecordingInterval_RecordsOnlyAfterI StringAssert.Contains(serialized, firstData); // Wait for enough time to elapse - now it should work. - timestamp = (long)(interval.Ticks / ChildBase.StopwatchTicksToDateTimeTicksFactor); + timestampSeconds = interval.TotalSeconds; counter.Inc(Exemplar.From(Exemplar.Pair(thirdData, thirdData))); diff --git a/Tests.NetCore/TextSerializerTests.cs b/Tests.NetCore/TextSerializerTests.cs index 64399543..922d8797 100644 --- a/Tests.NetCore/TextSerializerTests.cs +++ b/Tests.NetCore/TextSerializerTests.cs @@ -12,7 +12,13 @@ public class TextSerializerTests [ClassInitialize] public static void BeforeClass(TestContext testContext) { - ObservedExemplar.NowProvider = new TestNowProvider(); + ObservedExemplar.NowProvider = () => TestNow; + } + + [ClassCleanup] + public static void AfterClass() + { + ObservedExemplar.NowProvider = ObservedExemplar.DefaultNowProvider; } [TestMethod] @@ -266,14 +272,7 @@ public async Task ValidateOpenMetricsFmtCounter_TotalInNameSuffix() "); } - private class TestNowProvider : ObservedExemplar.INowProvider - { - public readonly double TestNow = 1668779954.714; - public double Now() - { - return TestNow; - } - } + private const double TestNow = 1668779954.714; private class TestCase { From baa6eb06d66becb414a30abe5ca009f9b93e433e Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 9 Feb 2023 09:39:20 +0200 Subject: [PATCH 105/230] Fix bug: Exemplar.None was passed in some overloads when null (==default) was expected --- Prometheus/AutoLeasingHistogram.cs | 2 +- Prometheus/Counter.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Prometheus/AutoLeasingHistogram.cs b/Prometheus/AutoLeasingHistogram.cs index 2ef44dcf..c9ca10d4 100644 --- a/Prometheus/AutoLeasingHistogram.cs +++ b/Prometheus/AutoLeasingHistogram.cs @@ -52,7 +52,7 @@ public void Observe(double val, Exemplar? exemplar) public void Observe(double val) { - Observe(val, Exemplar.None); + Observe(val, null); } } } diff --git a/Prometheus/Counter.cs b/Prometheus/Counter.cs index dd36db99..cf65b5d9 100644 --- a/Prometheus/Counter.cs +++ b/Prometheus/Counter.cs @@ -29,7 +29,7 @@ await serializer.WriteMetricPointAsync( public void Inc(double increment = 1.0) { - Inc(increment: increment, Exemplar.None); + Inc(increment: increment, null); } public void Inc(Exemplar? exemplar) From 4f8f59c132a9242192cd9c1caa6137e4712e7ed3 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 9 Feb 2023 09:44:44 +0200 Subject: [PATCH 106/230] We cannot record an exemplar every time we record an exemplar! --- Prometheus/ChildBase.cs | 3 ++- Prometheus/Exemplar.cs | 16 +++++++++++++++- Sample.Console.Exemplars/Program.cs | 7 ++++--- 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/Prometheus/ChildBase.cs b/Prometheus/ChildBase.cs index 31daa352..0ca2aff2 100644 --- a/Prometheus/ChildBase.cs +++ b/Prometheus/ChildBase.cs @@ -139,7 +139,8 @@ internal void RecordExemplar(Exemplar exemplar, ref ObservedExemplar storage, do ObservedExemplar.ReturnPooledIfNotEmpty(Interlocked.Exchange(ref storage, observedExemplar)); MarkNewExemplarHasBeenRecorded(); - ExemplarsRecorded?.Inc(); + // We cannot record an exemplar every time we record an exemplar! + ExemplarsRecorded?.Inc(Exemplar.None); } protected Exemplar GetDefaultExemplar(double value) diff --git a/Prometheus/Exemplar.cs b/Prometheus/Exemplar.cs index 9d2a5b1c..f41616d4 100644 --- a/Prometheus/Exemplar.cs +++ b/Prometheus/Exemplar.cs @@ -17,6 +17,7 @@ namespace Prometheus; /// 2. Exemplar key-value pairs created vvia key.WithValue() or Exemplar.Pair(). /// /// From the key-value pairs you can create one-use Exemplar values using Exemplar.From(). +/// You can clone Exemplar instances using Exemplar.Clone() - each clone can only be used once! /// public sealed class Exemplar { @@ -248,8 +249,21 @@ internal void ReturnToPoolIfNotEmpty() internal void MarkAsConsumed() { if (_consumed) - throw new InvalidOperationException($"An instance of {nameof(Exemplar)} was reused. You must obtain a new instance via Exemplar.From() for each observation."); + throw new InvalidOperationException($"An instance of {nameof(Exemplar)} was reused. You must obtain a new instance via Exemplar.From() or Exemplar.Clone() for each metric value observation."); _consumed = true; } + + /// + /// Clones the exemplar so it can be reused - each copy can only be used once! + /// + public Exemplar Clone() + { + if (_consumed) + throw new InvalidOperationException($"An instance of {nameof(Exemplar)} cannot be cloned after it has already been used."); + + var clone = AllocateFromPool(Length); + Array.Copy(Buffer, clone.Buffer, Length); + return clone; + } } \ No newline at end of file diff --git a/Sample.Console.Exemplars/Program.cs b/Sample.Console.Exemplars/Program.cs index fa3eb79d..3a9e6f6b 100644 --- a/Sample.Console.Exemplars/Program.cs +++ b/Sample.Console.Exemplars/Program.cs @@ -73,10 +73,11 @@ static Exemplar RecordExemplarForSlowRecordProcessingDuration(Collector metric, // CUSTOM EXEMPLAR: We pass the record ID key-value pair when we increment the metric. // When the metric data is published to Prometheus, the most recent record ID will be attached to it. - var exemplarLabelPair = recordIdKey.WithValue(recordId.ToString()); + var exemplar = Exemplar.From(recordIdKey.WithValue(recordId.ToString())); - recordsProcessed.Inc(Exemplar.From(exemplarLabelPair)); - recordSizeInPages.Observe(recordPageCount, Exemplar.From(exemplarLabelPair)); + // Note that one Exemplar object can only be used once. You must clone it to reuse it. + recordsProcessed.Inc(exemplar.Clone()); + recordSizeInPages.Observe(recordPageCount, exemplar); } }); From c35ac64063a3567a4334fc2c8430d1b2ede4a9ec Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 9 Feb 2023 11:59:22 +0200 Subject: [PATCH 107/230] Fix timestamp math to use floating point division --- Prometheus/LowGranularityTimeSource.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Prometheus/LowGranularityTimeSource.cs b/Prometheus/LowGranularityTimeSource.cs index 0cde9eb2..6a0b65de 100644 --- a/Prometheus/LowGranularityTimeSource.cs +++ b/Prometheus/LowGranularityTimeSource.cs @@ -18,7 +18,7 @@ public static double GetSecondsFromUnixEpoch() if (LastTickCount != currentTickCount) { - LastUnixSeconds = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() / 1e3; + LastUnixSeconds = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() / 1000.0; LastTickCount = currentTickCount; } From a7c1277f79124540ee8ea19f0a4a7f629fad37f3 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Fri, 10 Feb 2023 09:47:58 +0200 Subject: [PATCH 108/230] MeterAdapter and EventCounterAdapter will only start the default listener once per metrics registry Fixes #398 --- Prometheus/CollectorRegistry.cs | 5 +++++ Prometheus/EventCounterAdapter.cs | 19 +++++++++++++++++-- Prometheus/EventCounterAdapterOptions.cs | 4 ++-- Prometheus/MeterAdapter.cs | 19 +++++++++++++++++-- Prometheus/MeterAdapterOptions.cs | 4 ++-- Prometheus/NoopDisposable.cs | 8 ++++++++ 6 files changed, 51 insertions(+), 8 deletions(-) create mode 100644 Prometheus/NoopDisposable.cs diff --git a/Prometheus/CollectorRegistry.cs b/Prometheus/CollectorRegistry.cs index 7c5439f5..219616ce 100644 --- a/Prometheus/CollectorRegistry.cs +++ b/Prometheus/CollectorRegistry.cs @@ -378,4 +378,9 @@ private void UpdateRegistryMetrics() _metricTimeseriesPerType[type].Set(timeseries); } } + + // We only allow integration adapters to be started once per registry with the default configuration, to prevent double-counting values. + // This is useful because we switched on adapters by default in 7.0.0 but if someone has manual .StartListening() calls from before, they would now count metrics double. + internal bool PreventMeterAdapterRegistrationWithDefaultOptions; + internal bool PreventEventCounterAdapterRegistrationWithDefaultOptions; } diff --git a/Prometheus/EventCounterAdapter.cs b/Prometheus/EventCounterAdapter.cs index f63141a3..78113998 100644 --- a/Prometheus/EventCounterAdapter.cs +++ b/Prometheus/EventCounterAdapter.cs @@ -13,9 +13,24 @@ namespace Prometheus; /// public sealed class EventCounterAdapter : IDisposable { - public static IDisposable StartListening() => new EventCounterAdapter(EventCounterAdapterOptions.Default); + public static IDisposable StartListening() => StartListening(EventCounterAdapterOptions.Default); - public static IDisposable StartListening(EventCounterAdapterOptions options) => new EventCounterAdapter(options); + public static IDisposable StartListening(EventCounterAdapterOptions options) + { + // If we are re-registering an adapter with the default options, just pretend and move on. + // The purpose of this code is to avoid double-counting metrics if the adapter is registered twice with the default options. + // This could happen because in 7.0.0 we added automatic registration of the adapters on startup, but the user might still + // have a manual registration active from 6.0.0 days. We do this small thing here to make upgrading less hassle. + if (options == EventCounterAdapterOptions.Default) + { + if (options.Registry.PreventEventCounterAdapterRegistrationWithDefaultOptions) + return new NoopDisposable(); + + options.Registry.PreventEventCounterAdapterRegistrationWithDefaultOptions = true; + } + + return new EventCounterAdapter(options); + } private EventCounterAdapter(EventCounterAdapterOptions options) { diff --git a/Prometheus/EventCounterAdapterOptions.cs b/Prometheus/EventCounterAdapterOptions.cs index 2a14e248..55db8200 100644 --- a/Prometheus/EventCounterAdapterOptions.cs +++ b/Prometheus/EventCounterAdapterOptions.cs @@ -1,8 +1,8 @@ namespace Prometheus; -public sealed class EventCounterAdapterOptions +public sealed record EventCounterAdapterOptions { - public static readonly EventCounterAdapterOptions Default = new(); + public static EventCounterAdapterOptions Default => new(); /// /// By default we subscribe to a predefined set of generally useful event counters but this allows you to specify a custom filter by event source name. diff --git a/Prometheus/MeterAdapter.cs b/Prometheus/MeterAdapter.cs index bc1decd9..c2ff9e1e 100644 --- a/Prometheus/MeterAdapter.cs +++ b/Prometheus/MeterAdapter.cs @@ -11,9 +11,24 @@ namespace Prometheus; /// public sealed class MeterAdapter : IDisposable { - public static IDisposable StartListening() => new MeterAdapter(MeterAdapterOptions.Default); + public static IDisposable StartListening() => StartListening(MeterAdapterOptions.Default); - public static IDisposable StartListening(MeterAdapterOptions options) => new MeterAdapter(options); + public static IDisposable StartListening(MeterAdapterOptions options) + { + // If we are re-registering an adapter with the default options, just pretend and move on. + // The purpose of this code is to avoid double-counting metrics if the adapter is registered twice with the default options. + // This could happen because in 7.0.0 we added automatic registration of the adapters on startup, but the user might still + // have a manual registration active from 6.0.0 days. We do this small thing here to make upgrading less hassle. + if (options == MeterAdapterOptions.Default) + { + if (options.Registry.PreventMeterAdapterRegistrationWithDefaultOptions) + return new NoopDisposable(); + + options.Registry.PreventMeterAdapterRegistrationWithDefaultOptions = true; + } + + return new MeterAdapter(options); + } private MeterAdapter(MeterAdapterOptions options) { diff --git a/Prometheus/MeterAdapterOptions.cs b/Prometheus/MeterAdapterOptions.cs index 5b782d01..b66e7e1a 100644 --- a/Prometheus/MeterAdapterOptions.cs +++ b/Prometheus/MeterAdapterOptions.cs @@ -3,9 +3,9 @@ namespace Prometheus; -public sealed class MeterAdapterOptions +public sealed record MeterAdapterOptions { - public static readonly MeterAdapterOptions Default = new(); + public static MeterAdapterOptions Default => new(); // This is unlikely to be suitable for all cases, so you will want to customize it per-instrument. public static readonly double[] DefaultHistogramBuckets = Histogram.ExponentialBuckets(0.01, 2, 25); diff --git a/Prometheus/NoopDisposable.cs b/Prometheus/NoopDisposable.cs new file mode 100644 index 00000000..04364cb6 --- /dev/null +++ b/Prometheus/NoopDisposable.cs @@ -0,0 +1,8 @@ +namespace Prometheus; + +internal sealed class NoopDisposable : IDisposable +{ + public void Dispose() + { + } +} From 17e7c0fc11d52a9be4584e5986d2ab0100f5995d Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Sun, 12 Feb 2023 09:33:36 +0200 Subject: [PATCH 109/230] Add back missing overload of CollectAndExportAsTextAsync --- Prometheus/CollectorRegistry.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Prometheus/CollectorRegistry.cs b/Prometheus/CollectorRegistry.cs index 219616ce..fb278f03 100644 --- a/Prometheus/CollectorRegistry.cs +++ b/Prometheus/CollectorRegistry.cs @@ -131,6 +131,14 @@ internal LabelSequence GetStaticLabels() } #endregion + /// + /// Collects all metrics and exports them in text document format to the provided stream. + /// + /// This method is designed to be used with custom output mechanisms that do not use an IMetricServer. + /// + public Task CollectAndExportAsTextAsync(Stream to, CancellationToken cancel = default) + => CollectAndExportAsTextAsync(to, ExpositionFormat.PrometheusText, cancel); + /// /// Collects all metrics and exports them in text document format to the provided stream. /// From a055f5bdfccd7bc298bc1f726c481f1f4c36b54c Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Sun, 12 Feb 2023 14:23:15 +0200 Subject: [PATCH 110/230] Disambiguate osme overloads --- Prometheus/CollectorRegistry.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Prometheus/CollectorRegistry.cs b/Prometheus/CollectorRegistry.cs index fb278f03..0110152c 100644 --- a/Prometheus/CollectorRegistry.cs +++ b/Prometheus/CollectorRegistry.cs @@ -144,7 +144,7 @@ public Task CollectAndExportAsTextAsync(Stream to, CancellationToken cancel = de /// /// This method is designed to be used with custom output mechanisms that do not use an IMetricServer. /// - public Task CollectAndExportAsTextAsync(Stream to, ExpositionFormat format = ExpositionFormat.PrometheusText, CancellationToken cancel = default) + public Task CollectAndExportAsTextAsync(Stream to, ExpositionFormat format, CancellationToken cancel = default) { if (to == null) throw new ArgumentNullException(nameof(to)); From 718dffc2407efce8bd72b649edf77d8d86763cb9 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Tue, 18 Jul 2023 07:27:22 +0300 Subject: [PATCH 111/230] Allow ObservableCounter to be reset. Previously, the MeterAdapter output got stuck on its previous maximum if the underlying Meter reset its value to a lower value. Now we transform the value 1:1. --- History | 1 + Prometheus/MeterAdapter.cs | 4 ++-- Resources/SolutionAssemblyInfo.cs | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/History b/History index c9bc216a..3dd958ba 100644 --- a/History +++ b/History @@ -1,3 +1,4 @@ +- Allow ObservableCounter to be reset. Previously, the MeterAdapter output got stuck on its previous maximum if the underlying Meter reset its value to a lower value. Now we transform the value 1:1. * 8.0.0 - Added OpenMetrics exposition format support (#388). - Added exemplar support for Counter and Histogram (#388). diff --git a/Prometheus/MeterAdapter.cs b/Prometheus/MeterAdapter.cs index c2ff9e1e..2c545d00 100644 --- a/Prometheus/MeterAdapter.cs +++ b/Prometheus/MeterAdapter.cs @@ -150,8 +150,8 @@ private void OnMeasurementRecorded(Instrument instrument, T measurement, Read { var handle = _factory.CreateGauge(_instrumentPrometheusNames[instrument], _instrumentPrometheusHelp[instrument], labelNames); - // A measurement is the current value. - handle.WithLease(x => x.IncTo(value), labelValues); + // A measurement is the current value. We transform it into a Set() to allow the counter to reset itself (unusual but who are we to say no). + handle.WithLease(x => x.Set(value), labelValues); } #if NET7_0_OR_GREATER else if (instrument is UpDownCounter) diff --git a/Resources/SolutionAssemblyInfo.cs b/Resources/SolutionAssemblyInfo.cs index e6e5e379..63602934 100644 --- a/Resources/SolutionAssemblyInfo.cs +++ b/Resources/SolutionAssemblyInfo.cs @@ -2,7 +2,7 @@ using System.Runtime.CompilerServices; // This is the real version number, used in NuGet packages and for display purposes. -[assembly: AssemblyFileVersion("8.0.0")] +[assembly: AssemblyFileVersion("8.0.1")] // Only use major version here, with others kept at zero, for correct assembly binding logic. [assembly: AssemblyVersion("8.0.0")] From ea794f6186753782dbfd8a32ed2220f0699ee864 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Tue, 18 Jul 2023 10:37:09 +0300 Subject: [PATCH 112/230] 8.0.1 --- History | 1 + 1 file changed, 1 insertion(+) diff --git a/History b/History index 3dd958ba..db61c432 100644 --- a/History +++ b/History @@ -1,3 +1,4 @@ +* 8.0.1 - Allow ObservableCounter to be reset. Previously, the MeterAdapter output got stuck on its previous maximum if the underlying Meter reset its value to a lower value. Now we transform the value 1:1. * 8.0.0 - Added OpenMetrics exposition format support (#388). From 71a8668d5dd7a13f1acc71f803e8a5c7b1a925c3 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Sat, 28 Oct 2023 03:48:49 +0300 Subject: [PATCH 113/230] Add support for capturing HttpClient metrics from all registered HttpClients (`services.UseHttpClientMetrics()`). --- History | 2 + Prometheus/HttpClientMetricsExtensions.cs | 47 +++++++++++++++++++++++ README.md | 8 ++-- Resources/SolutionAssemblyInfo.cs | 2 +- Sample.Web/Program.cs | 5 ++- 5 files changed, 59 insertions(+), 5 deletions(-) diff --git a/History b/History index db61c432..0aaed18b 100644 --- a/History +++ b/History @@ -1,3 +1,5 @@ +* 8.1.0 +- Add support for capturing HttpClient metrics from all registered HttpClients (`services.UseHttpClientMetrics()`). * 8.0.1 - Allow ObservableCounter to be reset. Previously, the MeterAdapter output got stuck on its previous maximum if the underlying Meter reset its value to a lower value. Now we transform the value 1:1. * 8.0.0 diff --git a/Prometheus/HttpClientMetricsExtensions.cs b/Prometheus/HttpClientMetricsExtensions.cs index 8ca20250..ca0c22ab 100644 --- a/Prometheus/HttpClientMetricsExtensions.cs +++ b/Prometheus/HttpClientMetricsExtensions.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http; using Prometheus.HttpClientMetrics; namespace Prometheus @@ -50,5 +51,51 @@ public static IHttpClientBuilder UseHttpClientMetrics(this IHttpClientBuilder bu return builder; } + + /// + /// Configures the HttpMessageHandler pipeline to collect Prometheus metrics. + /// + public static HttpMessageHandlerBuilder UseHttpClientMetrics(this HttpMessageHandlerBuilder builder, HttpClientExporterOptions? options = null) + { + options ??= new HttpClientExporterOptions(); + + var identity = new HttpClientIdentity(builder.Name); + + if (options.InProgress.Enabled) + { + builder.AdditionalHandlers.Add(new HttpClientInProgressHandler(options.InProgress, identity)); + } + + if (options.RequestCount.Enabled) + { + builder.AdditionalHandlers.Add(new HttpClientRequestCountHandler(options.RequestCount, identity)); + } + + if (options.RequestDuration.Enabled) + { + builder.AdditionalHandlers.Add(new HttpClientRequestDurationHandler(options.RequestDuration, identity)); + } + + if (options.ResponseDuration.Enabled) + { + builder.AdditionalHandlers.Add(new HttpClientResponseDurationHandler(options.ResponseDuration, identity)); + } + + return builder; + } + + /// + /// Configures the service container to collect Prometheus metrics from all registered HttpClients. + /// + public static IServiceCollection UseHttpClientMetrics(this IServiceCollection services, HttpClientExporterOptions? options = null) + { + return services.ConfigureAll((HttpClientFactoryOptions optionsToConfigure) => + { + optionsToConfigure.HttpMessageHandlerBuilderActions.Add(builder => + { + builder.UseHttpClientMetrics(options); + }); + }); + } } } \ No newline at end of file diff --git a/README.md b/README.md index cbf0faaf..758e3aa0 100644 --- a/README.md +++ b/README.md @@ -643,20 +643,22 @@ The exposed metrics include: * Duration of HTTP client requests (from start of request to end of reading response headers). * Duration of HTTP client responses (from start of request to end of reading response body). -Example `Startup.cs` modification to enable these metrics: +Example `Startup.cs` modification to enable these metrics for all HttpClients registered in the service collection: ```csharp public void ConfigureServices(IServiceCollection services) { // ... - services.AddHttpClient(Options.DefaultName) - .UseHttpClientMetrics(); + services.UseHttpClientMetrics(); // ... } ``` +> **Note** +> You can also register HTTP client metrics only for a specific HttpClient by calling `services.AddHttpClient(...).UseHttpClientMetrics()`. + See also, [Sample.Web](Sample.Web/Program.cs). # ASP.NET Core health check status metrics diff --git a/Resources/SolutionAssemblyInfo.cs b/Resources/SolutionAssemblyInfo.cs index 63602934..5083f910 100644 --- a/Resources/SolutionAssemblyInfo.cs +++ b/Resources/SolutionAssemblyInfo.cs @@ -2,7 +2,7 @@ using System.Runtime.CompilerServices; // This is the real version number, used in NuGet packages and for display purposes. -[assembly: AssemblyFileVersion("8.0.1")] +[assembly: AssemblyFileVersion("8.1.0")] // Only use major version here, with others kept at zero, for correct assembly binding logic. [assembly: AssemblyVersion("8.0.0")] diff --git a/Sample.Web/Program.cs b/Sample.Web/Program.cs index 8668aacb..9eff1f3c 100644 --- a/Sample.Web/Program.cs +++ b/Sample.Web/Program.cs @@ -12,7 +12,10 @@ builder.Services.AddRazorPages(); // Define an HTTP client that reports metrics about its usage, to be used by a sample background service. -builder.Services.AddHttpClient(SampleService.HttpClientName).UseHttpClientMetrics(); +builder.Services.AddHttpClient(SampleService.HttpClientName); + +// Export metrics from all HTTP clients registered in services +builder.Services.UseHttpClientMetrics(); // A sample service that uses the above HTTP client. builder.Services.AddHostedService(); From 123a55d0d5e0deaeac77f887495e8324591ec2b3 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Sun, 29 Oct 2023 11:13:19 +0200 Subject: [PATCH 114/230] Clarify "in progress" comments --- Prometheus/HttpClientMetrics/HttpClientInProgressHandler.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Prometheus/HttpClientMetrics/HttpClientInProgressHandler.cs b/Prometheus/HttpClientMetrics/HttpClientInProgressHandler.cs index a3c908ab..234535d3 100644 --- a/Prometheus/HttpClientMetrics/HttpClientInProgressHandler.cs +++ b/Prometheus/HttpClientMetrics/HttpClientInProgressHandler.cs @@ -11,6 +11,7 @@ protected override async Task SendAsync(HttpRequestMessage { using (CreateChild(request, null).TrackInProgress()) { + // Returns when the response HEADERS are seen. return await base.SendAsync(request, cancellationToken); } } From 27d727c5771c20a686906f86d3a0905b6ff6bdb1 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Sun, 29 Oct 2023 11:14:41 +0200 Subject: [PATCH 115/230] Clarify client HTTP request in progress tracking end --- Prometheus/HttpClientMetrics/HttpClientInProgressHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Prometheus/HttpClientMetrics/HttpClientInProgressHandler.cs b/Prometheus/HttpClientMetrics/HttpClientInProgressHandler.cs index 234535d3..e539c2e6 100644 --- a/Prometheus/HttpClientMetrics/HttpClientInProgressHandler.cs +++ b/Prometheus/HttpClientMetrics/HttpClientInProgressHandler.cs @@ -20,7 +20,7 @@ protected override async Task SendAsync(HttpRequestMessage protected override ICollector CreateMetricInstance(string[] labelNames) => MetricFactory.CreateGauge( "httpclient_requests_in_progress", - "Number of requests currently being executed by an HttpClient.", + "Number of requests currently being executed by an HttpClient that have not yet received response headers. Value is decremented once response headers are received.", labelNames); } } \ No newline at end of file From 5d661e8537538408ef77594ee6ac5682c8e084f4 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Mon, 20 Nov 2023 06:19:43 +0200 Subject: [PATCH 116/230] Fix .NET 8 SDK build break --- Prometheus.NetFramework.AspNet/Usings.cs | 1 + Prometheus/Usings.cs | 1 + 2 files changed, 2 insertions(+) create mode 100644 Prometheus.NetFramework.AspNet/Usings.cs create mode 100644 Prometheus/Usings.cs diff --git a/Prometheus.NetFramework.AspNet/Usings.cs b/Prometheus.NetFramework.AspNet/Usings.cs new file mode 100644 index 00000000..e6a9eb40 --- /dev/null +++ b/Prometheus.NetFramework.AspNet/Usings.cs @@ -0,0 +1 @@ +global using System.Net.Http; \ No newline at end of file diff --git a/Prometheus/Usings.cs b/Prometheus/Usings.cs new file mode 100644 index 00000000..e6a9eb40 --- /dev/null +++ b/Prometheus/Usings.cs @@ -0,0 +1 @@ +global using System.Net.Http; \ No newline at end of file From b533d7ab1b80022ac3c4bc10a615750fc379da9c Mon Sep 17 00:00:00 2001 From: Utkarsh Umesan Pillai <66651184+utpilla@users.noreply.github.com> Date: Mon, 20 Nov 2023 00:10:26 -0800 Subject: [PATCH 117/230] Update Sdk comparison benchmarks (#447) --- Benchmark.NetCore/Benchmark.NetCore.csproj | 4 +- Benchmark.NetCore/SdkComparisonBenchmarks.cs | 192 ++++++++++++------- 2 files changed, 123 insertions(+), 73 deletions(-) diff --git a/Benchmark.NetCore/Benchmark.NetCore.csproj b/Benchmark.NetCore/Benchmark.NetCore.csproj index 5562708a..bfabcdc2 100644 --- a/Benchmark.NetCore/Benchmark.NetCore.csproj +++ b/Benchmark.NetCore/Benchmark.NetCore.csproj @@ -24,9 +24,9 @@ - + - + diff --git a/Benchmark.NetCore/SdkComparisonBenchmarks.cs b/Benchmark.NetCore/SdkComparisonBenchmarks.cs index e0c88245..65d18734 100644 --- a/Benchmark.NetCore/SdkComparisonBenchmarks.cs +++ b/Benchmark.NetCore/SdkComparisonBenchmarks.cs @@ -1,11 +1,29 @@ -using System.Diagnostics; -using System.Diagnostics.Metrics; +using System.Diagnostics.Metrics; using BenchmarkDotNet.Attributes; using OpenTelemetry.Metrics; using Prometheus; namespace Benchmark.NetCore; +/* +BenchmarkDotNet v0.13.10, Windows 11 (10.0.23424.1000) +Intel Core i7-9700 CPU 3.00GHz, 1 CPU, 8 logical and 8 physical cores +.NET SDK 8.0.100-rc.2.23502.2 + [Host] : .NET 7.0.13 (7.0.1323.51816), X64 RyuJIT AVX2 + DefaultJob : .NET 7.0.13 (7.0.1323.51816), X64 RyuJIT AVX2 + Job-AGCLMW : .NET 7.0.13 (7.0.1323.51816), X64 RyuJIT AVX2 + + +| Method | Job | MaxIterationCount | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | +|------------------------------ |----------- |------------------ |------------:|------------:|------------:|---------:|---------:|----------:| +| PromNetCounter | DefaultJob | Default | 771.5 us | 5.54 us | 4.91 us | - | - | 1 B | +| PromNetHistogram | DefaultJob | Default | 2,747.5 us | 27.86 us | 26.06 us | - | - | 3 B | +| OTelCounter | DefaultJob | Default | 14,470.8 us | 54.28 us | 48.12 us | - | - | 12 B | +| OTelHistogram | DefaultJob | Default | 15,856.9 us | 193.51 us | 181.01 us | - | - | 25 B | +| PromNetHistogramForAdHocLabel | Job-AGCLMW | 16 | 8,804.0 us | 1,083.49 us | 1,013.49 us | 500.0000 | 234.3750 | 3184062 B | +| OTelHistogramForAdHocLabel | Job-AGCLMW | 16 | 580.8 us | 6.08 us | 5.69 us | 14.6484 | - | 96001 B | +*/ + /// /// We compare pure measurement (not serializing the data) with prometheus-net SDK and OpenTelemetry .NET SDK. /// @@ -14,6 +32,7 @@ namespace Benchmark.NetCore; /// * Metrics are initialized once on application startup. /// * Metrics typically measure "sessions" - there are sets of metrics that are related through shared identifiers and a shared lifetime (e.g. HTTP request), /// with all the identifiers for the metrics created when the sesison is initialized (e.g. when the HTTP connection is established). +/// * Metrics typically are also used report SLI (Service Level Indicator); these involve emitting a lot of unique dimension values, for example: CustomerId. /// /// Excluded from measurement: /// * Meter setup (because meters are created once on application setup and not impactful later). @@ -27,9 +46,6 @@ namespace Benchmark.NetCore; [MemoryDiagnoser] public class SdkComparisonBenchmarks { - private const int CounterCount = 100; - private const int HistogramCount = 100; - // Unique sets of label/tag values per metric. You can think of each one as a "session" we are reporting data for. private const int TimeseriesPerMetric = 100; @@ -48,15 +64,6 @@ static SdkComparisonBenchmarks() SessionIds[i] = Guid.NewGuid().ToString(); } - [Params(MetricsSdk.PrometheusNet, MetricsSdk.OpenTelemetry)] - public MetricsSdk Sdk { get; set; } - - public enum MetricsSdk - { - PrometheusNet, - OpenTelemetry - } - /// /// Contains all the context that gets initialized at iteration setup time. /// @@ -76,13 +83,21 @@ private abstract class MetricsContext : IDisposable /// public abstract void ObserveHistogram(double value); + /// + /// Records an observation with one random label value as ad-hoc using a Histogram. + /// + public abstract void ObserveHistogramWithAnAdHocLabelValue(double value); + public virtual void Dispose() { } } private sealed class PrometheusNetMetricsContext : MetricsContext { - private readonly List _counterInstances = new(CounterCount * TimeseriesPerMetric); - private readonly List _histogramInstances = new(HistogramCount * TimeseriesPerMetric); + private readonly List _counterInstances = new(TimeseriesPerMetric); + private readonly List _histogramInstances = new(TimeseriesPerMetric); + private readonly Histogram _histogramForAdHocLabels; + + private readonly KestrelMetricServer _server; public PrometheusNetMetricsContext() { @@ -92,21 +107,22 @@ public PrometheusNetMetricsContext() // Do not emit any exemplars in this benchmark, as they are not yet equally supported by the SDKs. factory.ExemplarBehavior = ExemplarBehavior.NoExemplars(); - for (var counterIndex = 0; counterIndex < CounterCount; counterIndex++) - { - var counter = factory.CreateCounter("counter_" + counterIndex, "", LabelNames); + var counter = factory.CreateCounter("counter", "", LabelNames); - for (var i = 0; i < TimeseriesPerMetric; i++) - _counterInstances.Add(counter.WithLabels(Label1Value, Label2Value, SessionIds[i])); - } + for (var i = 0; i < TimeseriesPerMetric; i++) + _counterInstances.Add(counter.WithLabels(Label1Value, Label2Value, SessionIds[i])); - for (var histogramIndex = 0; histogramIndex < HistogramCount; histogramIndex++) - { - var histogram = factory.CreateHistogram("histogram_" + histogramIndex, "", LabelNames); + var histogram = factory.CreateHistogram("histogram", "", LabelNames); - for (var i = 0; i < TimeseriesPerMetric; i++) - _histogramInstances.Add(histogram.WithLabels(Label1Value, Label2Value, SessionIds[i])); - } + _histogramForAdHocLabels = factory.CreateHistogram("histogramForAdHocLabels", "", LabelNames); + + for (var i = 0; i < TimeseriesPerMetric; i++) + _histogramInstances.Add(histogram.WithLabels(Label1Value, Label2Value, SessionIds[i])); + + // `AddPrometheusHttpListener` of OpenTelemetry creates an HttpListener. + // Start a listener/server for Proemetheus Benchmarks for a fair comparison. + _server = new KestrelMetricServer(port: 1234); + _server.Start(); } public override void ObserveCounter(double value) @@ -120,6 +136,18 @@ public override void ObserveHistogram(double value) foreach (var histogram in _histogramInstances) histogram.Observe(value); } + + public override void ObserveHistogramWithAnAdHocLabelValue(double value) + { + _histogramForAdHocLabels.WithLabels(Label1Value, Label2Value, Guid.NewGuid().ToString()).Observe(value); + } + + public override void Dispose() + { + base.Dispose(); + + _server.Dispose(); + } } private sealed class OpenTelemetryMetricsContext : MetricsContext @@ -129,10 +157,9 @@ private sealed class OpenTelemetryMetricsContext : MetricsContext private readonly Meter _meter; private readonly MeterProvider _provider; - private readonly List> _counters = new(CounterCount); - private readonly List> _histograms = new(HistogramCount); - - private readonly List _sessions = new(TimeseriesPerMetric); + private readonly Counter _counter; + private readonly Histogram _histogram; + private readonly Histogram _histogramForAdHocLabels; public OpenTelemetryMetricsContext() { @@ -140,44 +167,47 @@ public OpenTelemetryMetricsContext() // at least for the "setup" benchmark which keeps getting slower every time we call it with the same metric name. _meter = new Meter(MeterBaseName + Guid.NewGuid()); + _counter = _meter.CreateCounter("counter"); + + _histogram = _meter.CreateHistogram("histogram"); + + _histogramForAdHocLabels = _meter.CreateHistogram("histogramForAdHocLabels"); + _provider = OpenTelemetry.Sdk.CreateMeterProviderBuilder() - .AddPrometheusExporter() + .AddView("histogram", new OpenTelemetry.Metrics.HistogramConfiguration() { RecordMinMax = false}) .AddMeter(_meter.Name) + .AddPrometheusHttpListener() .Build(); + } - for (var i = 0; i < CounterCount; i++) - _counters.Add(_meter.CreateCounter("counter_" + i)); - - for (var i = 0; i < HistogramCount; i++) - _histograms.Add(_meter.CreateHistogram("histogram_" + i)); - - for (var i = 0; i < TimeseriesPerMetric; i++) + public override void ObserveCounter(double value) + { + for (int i = 0; i < SessionIds.Length; i++) { var tag1 = new KeyValuePair(LabelNames[0], Label1Value); var tag2 = new KeyValuePair(LabelNames[1], Label2Value); var tag3 = new KeyValuePair(LabelNames[2], SessionIds[i]); - - var tagList = new TagList(new[] { tag1, tag2, tag3 }); - _sessions.Add(tagList); + _counter.Add(value, tag1, tag2, tag3); } } - public override void ObserveCounter(double value) + public override void ObserveHistogram(double value) { - foreach (var session in _sessions) + for (int i = 0; i < SessionIds.Length; i++) { - foreach (var counter in _counters) - counter.Add(value, session); + var tag1 = new KeyValuePair(LabelNames[0], Label1Value); + var tag2 = new KeyValuePair(LabelNames[1], Label2Value); + var tag3 = new KeyValuePair(LabelNames[2], SessionIds[i]); + _histogram.Record(value, tag1, tag2, tag3); } } - public override void ObserveHistogram(double value) + public override void ObserveHistogramWithAnAdHocLabelValue(double value) { - foreach (var session in _sessions) - { - foreach (var histogram in _histograms) - histogram.Record(value, session); - } + var tag1 = new KeyValuePair(LabelNames[0], Label1Value); + var tag2 = new KeyValuePair(LabelNames[1], Label2Value); + var tag3 = new KeyValuePair(LabelNames[2], Guid.NewGuid().ToString()); + _histogramForAdHocLabels.Record(value, tag1, tag2, tag3); } public override void Dispose() @@ -190,45 +220,65 @@ public override void Dispose() private MetricsContext _context; - [IterationSetup] - public void Setup() + [GlobalSetup(Targets = new string[] {nameof(OTelCounter), nameof(OTelHistogram), nameof(OTelHistogramForAdHocLabel)})] + public void OpenTelemetrySetup() { - _context = Sdk switch - { - MetricsSdk.PrometheusNet => new PrometheusNetMetricsContext(), - MetricsSdk.OpenTelemetry => new OpenTelemetryMetricsContext(), - _ => throw new NotImplementedException(), - }; + _context = new OpenTelemetryMetricsContext(); + } + + [GlobalSetup(Targets = new string[] { nameof(PromNetCounter), nameof(PromNetHistogram), nameof(PromNetHistogramForAdHocLabel) })] + public void PrometheusNetSetup() + { + _context = new PrometheusNetMetricsContext(); } [Benchmark] - public void CounterMeasurements() + public void PromNetCounter() { for (var observation = 0; observation < ObservationCount; observation++) _context.ObserveCounter(observation); } [Benchmark] - public void HistogramMeasurements() + public void PromNetHistogram() { for (var observation = 0; observation < ObservationCount; observation++) _context.ObserveHistogram(observation); } - [IterationCleanup] - public void Cleanup() + [Benchmark] + [MaxIterationCount(16)] // Need to set a lower iteration count as this benchmarks allocates a lot memory and takes too long to complete with the default number of iterations. + public void PromNetHistogramForAdHocLabel() { - _context.Dispose(); + for (var observation = 0; observation < ObservationCount; observation++) + _context.ObserveHistogramWithAnAdHocLabelValue(observation); } [Benchmark] - public void SetupBenchmark() + public void OTelCounter() { - // Here we just do the setup again, but this time as part of the measured data set, to compare the setup cost between SDKs. + for (var observation = 0; observation < ObservationCount; observation++) + _context.ObserveCounter(observation); + } - // We need to dispose of the automatically created context, in case there are any SDK-level singleton resources (which we do not want to accidentally reuse). - _context.Dispose(); + [Benchmark] + public void OTelHistogram() + { + for (var observation = 0; observation < ObservationCount; observation++) + _context.ObserveHistogram(observation); + } - Setup(); + [Benchmark] + [MaxIterationCount(16)] // Set the same number of iteration count as the corresponding PromNet benchmark. + public void OTelHistogramForAdHocLabel() + { + for (var observation = 0; observation < ObservationCount; observation++) + _context.ObserveHistogramWithAnAdHocLabelValue(observation); + } + + [GlobalCleanup] + public void Cleanup() + { + _context.Dispose(); } } From d762f303c5f59eb8f6a90399c86c0744cc141dce Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Mon, 20 Nov 2023 10:11:15 +0200 Subject: [PATCH 118/230] Listen on 127.0.0.1:0 in benchmarks to avoid firewall prompts --- Benchmark.NetCore/SdkComparisonBenchmarks.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Benchmark.NetCore/SdkComparisonBenchmarks.cs b/Benchmark.NetCore/SdkComparisonBenchmarks.cs index 65d18734..7a1e0fd0 100644 --- a/Benchmark.NetCore/SdkComparisonBenchmarks.cs +++ b/Benchmark.NetCore/SdkComparisonBenchmarks.cs @@ -120,7 +120,9 @@ public PrometheusNetMetricsContext() _histogramInstances.Add(histogram.WithLabels(Label1Value, Label2Value, SessionIds[i])); // `AddPrometheusHttpListener` of OpenTelemetry creates an HttpListener. - // Start a listener/server for Proemetheus Benchmarks for a fair comparison. + // Start a listener/server for Prometheus Benchmarks for a fair comparison. + // We listen on 127.0.0.1: to avoid firewall prompts (we do not expect to receive any traffic). + _server = new KestrelMetricServer("127.0.0.1", port: 0); _server = new KestrelMetricServer(port: 1234); _server.Start(); } @@ -164,7 +166,7 @@ private sealed class OpenTelemetryMetricsContext : MetricsContext public OpenTelemetryMetricsContext() { // We use a randomized name every time because otherwise there appears to be some "shared state" between benchmark invocations, - // at least for the "setup" benchmark which keeps getting slower every time we call it with the same metric name. + // at least for the "setup" part which keeps getting slower every time we call it with the same metric name. _meter = new Meter(MeterBaseName + Guid.NewGuid()); _counter = _meter.CreateCounter("counter"); From 953b5580879df33a737551c9408f6f9f57b945a9 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Mon, 20 Nov 2023 10:30:58 +0200 Subject: [PATCH 119/230] Benchmarks on .NET 8 --- Benchmark.NetCore/Benchmark.NetCore.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Benchmark.NetCore/Benchmark.NetCore.csproj b/Benchmark.NetCore/Benchmark.NetCore.csproj index bfabcdc2..00958443 100644 --- a/Benchmark.NetCore/Benchmark.NetCore.csproj +++ b/Benchmark.NetCore/Benchmark.NetCore.csproj @@ -2,7 +2,7 @@ Exe - net7.0 + net8.0 true ..\Resources\prometheus-net.snk From 5361f0ea7321e9cae1a9ab8e2a12f2d3378b756c Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Mon, 20 Nov 2023 11:50:18 +0200 Subject: [PATCH 120/230] Update benchmarks in readme --- Benchmark.NetCore/MeasurementBenchmarks.cs | 2 +- Benchmark.NetCore/SdkComparisonBenchmarks.cs | 32 +++++++++--------- Docs/MeasurementsBenchmarks.xlsx | Bin 10322 -> 10331 bytes Docs/SdkComparison-MeasurementCpuUsage.png | Bin 29189 -> 0 bytes Docs/SdkComparison-MeasurementMemoryUsage.png | Bin 29872 -> 0 bytes Docs/SdkComparison-SetupCpuUsage.png | Bin 26945 -> 0 bytes Docs/SdkComparison-SetupMemoryUsage.png | Bin 25448 -> 0 bytes Docs/SdkPerformanceComparison.xlsx | Bin 27721 -> 0 bytes README.md | 32 +++++++++++------- 9 files changed, 37 insertions(+), 29 deletions(-) delete mode 100644 Docs/SdkComparison-MeasurementCpuUsage.png delete mode 100644 Docs/SdkComparison-MeasurementMemoryUsage.png delete mode 100644 Docs/SdkComparison-SetupCpuUsage.png delete mode 100644 Docs/SdkComparison-SetupMemoryUsage.png delete mode 100644 Docs/SdkPerformanceComparison.xlsx diff --git a/Benchmark.NetCore/MeasurementBenchmarks.cs b/Benchmark.NetCore/MeasurementBenchmarks.cs index cbef34b1..9e640b21 100644 --- a/Benchmark.NetCore/MeasurementBenchmarks.cs +++ b/Benchmark.NetCore/MeasurementBenchmarks.cs @@ -29,7 +29,7 @@ public enum MetricType [Params(1, 16)] public int ThreadCount { get; set; } - [Params(MetricType.Counter, /*MetricType.Gauge,*/ MetricType.Histogram/*, MetricType.Summary*/)] + [Params(MetricType.Counter, MetricType.Gauge, MetricType.Histogram, MetricType.Summary)] public MetricType TargetMetricType { get; set; } [Params(ExemplarMode.Auto, ExemplarMode.None, ExemplarMode.Provided)] diff --git a/Benchmark.NetCore/SdkComparisonBenchmarks.cs b/Benchmark.NetCore/SdkComparisonBenchmarks.cs index 7a1e0fd0..b8162fe3 100644 --- a/Benchmark.NetCore/SdkComparisonBenchmarks.cs +++ b/Benchmark.NetCore/SdkComparisonBenchmarks.cs @@ -6,22 +6,22 @@ namespace Benchmark.NetCore; /* -BenchmarkDotNet v0.13.10, Windows 11 (10.0.23424.1000) -Intel Core i7-9700 CPU 3.00GHz, 1 CPU, 8 logical and 8 physical cores -.NET SDK 8.0.100-rc.2.23502.2 - [Host] : .NET 7.0.13 (7.0.1323.51816), X64 RyuJIT AVX2 - DefaultJob : .NET 7.0.13 (7.0.1323.51816), X64 RyuJIT AVX2 - Job-AGCLMW : .NET 7.0.13 (7.0.1323.51816), X64 RyuJIT AVX2 - - -| Method | Job | MaxIterationCount | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | -|------------------------------ |----------- |------------------ |------------:|------------:|------------:|---------:|---------:|----------:| -| PromNetCounter | DefaultJob | Default | 771.5 us | 5.54 us | 4.91 us | - | - | 1 B | -| PromNetHistogram | DefaultJob | Default | 2,747.5 us | 27.86 us | 26.06 us | - | - | 3 B | -| OTelCounter | DefaultJob | Default | 14,470.8 us | 54.28 us | 48.12 us | - | - | 12 B | -| OTelHistogram | DefaultJob | Default | 15,856.9 us | 193.51 us | 181.01 us | - | - | 25 B | -| PromNetHistogramForAdHocLabel | Job-AGCLMW | 16 | 8,804.0 us | 1,083.49 us | 1,013.49 us | 500.0000 | 234.3750 | 3184062 B | -| OTelHistogramForAdHocLabel | Job-AGCLMW | 16 | 580.8 us | 6.08 us | 5.69 us | 14.6484 | - | 96001 B | +BenchmarkDotNet v0.13.10, Windows 11 (10.0.22621.2715/22H2/2022Update/SunValley2) +AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores +.NET SDK 8.0.100 + [Host] : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 + DefaultJob : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 + Job-IZHPUA : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 + + +| Method | Job | MaxIterationCount | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | +|------------------------------ |----------- |------------------ |------------:|----------:|----------:|---------:|---------:|----------:| +| PromNetCounter | DefaultJob | Default | 232.0 us | 1.90 us | 1.78 us | - | - | - | +| PromNetHistogram | DefaultJob | Default | 1,200.4 us | 8.11 us | 7.19 us | - | - | 2 B | +| OTelCounter | DefaultJob | Default | 10,879.7 us | 47.76 us | 44.67 us | - | - | 11 B | +| OTelHistogram | DefaultJob | Default | 12,310.7 us | 57.24 us | 50.75 us | - | - | 24 B | +| PromNetHistogramForAdHocLabel | Job-IZHPUA | 16 | 5,765.7 us | 372.36 us | 330.09 us | 187.5000 | 171.8750 | 3184106 B | +| OTelHistogramForAdHocLabel | Job-IZHPUA | 16 | 348.7 us | 3.01 us | 2.67 us | 5.3711 | - | 96000 B | */ /// diff --git a/Docs/MeasurementsBenchmarks.xlsx b/Docs/MeasurementsBenchmarks.xlsx index 524f34d48ef2a813a6b227a9d3045e93c6e8109b..aeefba7a901bd6ed7ae65b0b8789acb8095c3d75 100644 GIT binary patch delta 5614 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 delta 5655 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 diff --git a/Docs/SdkComparison-MeasurementMemoryUsage.png b/Docs/SdkComparison-MeasurementMemoryUsage.png deleted file mode 100644 index e3c7ff1a94d83170df8c906263c5365356e4fb53..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 29872 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 P6^m2HBSX4_fB5-dFoi;- diff --git a/Docs/SdkComparison-SetupMemoryUsage.png b/Docs/SdkComparison-SetupMemoryUsage.png deleted file mode 100644 index 0c0ecc34f91c18146ac327a820b703995dd51559..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25448 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 ZU#>?Tw}^oe4GN_SzMsKqb~Tav>t7yI)F=P| diff --git a/README.md b/README.md index 758e3aa0..68af2757 100644 --- a/README.md +++ b/README.md @@ -877,27 +877,35 @@ As an example of the performance of measuring data using prometheus-net, we have | Metric type | Concurrency | Measurements per second | |-------------|------------:|------------------------:| -| Gauge | 1 thread | 521 million | -| Counter | 1 thread | 87 million | -| Histogram | 1 thread | 46 million | +| Gauge | 1 thread | 519 million | +| Counter | 1 thread | 134 million | +| Histogram | 1 thread | 69 million | | Summary | 1 thread | 2 million | -| Gauge | 16 threads | 55 million | -| Counter | 16 threads | 10 million | +| Gauge | 16 threads | 53 million | +| Counter | 16 threads | 9 million | | Histogram | 16 threads | 14 million | | Summary | 16 threads | 2 million | > **Note** > All measurements on all threads are recorded by the same metric instance, for maximum stress and concurrent load. In real-world apps with the load spread across multiple metrics, you can expect even better performance. -Another popular .NET SDK with Prometheus support is the OpenTelemetry SDK. To help you choose, we have [SdkComparisonBenchmarks.cs](Benchmark.NetCore/SdkComparisonBenchmarks.cs) to compare the two SDKs and give some idea of how they differer in the performance tradeoffs made. Both SDKs are evaluated in single-threaded mode under a comparable workload and enabled feature set. +Another popular .NET SDK with Prometheus support is the OpenTelemetry SDK. To help you choose, we have [SdkComparisonBenchmarks.cs](Benchmark.NetCore/SdkComparisonBenchmarks.cs) to compare the two SDKs and give some idea of how they differer in the performance tradeoffs made. Both SDKs are evaluated in single-threaded mode under a comparable workload and enabled feature set. A representative result is here: -![](Docs/SdkComparison-MeasurementCpuUsage.png) -![](Docs/SdkComparison-MeasurementMemoryUsage.png) -![](Docs/SdkComparison-SetupCpuUsage.png) -![](Docs/SdkComparison-SetupMemoryUsage.png) +```text +BenchmarkDotNet v0.13.10, Windows 11 (10.0.22621.2715/22H2/2022Update/SunValley2) +AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores + [Host] : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 -> **Note** -> As authors of this SDK are not necessarily experts in use of other SDKs, please feel free to submit pull requests with benchmark improvements to better highlight the strengths or weaknesses here. + +| Method | Mean | Gen0 | Gen1 | Allocated | +|------------------------------ |------------:|---------:|---------:|----------:| +| PromNetCounter | 232.0 us | - | - | - | +| OTelCounter | 10,879.7 us | - | - | 11 B | +| PromNetHistogram | 1,200.4 us | - | - | 2 B | +| OTelHistogram | 12,310.7 us | - | - | 24 B | +| PromNetHistogramForAdHocLabel | 5,765.7 us | 187.5000 | 171.8750 | 3184106 B | +| OTelHistogramForAdHocLabel | 348.7 us | 5.3711 | - | 96000 B | +``` # Community projects From d99863ba57bca1c6aac8486928a343692479125f Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Mon, 20 Nov 2023 11:51:19 +0200 Subject: [PATCH 121/230] fix copy-paste error in benchmarks --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 68af2757..8ea6adfe 100644 --- a/README.md +++ b/README.md @@ -883,7 +883,7 @@ As an example of the performance of measuring data using prometheus-net, we have | Summary | 1 thread | 2 million | | Gauge | 16 threads | 53 million | | Counter | 16 threads | 9 million | -| Histogram | 16 threads | 14 million | +| Histogram | 16 threads | 5 million | | Summary | 16 threads | 2 million | > **Note** From 9e0fdb5d3b0b4e232a0d7fe88708fe463b3d66bf Mon Sep 17 00:00:00 2001 From: Mikhail Dubovik Date: Tue, 28 Nov 2023 15:35:36 +0400 Subject: [PATCH 122/230] Update MeterAdapter.cs (#453) fix: add labelValues to UpDownCounter handle --- Prometheus/MeterAdapter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Prometheus/MeterAdapter.cs b/Prometheus/MeterAdapter.cs index 2c545d00..1b48418e 100644 --- a/Prometheus/MeterAdapter.cs +++ b/Prometheus/MeterAdapter.cs @@ -159,7 +159,7 @@ private void OnMeasurementRecorded(Instrument instrument, T measurement, Read var handle = _factory.CreateGauge(_instrumentPrometheusNames[instrument], _instrumentPrometheusHelp[instrument], labelNames); // A measurement is the increment. - handle.WithLease(x => x.Inc(value)); + handle.WithLease(x => x.Inc(value), labelValues); } #endif else if (instrument is ObservableGauge @@ -289,4 +289,4 @@ private static string TranslateInstrumentDescriptionToPrometheusHelp(Instrument return sb.ToString(); } } -#endif \ No newline at end of file +#endif From 17bb2a0aa5364527e7c22510e86acddd1354cec2 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Tue, 28 Nov 2023 13:38:27 +0200 Subject: [PATCH 123/230] 8.1.1 Fix bug in .NET Meters API adapter for UpDownCounter, which was incorrectly transformed to Prometheus metrics. #452 and #453 --- History | 2 ++ Resources/SolutionAssemblyInfo.cs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/History b/History index 0aaed18b..e0c536aa 100644 --- a/History +++ b/History @@ -1,3 +1,5 @@ +* 8.1.1 +- Fix bug in .NET Meters API adapter for UpDownCounter, which was incorrectly transformed to Prometheus metrics. #452 and #453 * 8.1.0 - Add support for capturing HttpClient metrics from all registered HttpClients (`services.UseHttpClientMetrics()`). * 8.0.1 diff --git a/Resources/SolutionAssemblyInfo.cs b/Resources/SolutionAssemblyInfo.cs index 5083f910..77bbc325 100644 --- a/Resources/SolutionAssemblyInfo.cs +++ b/Resources/SolutionAssemblyInfo.cs @@ -2,7 +2,7 @@ using System.Runtime.CompilerServices; // This is the real version number, used in NuGet packages and for display purposes. -[assembly: AssemblyFileVersion("8.1.0")] +[assembly: AssemblyFileVersion("8.1.1")] // Only use major version here, with others kept at zero, for correct assembly binding logic. [assembly: AssemblyVersion("8.0.0")] From 973e50e134cb75c7e02707bc43f558170607bb31 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 23 Nov 2023 09:27:21 +0200 Subject: [PATCH 124/230] Reduce string allocations in TextSerializer --- Prometheus/TextSerializer.cs | 37 ++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/Prometheus/TextSerializer.cs b/Prometheus/TextSerializer.cs index 77663274..6268c10f 100644 --- a/Prometheus/TextSerializer.cs +++ b/Prometheus/TextSerializer.cs @@ -277,6 +277,42 @@ internal static CanonicalLabel EncodeValueAsCanonicalLabel(byte[] name, double v if (double.IsPositiveInfinity(value)) return new CanonicalLabel(name, PositiveInfinity, PositiveInfinity); +#if NET + Span buffer = stackalloc char[128]; + + if (!value.TryFormat(buffer, out var charsWritten, "g", CultureInfo.InvariantCulture)) + throw new Exception("Failed to encode floating point value as string."); + + var prometheusChars = buffer[0..charsWritten]; + + var prometheusByteCount = PrometheusConstants.ExportEncoding.GetByteCount(prometheusChars); + var prometheusBytes = new byte[prometheusByteCount]; + + if (PrometheusConstants.ExportEncoding.GetBytes(prometheusChars, prometheusBytes) != prometheusByteCount) + throw new Exception("Internal error: counting the same bytes twice got us a different value."); + + var openMetricsByteCount = prometheusByteCount; + byte[] openMetricsBytes; + + // Identify whether the written characters are expressed as floating-point, by checking for presence of the 'e' or '.' characters. + if (prometheusChars.IndexOfAny(DotEChar) == -1) + { + // Prometheus defaults to integer-formatting without a decimal point, if possible. + // OpenMetrics requires labels containing numeric values to be expressed in floating point format. + // If all we find is an integer, we add a ".0" to the end to make it a floating point value. + openMetricsByteCount += 2; + + openMetricsBytes = new byte[openMetricsByteCount]; + Array.Copy(prometheusBytes, openMetricsBytes, prometheusByteCount); + Array.Copy(DotZero, 0, openMetricsBytes, prometheusByteCount, DotZero.Length); + } + else + { + // It is already a floating-point value in Prometheus representation - reuse same bytes for OpenMetrics. + openMetricsBytes = prometheusBytes; + } + +#else var valueAsString = value.ToString("g", CultureInfo.InvariantCulture); var prometheusBytes = PrometheusConstants.ExportEncoding.GetBytes(valueAsString); @@ -289,6 +325,7 @@ internal static CanonicalLabel EncodeValueAsCanonicalLabel(byte[] name, double v // If all we find is an integer, we add a ".0" to the end to make it a floating point value. openMetricsBytes = PrometheusConstants.ExportEncoding.GetBytes(valueAsString + ".0"); } +#endif return new CanonicalLabel(name, prometheusBytes, openMetricsBytes); } From 03a396fdcb565c5cc75c18a6615b6eda0ad3bd69 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 23 Nov 2023 09:28:21 +0200 Subject: [PATCH 125/230] 32 should be enough for a value of type double --- Prometheus/TextSerializer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Prometheus/TextSerializer.cs b/Prometheus/TextSerializer.cs index 6268c10f..2520b939 100644 --- a/Prometheus/TextSerializer.cs +++ b/Prometheus/TextSerializer.cs @@ -278,7 +278,7 @@ internal static CanonicalLabel EncodeValueAsCanonicalLabel(byte[] name, double v return new CanonicalLabel(name, PositiveInfinity, PositiveInfinity); #if NET - Span buffer = stackalloc char[128]; + Span buffer = stackalloc char[32]; if (!value.TryFormat(buffer, out var charsWritten, "g", CultureInfo.InvariantCulture)) throw new Exception("Failed to encode floating point value as string."); From 157772d956209b689fce122f2086e14de1c1b3f4 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 23 Nov 2023 09:44:09 +0200 Subject: [PATCH 126/230] Reduce memory allocations during label sequence serialization --- Prometheus/ChildBase.cs | 2 +- Prometheus/LabelSequence.cs | 76 ++++++++++++++++++++++++++++++------ Prometheus/TextSerializer.cs | 46 +++++++++++----------- 3 files changed, 87 insertions(+), 37 deletions(-) diff --git a/Prometheus/ChildBase.cs b/Prometheus/ChildBase.cs index 0ca2aff2..57a4e79d 100644 --- a/Prometheus/ChildBase.cs +++ b/Prometheus/ChildBase.cs @@ -12,7 +12,7 @@ internal ChildBase(Collector parent, LabelSequence instanceLabels, LabelSequence Parent = parent; InstanceLabels = instanceLabels; FlattenedLabels = flattenedLabels; - FlattenedLabelsBytes = PrometheusConstants.ExportEncoding.GetBytes(flattenedLabels.Serialize()); + FlattenedLabelsBytes = flattenedLabels.Serialize(); _publish = publish; _exemplarBehavior = exemplarBehavior; } diff --git a/Prometheus/LabelSequence.cs b/Prometheus/LabelSequence.cs index 4f6b1e30..faf4efa3 100644 --- a/Prometheus/LabelSequence.cs +++ b/Prometheus/LabelSequence.cs @@ -1,4 +1,5 @@ -using System.Text; +using System.ComponentModel.DataAnnotations; +using System.Text; namespace Prometheus; @@ -86,34 +87,83 @@ private static string EscapeLabelValue(string value) .Replace("\"", @"\"""); } + private static int GetEscapedLabelValueByteCount(string value) + { + var byteCount = PrometheusConstants.ExportEncoding.GetByteCount(value); + + foreach (var c in value) + { + if (c == '\\' || c == '\n' || c == '"') + byteCount++; + } + + return byteCount; + } + ///

- /// Serializes to the labelkey1="labelvalue1",labelkey2="labelvalue2" label string. + /// Serializes to the labelkey1="labelvalue1",labelkey2="labelvalue2" label string as bytes. /// - public string Serialize() + public byte[] Serialize() { - // Result is cached in child collector - no need to worry about efficiency here. - - var sb = new StringBuilder(); + // Result is cached in child collector, though we still might be making many of these child collectors if they are not reused. + // Let's try to be efficient to avoid allocations if this gets called in a hot path. + // First pass - calculate how many bytes we need to allocate. var nameEnumerator = Names.GetEnumerator(); var valueEnumerator = Values.GetEnumerator(); + var byteCount = 0; + + for (var i = 0; i < Names.Length; i++) + { + if (!nameEnumerator.MoveNext()) throw new Exception("API contract violation."); + if (!valueEnumerator.MoveNext()) throw new Exception("API contract violation."); + + if (i != 0) + byteCount += TextSerializer.Comma.Length; + + byteCount += PrometheusConstants.ExportEncoding.GetByteCount(nameEnumerator.Current); + byteCount += TextSerializer.Equal.Length; + byteCount += TextSerializer.Quote.Length; + byteCount += GetEscapedLabelValueByteCount(valueEnumerator.Current); + byteCount += TextSerializer.Quote.Length; + } + + var bytes = new byte[byteCount]; + var index = 0; + + nameEnumerator = Names.GetEnumerator(); + valueEnumerator = Values.GetEnumerator(); + for (var i = 0; i < Names.Length; i++) { if (!nameEnumerator.MoveNext()) throw new Exception("API contract violation."); if (!valueEnumerator.MoveNext()) throw new Exception("API contract violation."); if (i != 0) - sb.Append(','); + { + Array.Copy(TextSerializer.Comma, 0, bytes, index, TextSerializer.Comma.Length); + index += TextSerializer.Comma.Length; + } + + index += PrometheusConstants.ExportEncoding.GetBytes(nameEnumerator.Current, 0, nameEnumerator.Current.Length, bytes, index); + + Array.Copy(TextSerializer.Equal, 0, bytes, index, TextSerializer.Equal.Length); + index += TextSerializer.Equal.Length; - sb.Append(nameEnumerator.Current); - sb.Append('='); - sb.Append('"'); - sb.Append(EscapeLabelValue(valueEnumerator.Current)); - sb.Append('"'); + Array.Copy(TextSerializer.Quote, 0, bytes, index, TextSerializer.Quote.Length); + index += TextSerializer.Quote.Length; + + var escapedLabelValue = EscapeLabelValue(valueEnumerator.Current); + index += PrometheusConstants.ExportEncoding.GetBytes(escapedLabelValue, 0, escapedLabelValue.Length, bytes, index); + + Array.Copy(TextSerializer.Quote, 0, bytes, index, TextSerializer.Quote.Length); + index += TextSerializer.Quote.Length; } - return sb.ToString(); + if (index != byteCount) throw new Exception("API contract violation - we counted the same bytes twice but got different numbers."); + + return bytes; } public bool Equals(LabelSequence other) diff --git a/Prometheus/TextSerializer.cs b/Prometheus/TextSerializer.cs index 2520b939..2ab7bdcb 100644 --- a/Prometheus/TextSerializer.cs +++ b/Prometheus/TextSerializer.cs @@ -7,29 +7,29 @@ namespace Prometheus; /// internal sealed class TextSerializer : IMetricsSerializer { - private static readonly byte[] NewLine = { (byte)'\n' }; - private static readonly byte[] Quote = { (byte)'"' }; - private static readonly byte[] Equal = { (byte)'=' }; - private static readonly byte[] Comma = { (byte)',' }; - private static readonly byte[] Underscore = { (byte)'_' }; - private static readonly byte[] LeftBrace = { (byte)'{' }; - private static readonly byte[] RightBraceSpace = { (byte)'}', (byte)' ' }; - private static readonly byte[] Space = { (byte)' ' }; - private static readonly byte[] SpaceHashSpaceLeftBrace = { (byte)' ', (byte)'#', (byte)' ', (byte)'{' }; - private static readonly byte[] PositiveInfinity = PrometheusConstants.ExportEncoding.GetBytes("+Inf"); - private static readonly byte[] NegativeInfinity = PrometheusConstants.ExportEncoding.GetBytes("-Inf"); - private static readonly byte[] NotANumber = PrometheusConstants.ExportEncoding.GetBytes("NaN"); - private static readonly byte[] DotZero = PrometheusConstants.ExportEncoding.GetBytes(".0"); - private static readonly byte[] FloatPositiveOne = PrometheusConstants.ExportEncoding.GetBytes("1.0"); - private static readonly byte[] FloatZero = PrometheusConstants.ExportEncoding.GetBytes("0.0"); - private static readonly byte[] FloatNegativeOne = PrometheusConstants.ExportEncoding.GetBytes("-1.0"); - private static readonly byte[] IntPositiveOne = PrometheusConstants.ExportEncoding.GetBytes("1"); - private static readonly byte[] IntZero = PrometheusConstants.ExportEncoding.GetBytes("0"); - private static readonly byte[] IntNegativeOne = PrometheusConstants.ExportEncoding.GetBytes("-1"); - private static readonly byte[] EofNewLine = PrometheusConstants.ExportEncoding.GetBytes("# EOF\n"); - private static readonly byte[] HashHelpSpace = PrometheusConstants.ExportEncoding.GetBytes("# HELP "); - private static readonly byte[] NewlineHashTypeSpace = PrometheusConstants.ExportEncoding.GetBytes("\n# TYPE "); - private static readonly byte[] Unknown = PrometheusConstants.ExportEncoding.GetBytes("unknown"); + internal static readonly byte[] NewLine = { (byte)'\n' }; + internal static readonly byte[] Quote = { (byte)'"' }; + internal static readonly byte[] Equal = { (byte)'=' }; + internal static readonly byte[] Comma = { (byte)',' }; + internal static readonly byte[] Underscore = { (byte)'_' }; + internal static readonly byte[] LeftBrace = { (byte)'{' }; + internal static readonly byte[] RightBraceSpace = { (byte)'}', (byte)' ' }; + internal static readonly byte[] Space = { (byte)' ' }; + internal static readonly byte[] SpaceHashSpaceLeftBrace = { (byte)' ', (byte)'#', (byte)' ', (byte)'{' }; + internal static readonly byte[] PositiveInfinity = PrometheusConstants.ExportEncoding.GetBytes("+Inf"); + internal static readonly byte[] NegativeInfinity = PrometheusConstants.ExportEncoding.GetBytes("-Inf"); + internal static readonly byte[] NotANumber = PrometheusConstants.ExportEncoding.GetBytes("NaN"); + internal static readonly byte[] DotZero = PrometheusConstants.ExportEncoding.GetBytes(".0"); + internal static readonly byte[] FloatPositiveOne = PrometheusConstants.ExportEncoding.GetBytes("1.0"); + internal static readonly byte[] FloatZero = PrometheusConstants.ExportEncoding.GetBytes("0.0"); + internal static readonly byte[] FloatNegativeOne = PrometheusConstants.ExportEncoding.GetBytes("-1.0"); + internal static readonly byte[] IntPositiveOne = PrometheusConstants.ExportEncoding.GetBytes("1"); + internal static readonly byte[] IntZero = PrometheusConstants.ExportEncoding.GetBytes("0"); + internal static readonly byte[] IntNegativeOne = PrometheusConstants.ExportEncoding.GetBytes("-1"); + internal static readonly byte[] EofNewLine = PrometheusConstants.ExportEncoding.GetBytes("# EOF\n"); + internal static readonly byte[] HashHelpSpace = PrometheusConstants.ExportEncoding.GetBytes("# HELP "); + internal static readonly byte[] NewlineHashTypeSpace = PrometheusConstants.ExportEncoding.GetBytes("\n# TYPE "); + internal static readonly byte[] Unknown = PrometheusConstants.ExportEncoding.GetBytes("unknown"); private static readonly char[] DotEChar = { '.', 'e' }; From 84fc1fa47ffd0a4adfde4e3b358ca0636508a05a Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 23 Nov 2023 09:56:38 +0200 Subject: [PATCH 127/230] Reduce memory allocations by removing unnecessary label allocation duplication in histogram children --- Prometheus/Histogram.cs | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/Prometheus/Histogram.cs b/Prometheus/Histogram.cs index 363b6ec3..b62ab2b7 100644 --- a/Prometheus/Histogram.cs +++ b/Prometheus/Histogram.cs @@ -7,8 +7,14 @@ namespace Prometheus; public sealed class Histogram : Collector, IHistogram { private static readonly double[] DefaultBuckets = { .005, .01, .025, .05, .075, .1, .25, .5, .75, 1, 2.5, 5, 7.5, 10 }; + private readonly double[] _buckets; + // These labels go together with the buckets, so we do not need to allocate them for every child. + private readonly CanonicalLabel[] _leLabels; + + private static readonly byte[] LeLabelName = PrometheusConstants.ExportEncoding.GetBytes("le"); + internal Histogram(string name, string help, StringSequence instanceLabelNames, LabelSequence staticLabels, bool suppressInitialValue, double[]? buckets, ExemplarBehavior exemplarBehavior) : base(name, help, instanceLabelNames, staticLabels, suppressInitialValue, exemplarBehavior) { @@ -16,6 +22,7 @@ internal Histogram(string name, string help, StringSequence instanceLabelNames, { throw new ArgumentException("'le' is a reserved label name"); } + _buckets = buckets ?? DefaultBuckets; if (_buckets.Length == 0) @@ -35,6 +42,12 @@ internal Histogram(string name, string help, StringSequence instanceLabelNames, throw new ArgumentException("Bucket values must be increasing"); } } + + _leLabels = new CanonicalLabel[_buckets.Length]; + for (var i = 0; i < _buckets.Length; i++) + { + _leLabels[i] = TextSerializer.EncodeValueAsCanonicalLabel(LeLabelName, _buckets[i]); + } } private protected override Child NewChild(LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish, ExemplarBehavior exemplarBehavior) @@ -48,14 +61,10 @@ internal Child(Histogram parent, LabelSequence instanceLabels, LabelSequence fla : base(parent, instanceLabels, flattenedLabels, publish, exemplarBehavior) { Parent = parent; - + _upperBounds = Parent._buckets; _bucketCounts = new ThreadSafeLong[_upperBounds.Length]; - _leLabels = new CanonicalLabel[_upperBounds.Length]; - for (var i = 0; i < Parent._buckets.Length; i++) - { - _leLabels[i] = TextSerializer.EncodeValueAsCanonicalLabel(LeLabelName, Parent._buckets[i]); - } + _exemplars = new ObservedExemplar[_upperBounds.Length]; for (var i = 0; i < _upperBounds.Length; i++) { @@ -68,11 +77,9 @@ internal Child(Histogram parent, LabelSequence instanceLabels, LabelSequence fla private ThreadSafeDouble _sum = new ThreadSafeDouble(0.0D); private readonly ThreadSafeLong[] _bucketCounts; private readonly double[] _upperBounds; - private readonly CanonicalLabel[] _leLabels; private static readonly byte[] SumSuffix = PrometheusConstants.ExportEncoding.GetBytes("sum"); private static readonly byte[] CountSuffix = PrometheusConstants.ExportEncoding.GetBytes("count"); private static readonly byte[] BucketSuffix = PrometheusConstants.ExportEncoding.GetBytes("bucket"); - private static readonly byte[] LeLabelName = PrometheusConstants.ExportEncoding.GetBytes("le"); private readonly ObservedExemplar[] _exemplars; private protected override async Task CollectAndSerializeImplAsync(IMetricsSerializer serializer, @@ -108,7 +115,7 @@ await serializer.WriteMetricPointAsync( await serializer.WriteMetricPointAsync( Parent.NameBytes, FlattenedLabelsBytes, - _leLabels[i], + Parent._leLabels[i], cancel, cumulativeCount, exemplar, @@ -144,7 +151,7 @@ private void ObserveInternal(double val, long count, Exemplar? exemplar) if (exemplar != null) RecordExemplar(exemplar, ref _exemplars[i], val); - + break; } } From 3ad8763bf2eb8afbcebd790f48e6624282963434 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 23 Nov 2023 11:01:01 +0200 Subject: [PATCH 128/230] Remove unnecessary allocation of new delegate on every call to add a new child collector --- Prometheus/Collector.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Prometheus/Collector.cs b/Prometheus/Collector.cs index decd30c1..15bed80a 100644 --- a/Prometheus/Collector.cs +++ b/Prometheus/Collector.cs @@ -210,7 +210,7 @@ private TChild GetOrAddLabelled(LabelSequence instanceLabels) if (_labelledMetrics.TryGetValue(instanceLabels, out var metric)) return metric; - return _labelledMetrics.GetOrAdd(instanceLabels, CreateLabelledChild); + return _labelledMetrics.GetOrAdd(instanceLabels, _createdLabelledChildFunc); } private TChild CreateLabelledChild(LabelSequence instanceLabels) @@ -221,6 +221,9 @@ private TChild CreateLabelledChild(LabelSequence instanceLabels) return NewChild(instanceLabels, flattenedLabels, publish: !_suppressInitialValue, _exemplarBehavior); } + // Cache the delegate to avoid allocating a new one every time in GetOrAddLabelled. + private readonly Func _createdLabelledChildFunc; + /// /// For tests that want to see what instance-level label values were used when metrics were created. /// @@ -233,6 +236,8 @@ internal Collector(string name, string help, StringSequence instanceLabelNames, _exemplarBehavior = exemplarBehavior; _unlabelledLazy = GetUnlabelledLazyInitializer(); + + _createdLabelledChildFunc = CreateLabelledChild; } /// From af429394207e47dd5c688dff091f032a30c1610a Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 23 Nov 2023 11:36:53 +0200 Subject: [PATCH 129/230] Reduced the total size of memory allocations performed when initializing metrics, especially histograms. --- Benchmark.NetCore/SdkComparisonBenchmarks.cs | 16 ++++++++-------- History | 2 ++ README.md | 16 ++++++++-------- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/Benchmark.NetCore/SdkComparisonBenchmarks.cs b/Benchmark.NetCore/SdkComparisonBenchmarks.cs index b8162fe3..a4e1b51c 100644 --- a/Benchmark.NetCore/SdkComparisonBenchmarks.cs +++ b/Benchmark.NetCore/SdkComparisonBenchmarks.cs @@ -14,14 +14,14 @@ .NET SDK 8.0.100 Job-IZHPUA : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 -| Method | Job | MaxIterationCount | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | -|------------------------------ |----------- |------------------ |------------:|----------:|----------:|---------:|---------:|----------:| -| PromNetCounter | DefaultJob | Default | 232.0 us | 1.90 us | 1.78 us | - | - | - | -| PromNetHistogram | DefaultJob | Default | 1,200.4 us | 8.11 us | 7.19 us | - | - | 2 B | -| OTelCounter | DefaultJob | Default | 10,879.7 us | 47.76 us | 44.67 us | - | - | 11 B | -| OTelHistogram | DefaultJob | Default | 12,310.7 us | 57.24 us | 50.75 us | - | - | 24 B | -| PromNetHistogramForAdHocLabel | Job-IZHPUA | 16 | 5,765.7 us | 372.36 us | 330.09 us | 187.5000 | 171.8750 | 3184106 B | -| OTelHistogramForAdHocLabel | Job-IZHPUA | 16 | 348.7 us | 3.01 us | 2.67 us | 5.3711 | - | 96000 B | +| Method | Job | MaxIterationCount | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | +|------------------------------ |----------- |------------------ |------------:|----------:|----------:|--------:|--------:|----------:| +| PromNetCounter | DefaultJob | Default | 237.1 us | 1.71 us | 1.43 us | - | - | - | +| PromNetHistogram | DefaultJob | Default | 1,236.2 us | 9.99 us | 8.86 us | - | - | 2 B | +| OTelCounter | DefaultJob | Default | 10,981.5 us | 64.49 us | 57.17 us | - | - | 11 B | +| OTelHistogram | DefaultJob | Default | 12,078.9 us | 126.10 us | 117.95 us | - | - | 24 B | +| PromNetHistogramForAdHocLabel | Job-NDQITE | 16 | 1,877.7 us | 104.83 us | 87.54 us | 50.7813 | 48.8281 | 872701 B | +| OTelHistogramForAdHocLabel | Job-NDQITE | 16 | 354.0 us | 4.05 us | 3.78 us | 5.3711 | - | 96000 B | */ /// diff --git a/History b/History index e0c536aa..e78233d8 100644 --- a/History +++ b/History @@ -1,3 +1,5 @@ +* 8.2.0 +- Various optimizations to reduce spent CPU time and allocated memory. * 8.1.1 - Fix bug in .NET Meters API adapter for UpDownCounter, which was incorrectly transformed to Prometheus metrics. #452 and #453 * 8.1.0 diff --git a/README.md b/README.md index 8ea6adfe..ea324dcb 100644 --- a/README.md +++ b/README.md @@ -897,14 +897,14 @@ AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores [Host] : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 -| Method | Mean | Gen0 | Gen1 | Allocated | -|------------------------------ |------------:|---------:|---------:|----------:| -| PromNetCounter | 232.0 us | - | - | - | -| OTelCounter | 10,879.7 us | - | - | 11 B | -| PromNetHistogram | 1,200.4 us | - | - | 2 B | -| OTelHistogram | 12,310.7 us | - | - | 24 B | -| PromNetHistogramForAdHocLabel | 5,765.7 us | 187.5000 | 171.8750 | 3184106 B | -| OTelHistogramForAdHocLabel | 348.7 us | 5.3711 | - | 96000 B | +| Method | Mean | Gen0 | Gen1 | Allocated | +|------------------------------ |------------:|--------:|--------:|----------:| +| PromNetCounter | 237.1 us | - | - | - | +| PromNetHistogram | 1,236.2 us | - | - | 2 B | +| OTelCounter | 10,981.5 us | - | - | 11 B | +| OTelHistogram | 12,078.9 us | - | - | 24 B | +| PromNetHistogramForAdHocLabel | 1,877.7 us | 50.7813 | 48.8281 | 872701 B | +| OTelHistogramForAdHocLabel | 354.0 us | 5.3711 | - | 96000 B | ``` # Community projects From 3128f29a6ed6e1380fac31b0a5ab5e361d61ba2d Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 23 Nov 2023 13:13:09 +0200 Subject: [PATCH 130/230] LabelSequence should be IEquatable for optimal comparisons --- Prometheus/LabelSequence.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/Prometheus/LabelSequence.cs b/Prometheus/LabelSequence.cs index faf4efa3..a4a5eedc 100644 --- a/Prometheus/LabelSequence.cs +++ b/Prometheus/LabelSequence.cs @@ -1,12 +1,9 @@ -using System.ComponentModel.DataAnnotations; -using System.Text; - -namespace Prometheus; +namespace Prometheus; /// /// A sequence of metric label-name pairs. /// -internal struct LabelSequence +internal struct LabelSequence : IEquatable { public static readonly LabelSequence Empty = new(); From 1d3c9528d4faa53922bd9dff675e69f295870bab Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 23 Nov 2023 13:13:23 +0200 Subject: [PATCH 131/230] Be explicit about ordinal string comparison for collector familiy --- Benchmark.NetCore/MetricCreationBenchmarks.cs | 1 + Prometheus/CollectorRegistry.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Benchmark.NetCore/MetricCreationBenchmarks.cs b/Benchmark.NetCore/MetricCreationBenchmarks.cs index f73d34f7..d5a8fbb4 100644 --- a/Benchmark.NetCore/MetricCreationBenchmarks.cs +++ b/Benchmark.NetCore/MetricCreationBenchmarks.cs @@ -8,6 +8,7 @@ namespace Benchmark.NetCore /// creating a brand new set of metrics for each scrape. So let's benchmark this scenario. /// [MemoryDiagnoser] + [EventPipeProfiler(BenchmarkDotNet.Diagnosers.EventPipeProfile.GcVerbose)] public class MetricCreationBenchmarks { /// diff --git a/Prometheus/CollectorRegistry.cs b/Prometheus/CollectorRegistry.cs index 0110152c..8e7eb06f 100644 --- a/Prometheus/CollectorRegistry.cs +++ b/Prometheus/CollectorRegistry.cs @@ -232,7 +232,7 @@ static CollectorFamily ValidateFamily(CollectorFamily candidate) // Each collector family has an identity (the base name of the metric, in Prometheus format) and any number of collectors within. // Different collectors in the same family may have different sets of labels (static and instance) depending on how they were created. - private readonly ConcurrentDictionary _families = new(); + private readonly ConcurrentDictionary _families = new(StringComparer.Ordinal); internal void SetBeforeFirstCollectCallback(Action a) { From efb1dc5d981fc4eed15541d4df05986b1abf79e8 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 23 Nov 2023 13:13:41 +0200 Subject: [PATCH 132/230] Remove accidental debug commit --- Benchmark.NetCore/MetricCreationBenchmarks.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Benchmark.NetCore/MetricCreationBenchmarks.cs b/Benchmark.NetCore/MetricCreationBenchmarks.cs index d5a8fbb4..f73d34f7 100644 --- a/Benchmark.NetCore/MetricCreationBenchmarks.cs +++ b/Benchmark.NetCore/MetricCreationBenchmarks.cs @@ -8,7 +8,6 @@ namespace Benchmark.NetCore /// creating a brand new set of metrics for each scrape. So let's benchmark this scenario. /// [MemoryDiagnoser] - [EventPipeProfiler(BenchmarkDotNet.Diagnosers.EventPipeProfile.GcVerbose)] public class MetricCreationBenchmarks { /// From c99bca270901f155e68f1b324f89c97801ff8655 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 23 Nov 2023 14:04:22 +0200 Subject: [PATCH 133/230] LabelSequence can be readonly --- Prometheus/LabelSequence.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Prometheus/LabelSequence.cs b/Prometheus/LabelSequence.cs index a4a5eedc..346765ab 100644 --- a/Prometheus/LabelSequence.cs +++ b/Prometheus/LabelSequence.cs @@ -3,7 +3,7 @@ /// /// A sequence of metric label-name pairs. /// -internal struct LabelSequence : IEquatable +internal readonly struct LabelSequence : IEquatable { public static readonly LabelSequence Empty = new(); From 4a9d2e76df170eab1a5b194cbf2d80df16cf243d Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 23 Nov 2023 14:17:05 +0200 Subject: [PATCH 134/230] Rightsize HashSet used for label collision detection to reduce memory allocations --- Prometheus/Collector.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Prometheus/Collector.cs b/Prometheus/Collector.cs index 15bed80a..7c8663d9 100644 --- a/Prometheus/Collector.cs +++ b/Prometheus/Collector.cs @@ -77,7 +77,11 @@ internal Collector(string name, string help, StringSequence instanceLabelNames, FlattenedLabelNames = instanceLabelNames.Concat(staticLabels.Names); // Used to check uniqueness. +#if NET + var uniqueLabelNames = new HashSet(FlattenedLabelNames.Length, StringComparer.Ordinal); +#else var uniqueLabelNames = new HashSet(StringComparer.Ordinal); +#endif var labelNameEnumerator = FlattenedLabelNames.GetEnumerator(); while (labelNameEnumerator.MoveNext()) From 0762c125b1b638253ab7b8c9d60adb05627b09e1 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 23 Nov 2023 14:24:47 +0200 Subject: [PATCH 135/230] Simplify some redundant code --- Prometheus/Collector.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Prometheus/Collector.cs b/Prometheus/Collector.cs index 7c8663d9..66aa91e3 100644 --- a/Prometheus/Collector.cs +++ b/Prometheus/Collector.cs @@ -208,12 +208,7 @@ private TChild GetOrAddLabelled(LabelSequence instanceLabels) // NOTE: We do not try to find a metric instance with the same set of label names but in a DIFFERENT order. // Order of labels matterns in data creation, although does not matter when the exported data set is imported later. // If we somehow end up registering the same metric with the same label names in different order, we will publish it twice, in two orders... - // That is not ideal but also not that big of a deal to do a lookup every time a metric instance is registered. - - // Don't allocate lambda for GetOrAdd in the common case that the labeled metrics exist. - if (_labelledMetrics.TryGetValue(instanceLabels, out var metric)) - return metric; - + // That is not ideal but also not that big of a deal to justify a lookup every time a metric instance is registered. return _labelledMetrics.GetOrAdd(instanceLabels, _createdLabelledChildFunc); } From d9f668d2b4d995c5411b36d89932f544a2afa2c4 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 23 Nov 2023 14:34:13 +0200 Subject: [PATCH 136/230] Reduce duplicate conversions in metric type to byte[] mapping --- Prometheus/Collector.cs | 6 +++--- Prometheus/TextSerializer.cs | 8 ++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/Prometheus/Collector.cs b/Prometheus/Collector.cs index 66aa91e3..c13cb25c 100644 --- a/Prometheus/Collector.cs +++ b/Prometheus/Collector.cs @@ -40,8 +40,8 @@ public abstract class Collector internal LabelSequence StaticLabels; internal abstract MetricType Type { get; } - - internal byte[] TypeBytes { get; } + + internal byte[] TypeBytes { get; } internal abstract int ChildCount { get; } internal abstract int TimeseriesCount { get; } @@ -66,7 +66,7 @@ internal Collector(string name, string help, StringSequence instanceLabelNames, Name = name; NameBytes = PrometheusConstants.ExportEncoding.GetBytes(Name); - TypeBytes = PrometheusConstants.ExportEncoding.GetBytes(Type.ToString().ToLowerInvariant()); + TypeBytes = TextSerializer.MetricTypeToBytes[Type]; Help = help; HelpBytes = String.IsNullOrWhiteSpace(help) ? Array.Empty() diff --git a/Prometheus/TextSerializer.cs b/Prometheus/TextSerializer.cs index 2ab7bdcb..3ee19013 100644 --- a/Prometheus/TextSerializer.cs +++ b/Prometheus/TextSerializer.cs @@ -31,6 +31,14 @@ internal sealed class TextSerializer : IMetricsSerializer internal static readonly byte[] NewlineHashTypeSpace = PrometheusConstants.ExportEncoding.GetBytes("\n# TYPE "); internal static readonly byte[] Unknown = PrometheusConstants.ExportEncoding.GetBytes("unknown"); + internal static readonly Dictionary MetricTypeToBytes = new() + { + { MetricType.Gauge, PrometheusConstants.ExportEncoding.GetBytes("gauge") }, + { MetricType.Counter, PrometheusConstants.ExportEncoding.GetBytes("counter") }, + { MetricType.Histogram, PrometheusConstants.ExportEncoding.GetBytes("histogram") }, + { MetricType.Summary, PrometheusConstants.ExportEncoding.GetBytes("summary") }, + }; + private static readonly char[] DotEChar = { '.', 'e' }; public TextSerializer(Stream stream, ExpositionFormat fmt = ExpositionFormat.PrometheusText) From febfbe951686b4362c86d8bbe922e338131ed9f3 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 23 Nov 2023 15:54:22 +0200 Subject: [PATCH 137/230] Reduce string allocations in TextSerializer --- Prometheus/TextSerializer.cs | 36 +++++++++++++++++++++++++++++++----- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/Prometheus/TextSerializer.cs b/Prometheus/TextSerializer.cs index 3ee19013..cce166de 100644 --- a/Prometheus/TextSerializer.cs +++ b/Prometheus/TextSerializer.cs @@ -183,6 +183,23 @@ private async Task WriteValue(double value, CancellationToken cancel) } } +#if NET + static bool RequiresDotZero(char[] buffer, int length) + { + return buffer.AsSpan(0..length).IndexOfAny(DotEChar) == -1; /* did not contain .|e */ + } + + // Size limit guided by https://stackoverflow.com/questions/21146544/what-is-the-maximum-length-of-double-tostringd + if (!value.TryFormat(_stringCharsBuffer, out var charsWritten, "g", CultureInfo.InvariantCulture)) + throw new Exception("Failed to encode floating point value as string."); + + var encodedBytes = PrometheusConstants.ExportEncoding.GetBytes(_stringCharsBuffer, 0, charsWritten, _stringBytesBuffer, 0); + await _stream.Value.WriteAsync(_stringBytesBuffer, 0, encodedBytes, cancel); + + // In certain places (e.g. "le" label) we need floating point values to actually have the decimal point in them for OpenMetrics. + if (_expositionFormat == ExpositionFormat.OpenMetricsText && RequiresDotZero(_stringCharsBuffer, charsWritten)) + await _stream.Value.WriteAsync(DotZero, 0, DotZero.Length, cancel); +#else var valueAsString = value.ToString("g", CultureInfo.InvariantCulture); var numBytes = PrometheusConstants.ExportEncoding.GetBytes(valueAsString, 0, valueAsString.Length, _stringBytesBuffer, 0); @@ -191,6 +208,7 @@ private async Task WriteValue(double value, CancellationToken cancel) // In certain places (e.g. "le" label) we need floating point values to actually have the decimal point in them for OpenMetrics. if (_expositionFormat == ExpositionFormat.OpenMetricsText && valueAsString.IndexOfAny(DotEChar) == -1 /* did not contain .|e */) await _stream.Value.WriteAsync(DotZero, 0, DotZero.Length, cancel); +#endif } private async Task WriteValue(long value, CancellationToken cancel) @@ -211,17 +229,24 @@ private async Task WriteValue(long value, CancellationToken cancel) } } +#if NET + if (!value.TryFormat(_stringCharsBuffer, out var charsWritten, "D", CultureInfo.InvariantCulture)) + throw new Exception("Failed to encode integer value as string."); + + var encodedBytes = PrometheusConstants.ExportEncoding.GetBytes(_stringCharsBuffer, 0, charsWritten, _stringBytesBuffer, 0); + await _stream.Value.WriteAsync(_stringBytesBuffer, 0, encodedBytes, cancel); +#else var valueAsString = value.ToString("D", CultureInfo.InvariantCulture); - var numBytes = PrometheusConstants.ExportEncoding.GetBytes(valueAsString, 0, valueAsString.Length, _stringBytesBuffer, 0); await _stream.Value.WriteAsync(_stringBytesBuffer, 0, numBytes, cancel); +#endif } - // Reuse a buffer to do the UTF-8 encoding. - // Maybe one day also ValueStringBuilder but that would be .NET Core only. - // https://github.com/dotnet/corefx/issues/28379 + // Reuse a buffer to do the serialization and UTF-8 encoding. // Size limit guided by https://stackoverflow.com/questions/21146544/what-is-the-maximum-length-of-double-tostringd + private readonly char[] _stringCharsBuffer = new char[32]; private readonly byte[] _stringBytesBuffer = new byte[32]; + private readonly ExpositionFormat _expositionFormat; /// @@ -286,6 +311,7 @@ internal static CanonicalLabel EncodeValueAsCanonicalLabel(byte[] name, double v return new CanonicalLabel(name, PositiveInfinity, PositiveInfinity); #if NET + // Size limit guided by https://stackoverflow.com/questions/21146544/what-is-the-maximum-length-of-double-tostringd Span buffer = stackalloc char[32]; if (!value.TryFormat(buffer, out var charsWritten, "g", CultureInfo.InvariantCulture)) @@ -319,7 +345,7 @@ internal static CanonicalLabel EncodeValueAsCanonicalLabel(byte[] name, double v // It is already a floating-point value in Prometheus representation - reuse same bytes for OpenMetrics. openMetricsBytes = prometheusBytes; } - + #else var valueAsString = value.ToString("g", CultureInfo.InvariantCulture); var prometheusBytes = PrometheusConstants.ExportEncoding.GetBytes(valueAsString); From 1f5d6ce7d3867bc42fb248790d0c907584439fe9 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 23 Nov 2023 15:58:32 +0200 Subject: [PATCH 138/230] Reduce defensive copy allocations during serialization --- Prometheus/Collector.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/Prometheus/Collector.cs b/Prometheus/Collector.cs index c13cb25c..4c99e566 100644 --- a/Prometheus/Collector.cs +++ b/Prometheus/Collector.cs @@ -13,15 +13,15 @@ public abstract class Collector /// The metric name, e.g. http_requests_total. /// public string Name { get; } - - internal byte[] NameBytes { get; } + + internal byte[] NameBytes { get; } /// /// The help text describing the metric for a human audience. /// public string Help { get; } - internal byte[] HelpBytes { get; } + internal byte[] HelpBytes { get; } /// /// Names of the instance-specific labels (name-value pairs) that apply to this metric. @@ -63,7 +63,7 @@ internal Collector(string name, string help, StringSequence instanceLabelNames, { if (!MetricNameRegex.IsMatch(name)) throw new ArgumentException($"Metric name '{name}' does not match regex '{ValidMetricNameExpression}'."); - + Name = name; NameBytes = PrometheusConstants.ExportEncoding.GetBytes(Name); TypeBytes = TextSerializer.MetricTypeToBytes[Type]; @@ -252,8 +252,11 @@ internal override async Task CollectAndSerializeAsync(IMetricsSerializer seriali if (writeFamilyDeclaration) await serializer.WriteFamilyDeclarationAsync(Name, NameBytes, HelpBytes, Type, TypeBytes, cancel); - foreach (var child in _labelledMetrics.Values) - await child.CollectAndSerializeAsync(serializer, cancel); + // We iterate the pairs to avoid allocating a temporary array from .Values. + foreach (var pair in _labelledMetrics) + { + await pair.Value.CollectAndSerializeAsync(serializer, cancel); + } } private readonly bool _suppressInitialValue; From dc2de0d5e5a21e5dfcb82ed477fcf6d72a013dd6 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 23 Nov 2023 16:04:58 +0200 Subject: [PATCH 139/230] Reduce LINQ memory usage during serialization --- Prometheus/Histogram.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/Prometheus/Histogram.cs b/Prometheus/Histogram.cs index b62ab2b7..5347d081 100644 --- a/Prometheus/Histogram.cs +++ b/Prometheus/Histogram.cs @@ -101,7 +101,7 @@ await serializer.WriteMetricPointAsync( FlattenedLabelsBytes, CanonicalLabel.Empty, cancel, - _bucketCounts.Sum(b => b.Value), + Count, ObservedExemplar.Empty, suffix: CountSuffix); @@ -126,7 +126,19 @@ await serializer.WriteMetricPointAsync( } public double Sum => _sum.Value; - public long Count => _bucketCounts.Sum(b => b.Value); + + public long Count + { + get + { + long total = 0; + + foreach (var count in _bucketCounts) + total += count.Value; + + return total; + } + } public void Observe(double val, Exemplar? exemplarLabels) => ObserveInternal(val, 1, exemplarLabels); From ab05f3f242348efd79a43cfae81c0fc2a2c37772 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 23 Nov 2023 16:30:35 +0200 Subject: [PATCH 140/230] Reduce defensive copies --- Prometheus/CollectorFamily.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Prometheus/CollectorFamily.cs b/Prometheus/CollectorFamily.cs index 49c87f83..7e8e6aa4 100644 --- a/Prometheus/CollectorFamily.cs +++ b/Prometheus/CollectorFamily.cs @@ -17,9 +17,10 @@ internal async Task CollectAndSerializeAsync(IMetricsSerializer serializer, Canc { bool isFirst = true; - foreach (var collector in Collectors.Values) + // Iterate the pairs to avoid a defensive copy. + foreach (var pair in Collectors) { - await collector.CollectAndSerializeAsync(serializer, isFirst, cancel); + await pair.Value.CollectAndSerializeAsync(serializer, isFirst, cancel); isFirst = false; } } From 9415dc20a871952d3df1ddbb7f23704a41384cf8 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 23 Nov 2023 16:43:26 +0200 Subject: [PATCH 141/230] Reduce memory allocation during summary export --- Prometheus/Summary.cs | 87 ++++++++++++++++++++++++------------------- 1 file changed, 49 insertions(+), 38 deletions(-) diff --git a/Prometheus/Summary.cs b/Prometheus/Summary.cs index a147ee2e..9b441bb9 100644 --- a/Prometheus/Summary.cs +++ b/Prometheus/Summary.cs @@ -1,4 +1,5 @@ -using Prometheus.SummaryImpl; +using System.Buffers; +using Prometheus.SummaryImpl; namespace Prometheus { @@ -120,55 +121,65 @@ private protected override async Task CollectAndSerializeImplAsync(IMetricsSeria long count; double sum; - var values = new List<(double quantile, double value)>(_objectives.Count); - lock (_bufLock) + var values = ArrayPool<(double quantile, double value)>.Shared.Rent(_objectives.Count); + var valuesIndex = 0; + + try { - lock (_lock) + + lock (_bufLock) { - // Swap bufs even if hotBuf is empty to set new hotBufExpTime. - SwapBufs(now); - FlushColdBuf(); + lock (_lock) + { + // Swap bufs even if hotBuf is empty to set new hotBufExpTime. + SwapBufs(now); + FlushColdBuf(); - count = _count; - sum = _sum; + count = _count; + sum = _sum; - for (var i = 0; i < _sortedObjectives.Length; i++) - { - var quantile = _sortedObjectives[i]; - var value = _headStream.Count == 0 ? double.NaN : _headStream.Query(quantile); + for (var i = 0; i < _sortedObjectives.Length; i++) + { + var quantile = _sortedObjectives[i]; + var value = _headStream.Count == 0 ? double.NaN : _headStream.Query(quantile); - values.Add((quantile, value)); + values[valuesIndex++] = (quantile, value); + } } } - } - await serializer.WriteMetricPointAsync( - Parent.NameBytes, - FlattenedLabelsBytes, - CanonicalLabel.Empty, - cancel, - sum, - ObservedExemplar.Empty, - suffix: SumSuffix); - await serializer.WriteMetricPointAsync( - Parent.NameBytes, - FlattenedLabelsBytes, - CanonicalLabel.Empty, - cancel, - count, - ObservedExemplar.Empty, - suffix: CountSuffix); - - for (var i = 0; i < values.Count; i++) - { await serializer.WriteMetricPointAsync( Parent.NameBytes, FlattenedLabelsBytes, - _quantileLabels[i], - cancel, - values[i].value, - ObservedExemplar.Empty); + CanonicalLabel.Empty, + cancel, + sum, + ObservedExemplar.Empty, + suffix: SumSuffix); + await serializer.WriteMetricPointAsync( + Parent.NameBytes, + FlattenedLabelsBytes, + CanonicalLabel.Empty, + cancel, + count, + ObservedExemplar.Empty, + suffix: CountSuffix); + + for (var i = 0; i < _objectives.Count; i++) + { + await serializer.WriteMetricPointAsync( + Parent.NameBytes, + FlattenedLabelsBytes, + _quantileLabels[i], + cancel, + values[i].value, + ObservedExemplar.Empty); + } + } + finally + { + ArrayPool<(double quantile, double value)>.Shared.Return(values); } } From b43770d55c082b84ad3065e8f7331382050c4251 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Fri, 24 Nov 2023 09:06:10 +0200 Subject: [PATCH 142/230] Use HttpListener for Prometheus instead of Kestrel, for more fair comparison --- Benchmark.NetCore/SdkComparisonBenchmarks.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Benchmark.NetCore/SdkComparisonBenchmarks.cs b/Benchmark.NetCore/SdkComparisonBenchmarks.cs index a4e1b51c..1ca77167 100644 --- a/Benchmark.NetCore/SdkComparisonBenchmarks.cs +++ b/Benchmark.NetCore/SdkComparisonBenchmarks.cs @@ -44,6 +44,7 @@ .NET SDK 8.0.100 /// Both SDKs support it as an optimization (though OpenTelemetry forces it for counters) but let's try keep the logic here simple and exclude it for now. /// [MemoryDiagnoser] +[EventPipeProfiler(BenchmarkDotNet.Diagnosers.EventPipeProfile.GcVerbose)] public class SdkComparisonBenchmarks { // Unique sets of label/tag values per metric. You can think of each one as a "session" we are reporting data for. @@ -97,7 +98,7 @@ private sealed class PrometheusNetMetricsContext : MetricsContext private readonly List _histogramInstances = new(TimeseriesPerMetric); private readonly Histogram _histogramForAdHocLabels; - private readonly KestrelMetricServer _server; + private readonly IMetricServer _server; public PrometheusNetMetricsContext() { @@ -120,10 +121,9 @@ public PrometheusNetMetricsContext() _histogramInstances.Add(histogram.WithLabels(Label1Value, Label2Value, SessionIds[i])); // `AddPrometheusHttpListener` of OpenTelemetry creates an HttpListener. - // Start a listener/server for Prometheus Benchmarks for a fair comparison. - // We listen on 127.0.0.1: to avoid firewall prompts (we do not expect to receive any traffic). - _server = new KestrelMetricServer("127.0.0.1", port: 0); - _server = new KestrelMetricServer(port: 1234); + // Start an equivalent listener for Prometheus to ensure a fair comparison. + // We listen on 127.0.0.1 to avoid firewall prompts (randomly chosen port - we do not expect to receive any traffic). + _server = new MetricServer("127.0.0.1", port: 8051); _server.Start(); } @@ -176,7 +176,7 @@ public OpenTelemetryMetricsContext() _histogramForAdHocLabels = _meter.CreateHistogram("histogramForAdHocLabels"); _provider = OpenTelemetry.Sdk.CreateMeterProviderBuilder() - .AddView("histogram", new OpenTelemetry.Metrics.HistogramConfiguration() { RecordMinMax = false}) + .AddView("histogram", new OpenTelemetry.Metrics.HistogramConfiguration() { RecordMinMax = false }) .AddMeter(_meter.Name) .AddPrometheusHttpListener() .Build(); @@ -222,7 +222,7 @@ public override void Dispose() private MetricsContext _context; - [GlobalSetup(Targets = new string[] {nameof(OTelCounter), nameof(OTelHistogram), nameof(OTelHistogramForAdHocLabel)})] + [GlobalSetup(Targets = new string[] { nameof(OTelCounter), nameof(OTelHistogram), nameof(OTelHistogramForAdHocLabel) })] public void OpenTelemetrySetup() { _context = new OpenTelemetryMetricsContext(); From c53313fbe7dff71747153a54faa8ea1c0ab39c02 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Fri, 24 Nov 2023 09:09:28 +0200 Subject: [PATCH 143/230] Remove accidentally committed profiler attribute --- Benchmark.NetCore/SdkComparisonBenchmarks.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Benchmark.NetCore/SdkComparisonBenchmarks.cs b/Benchmark.NetCore/SdkComparisonBenchmarks.cs index 1ca77167..200b66c9 100644 --- a/Benchmark.NetCore/SdkComparisonBenchmarks.cs +++ b/Benchmark.NetCore/SdkComparisonBenchmarks.cs @@ -44,7 +44,6 @@ .NET SDK 8.0.100 /// Both SDKs support it as an optimization (though OpenTelemetry forces it for counters) but let's try keep the logic here simple and exclude it for now. /// [MemoryDiagnoser] -[EventPipeProfiler(BenchmarkDotNet.Diagnosers.EventPipeProfile.GcVerbose)] public class SdkComparisonBenchmarks { // Unique sets of label/tag values per metric. You can think of each one as a "session" we are reporting data for. From 56a82d96baaad001181b305ce352b9e149c6889a Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Fri, 24 Nov 2023 13:36:06 +0200 Subject: [PATCH 144/230] Tidy up MetricCreationBenchmarks and use larger workload for more stable results --- Benchmark.NetCore/MetricCreationBenchmarks.cs | 219 +++++++++--------- 1 file changed, 109 insertions(+), 110 deletions(-) diff --git a/Benchmark.NetCore/MetricCreationBenchmarks.cs b/Benchmark.NetCore/MetricCreationBenchmarks.cs index f73d34f7..40ba1ee2 100644 --- a/Benchmark.NetCore/MetricCreationBenchmarks.cs +++ b/Benchmark.NetCore/MetricCreationBenchmarks.cs @@ -1,137 +1,136 @@ using BenchmarkDotNet.Attributes; using Prometheus; -namespace Benchmark.NetCore +namespace Benchmark.NetCore; + +/// +/// One pattern advocated by Prometheus documentation is to implement scraping of external systems by +/// creating a brand new set of metrics for each scrape. So let's benchmark this scenario. +/// +[MemoryDiagnoser] +public class MetricCreationBenchmarks { /// - /// One pattern advocated by Prometheus documentation is to implement scraping of external systems by - /// creating a brand new set of metrics for each scrape. So let's benchmark this scenario. + /// Just to ensure that a benchmark iteration has enough to do for stable and meaningful results. /// - [MemoryDiagnoser] - public class MetricCreationBenchmarks - { - /// - /// Just to ensure that a benchmark iteration has enough to do for stable and meaningful results. - /// - private const int _metricCount = 100; + private const int _metricCount = 10_000; - /// - /// How many times we repeat acquiring and incrementing the same instance. - /// - [Params(1, 10)] - public int RepeatCount { get; set; } + /// + /// How many times we repeat acquiring and incrementing the same instance. + /// + [Params(1, 10)] + public int RepeatCount { get; set; } - /// - /// How many times we should try to register a metric that already exists. - /// - [Params(1, 10)] - public int DuplicateCount { get; set; } + /// + /// How many times we should try to register a metric that already exists. + /// + [Params(1, 10)] + public int DuplicateCount { get; set; } - [Params(true, false)] - public bool IncludeStaticLabels { get; set; } + [Params(true, false)] + public bool IncludeStaticLabels { get; set; } - private const string _help = "arbitrary help message for metric, not relevant for benchmarking"; + private const string _help = "arbitrary help message for metric, not relevant for benchmarking"; - private static readonly string[] _metricNames; + private static readonly string[] _metricNames; - static MetricCreationBenchmarks() - { - _metricNames = new string[_metricCount]; + static MetricCreationBenchmarks() + { + _metricNames = new string[_metricCount]; - for (var i = 0; i < _metricCount; i++) - _metricNames[i] = $"metric_{i:D4}"; - } + for (var i = 0; i < _metricCount; i++) + _metricNames[i] = $"metric_{i:D4}"; + } + + private CollectorRegistry _registry; + private IMetricFactory _factory; - private CollectorRegistry _registry; - private IMetricFactory _factory; + [IterationSetup] + public void Setup() + { + _registry = Metrics.NewCustomRegistry(); + _factory = Metrics.WithCustomRegistry(_registry); - [IterationSetup] - public void Setup() + if (IncludeStaticLabels) { - _registry = Metrics.NewCustomRegistry(); - _factory = Metrics.WithCustomRegistry(_registry); + _registry.SetStaticLabels(new Dictionary + { + { "static_foo", "static_bar" }, + { "static_foo1", "static_bar" }, + { "static_foo2", "static_bar" }, + { "static_foo3", "static_bar" }, + { "static_foo4", "static_bar" } + }); + + _factory = _factory.WithLabels(new Dictionary + { + { "static_gaa", "static_bar" }, + { "static_gaa1", "static_bar" }, + { "static_gaa2", "static_bar" }, + { "static_gaa3", "static_bar" }, + { "static_gaa4", "static_bar" }, + { "static_gaa5", "static_bar" }, + }); + } + } + + // We use the same strings both for the names and the values. + private static readonly string[] _labels = ["foo", "bar", "baz"]; + + private readonly CounterConfiguration _counterConfiguration = CounterConfiguration.Default; + private readonly GaugeConfiguration _gaugeConfiguration = GaugeConfiguration.Default; + private readonly SummaryConfiguration _summaryConfiguration = SummaryConfiguration.Default; + private readonly HistogramConfiguration _histogramConfiguration = HistogramConfiguration.Default; - if (IncludeStaticLabels) + [Benchmark] + public void Counter() + { + for (var dupe = 0; dupe < DuplicateCount; dupe++) + for (var i = 0; i < _metricCount; i++) { - _registry.SetStaticLabels(new Dictionary - { - { "static_foo", "static_bar" }, - { "static_foo1", "static_bar" }, - { "static_foo2", "static_bar" }, - { "static_foo3", "static_bar" }, - { "static_foo4", "static_bar" } - }); - - _factory = _factory.WithLabels(new Dictionary - { - { "static_gaa", "static_bar" }, - { "static_gaa1", "static_bar" }, - { "static_gaa2", "static_bar" }, - { "static_gaa3", "static_bar" }, - { "static_gaa4", "static_bar" }, - { "static_gaa5", "static_bar" }, - }); + var metric = _factory.CreateCounter(_metricNames[i], _help, _labels, _counterConfiguration); + + for (var repeat = 0; repeat < RepeatCount; repeat++) + metric.WithLabels(_labels).Inc(); } - } + } - // We use the same strings both for the names and the values. - private static readonly string[] _labels = new[] { "foo", "bar", "baz" }; + [Benchmark] + public void Gauge() + { + for (var dupe = 0; dupe < DuplicateCount; dupe++) + for (var i = 0; i < _metricCount; i++) + { + var metric = _factory.CreateGauge(_metricNames[i], _help, _labels, _gaugeConfiguration); - private CounterConfiguration _counterConfiguration = CounterConfiguration.Default; - private GaugeConfiguration _gaugeConfiguration = GaugeConfiguration.Default; - private SummaryConfiguration _summaryConfiguration = SummaryConfiguration.Default; - private HistogramConfiguration _histogramConfiguration = HistogramConfiguration.Default; + for (var repeat = 0; repeat < RepeatCount; repeat++) + metric.WithLabels(_labels).Inc(); + } + } - [Benchmark] - public void Counter_Many() - { - for (var dupe = 0; dupe < DuplicateCount; dupe++) - for (var i = 0; i < _metricCount; i++) - { - var metric = _factory.CreateCounter(_metricNames[i], _help, _labels, _counterConfiguration); - - for (var repeat = 0; repeat < RepeatCount; repeat++) - metric.WithLabels(_labels).Inc(); - } - } + [Benchmark] + public void Summary() + { + for (var dupe = 0; dupe < DuplicateCount; dupe++) + for (var i = 0; i < _metricCount; i++) + { + var metric = _factory.CreateSummary(_metricNames[i], _help, _labels, _summaryConfiguration); - [Benchmark] - public void Gauge_Many() - { - for (var dupe = 0; dupe < DuplicateCount; dupe++) - for (var i = 0; i < _metricCount; i++) - { - var metric = _factory.CreateGauge(_metricNames[i], _help, _labels, _gaugeConfiguration); - - for (var repeat = 0; repeat < RepeatCount; repeat++) - metric.WithLabels(_labels).Inc(); - } - } + for (var repeat = 0; repeat < RepeatCount; repeat++) + metric.WithLabels(_labels).Observe(123); + } + } - [Benchmark] - public void Summary_Many() - { - for (var dupe = 0; dupe < DuplicateCount; dupe++) - for (var i = 0; i < _metricCount; i++) - { - var metric = _factory.CreateSummary(_metricNames[i], _help, _labels, _summaryConfiguration); - - for (var repeat = 0; repeat < RepeatCount; repeat++) - metric.WithLabels(_labels).Observe(123); - } - } + [Benchmark] + public void Histogram() + { + for (var dupe = 0; dupe < DuplicateCount; dupe++) + for (var i = 0; i < _metricCount; i++) + { + var metric = _factory.CreateHistogram(_metricNames[i], _help, _labels, _histogramConfiguration); - [Benchmark] - public void Histogram_Many() - { - for (var dupe = 0; dupe < DuplicateCount; dupe++) - for (var i = 0; i < _metricCount; i++) - { - var metric = _factory.CreateHistogram(_metricNames[i], _help, _labels, _histogramConfiguration); - - for (var repeat = 0; repeat < RepeatCount; repeat++) - metric.WithLabels(_labels).Observe(123); - } - } + for (var repeat = 0; repeat < RepeatCount; repeat++) + metric.WithLabels(_labels).Observe(123); + } } } From 9f0752301601d0b33de33639509f3b8716f74cf2 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Fri, 24 Nov 2023 15:02:06 +0200 Subject: [PATCH 145/230] Reduce CPU and memory use during metric registration --- Prometheus/CollectorFamily.cs | 96 +++++++++++++++++++++++++++++++-- Prometheus/CollectorRegistry.cs | 16 +++--- 2 files changed, 99 insertions(+), 13 deletions(-) diff --git a/Prometheus/CollectorFamily.cs b/Prometheus/CollectorFamily.cs index 7e8e6aa4..e37c92b9 100644 --- a/Prometheus/CollectorFamily.cs +++ b/Prometheus/CollectorFamily.cs @@ -1,4 +1,4 @@ -using System.Collections.Concurrent; +using System.Buffers; namespace Prometheus; @@ -6,7 +6,8 @@ internal sealed class CollectorFamily { public Type CollectorType { get; } - public ConcurrentDictionary Collectors { get; } = new(); + private readonly Dictionary _collectors = new(); + private readonly ReaderWriterLockSlim _lock = new(); public CollectorFamily(Type collectorType) { @@ -15,13 +16,98 @@ public CollectorFamily(Type collectorType) internal async Task CollectAndSerializeAsync(IMetricsSerializer serializer, CancellationToken cancel) { + // The first family member we serialize requires different serialization from the others. bool isFirst = true; - // Iterate the pairs to avoid a defensive copy. - foreach (var pair in Collectors) + await ForEachCollectorAsync(async (collector, c) => { - await pair.Value.CollectAndSerializeAsync(serializer, isFirst, cancel); + await collector.CollectAndSerializeAsync(serializer, isFirst, cancel); isFirst = false; + }, cancel); + } + + internal Collector GetOrAdd(CollectorIdentity identity, in CollectorRegistry.CollectorInitializer initializer) + where TCollector : Collector + where TConfiguration : MetricConfiguration + { + // First we try just holding a read lock. This is the happy path. + _lock.EnterReadLock(); + + try + { + if (_collectors.TryGetValue(identity, out var collector)) + return collector; + } + finally + { + _lock.ExitReadLock(); + } + + // Then we grab a write lock. This is the slow path. It could still be that someone beats us to it! + + _lock.EnterWriteLock(); + + try + { + if (_collectors.TryGetValue(identity, out var collector)) + return collector; + + var newCollector = initializer.CreateInstance(); + _collectors.Add(identity, newCollector); + return newCollector; + } + finally + { + _lock.ExitWriteLock(); + } + } + + internal void ForEachCollector(Action action) + { + _lock.EnterReadLock(); + + try + { + foreach (var collector in _collectors.Values) + action(collector); + } + finally + { + _lock.ExitReadLock(); + } + } + + internal async Task ForEachCollectorAsync(Func func, CancellationToken cancel) + { + // This could potentially take nontrivial time, as we are serializing to a stream (potentially, a network stream). + // Therefore we operate on a defensive copy in a reused buffer. + Collector[] buffer; + + _lock.EnterReadLock(); + + var collectorCount = _collectors.Count; + buffer = ArrayPool.Shared.Rent(collectorCount); + + try + { + try + { + _collectors.Values.CopyTo(buffer, 0); + } + finally + { + _lock.ExitReadLock(); + } + + for (var i = 0; i < collectorCount; i++) + { + var collector = buffer[i]; + await func(collector, cancel); + } + } + finally + { + ArrayPool.Shared.Return(buffer, clearArray: true); } } } diff --git a/Prometheus/CollectorRegistry.cs b/Prometheus/CollectorRegistry.cs index 8e7eb06f..243029c9 100644 --- a/Prometheus/CollectorRegistry.cs +++ b/Prometheus/CollectorRegistry.cs @@ -154,7 +154,7 @@ public Task CollectAndExportAsTextAsync(Stream to, ExpositionFormat format, Canc // We pass this thing to GetOrAdd to avoid allocating a collector or a closure. // This reduces memory usage in situations where the collector is already registered. - internal readonly struct CollectorInitializer + internal readonly ref struct CollectorInitializer where TCollector : Collector where TConfiguration : MetricConfiguration { @@ -182,7 +182,7 @@ public CollectorInitializer(CreateInstanceDelegate createInstance, string name, _exemplarBehavior = exemplarBehavior; } - public TCollector CreateInstance(CollectorIdentity _) => _createInstance(_name, _help, _instanceLabelNames, _staticLabels, _configuration, _exemplarBehavior); + public TCollector CreateInstance() => _createInstance(_name, _help, _instanceLabelNames, _staticLabels, _configuration, _exemplarBehavior); public delegate TCollector CreateInstanceDelegate(string name, string help, StringSequence instanceLabelNames, LabelSequence staticLabels, TConfiguration configuration, ExemplarBehavior exemplarBehavior); } @@ -202,10 +202,7 @@ internal TCollector GetOrAdd(in CollectorInitializer var collectorIdentity = new CollectorIdentity(initializer.InstanceLabelNames, initializer.StaticLabels); - if (family.Collectors.TryGetValue(collectorIdentity, out var existing)) - return (TCollector)existing; - - return (TCollector)family.Collectors.GetOrAdd(collectorIdentity, initializer.CreateInstance); + return (TCollector)family.GetOrAdd(collectorIdentity, initializer); } private CollectorFamily GetOrAddCollectorFamily(in CollectorInitializer initializer) @@ -370,12 +367,15 @@ private void UpdateRegistryMetrics() { bool hadMatchingType = false; - foreach (var collector in family.Collectors.Values.Where(c => c.Type == type)) + family.ForEachCollector(collector => { + if (collector.Type != type) + return; + hadMatchingType = true; instances += collector.ChildCount; timeseries += collector.TimeseriesCount; - } + }); if (hadMatchingType) families++; From 55165b55913fd2716e2ac780d4103b8013b6f41f Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Fri, 24 Nov 2023 15:21:02 +0200 Subject: [PATCH 146/230] Reduce CPU and memory cost of metric creation --- Prometheus/CollectorRegistry.cs | 71 +++++++++++++++++++++++++++++---- 1 file changed, 63 insertions(+), 8 deletions(-) diff --git a/Prometheus/CollectorRegistry.cs b/Prometheus/CollectorRegistry.cs index 243029c9..d714f95d 100644 --- a/Prometheus/CollectorRegistry.cs +++ b/Prometheus/CollectorRegistry.cs @@ -1,4 +1,5 @@ -using System.Collections.Concurrent; +using System.Buffers; +using System.Collections.Concurrent; using System.Diagnostics; namespace Prometheus; @@ -220,16 +221,41 @@ static CollectorFamily ValidateFamily(CollectorFamily candidate) return candidate; } - if (_families.TryGetValue(initializer.Name, out var existing)) - return ValidateFamily(existing); + // First try to get the family with only a read lock, with the assumption that it might already exist and therefore we do not need an expensive write lock. + _familiesLock.EnterReadLock(); - var collector = _families.GetOrAdd(initializer.Name, new CollectorFamily(typeof(TCollector))); - return ValidateFamily(collector); + try + { + if (_families.TryGetValue(initializer.Name, out var existing)) + return ValidateFamily(existing); + } + finally + { + _familiesLock.ExitReadLock(); + } + + // It does not exist. OK, just create it. + _familiesLock.EnterWriteLock(); + + try + { + if (_families.TryGetValue(initializer.Name, out var existing)) + return ValidateFamily(existing); + + var newFamily = new CollectorFamily(typeof(TCollector)); + _families.Add(initializer.Name, newFamily); + return newFamily; + } + finally + { + _familiesLock.ExitWriteLock(); + } } // Each collector family has an identity (the base name of the metric, in Prometheus format) and any number of collectors within. // Different collectors in the same family may have different sets of labels (static and instance) depending on how they were created. - private readonly ConcurrentDictionary _families = new(StringComparer.Ordinal); + private readonly Dictionary _families = new(StringComparer.Ordinal); + private readonly ReaderWriterLockSlim _familiesLock = new(); internal void SetBeforeFirstCollectCallback(Action a) { @@ -268,8 +294,37 @@ internal async Task CollectAndSerializeAsync(IMetricsSerializer serializer, Canc UpdateRegistryMetrics(); - foreach (var collector in _families.Values) - await collector.CollectAndSerializeAsync(serializer, cancel); + // This could potentially take nontrivial time, as we are serializing to a stream (potentially, a network stream). + // Therefore we operate on a defensive copy in a reused buffer. + CollectorFamily[] buffer; + + _familiesLock.EnterReadLock(); + + var familiesCount = _families.Count; + buffer = ArrayPool.Shared.Rent(familiesCount); + + try + { + try + { + _families.Values.CopyTo(buffer, 0); + } + finally + { + _familiesLock.ExitReadLock(); + } + + for (var i = 0; i < familiesCount; i++) + { + var family = buffer[i]; + await family.CollectAndSerializeAsync(serializer, cancel); + } + } + finally + { + ArrayPool.Shared.Return(buffer, clearArray: true); + } + await serializer.WriteEnd(cancel); await serializer.FlushAsync(cancel); } From c50c8a2c15f171cc250ae37875adfbd2826663dc Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Fri, 24 Nov 2023 15:45:49 +0200 Subject: [PATCH 147/230] Reduce CPU and memory consumption during metric creation --- Prometheus/Collector.cs | 135 ++++++++++++++++-- .../RequestCountMiddlewareTests.cs | 2 +- 2 files changed, 121 insertions(+), 16 deletions(-) diff --git a/Prometheus/Collector.cs b/Prometheus/Collector.cs index 4c99e566..b59d249a 100644 --- a/Prometheus/Collector.cs +++ b/Prometheus/Collector.cs @@ -1,4 +1,4 @@ -using System.Collections.Concurrent; +using System.Buffers; using System.ComponentModel; using System.Text.RegularExpressions; @@ -126,7 +126,8 @@ public abstract class Collector : Collector, ICollector where TChild : ChildBase { // Keyed by the instance labels (not by flattened labels!). - private readonly ConcurrentDictionary _labelledMetrics = new(); + private readonly Dictionary _children = new(); + private readonly ReaderWriterLockSlim _childrenLock = new(); // Lazy-initialized since not every collector will use a child with no labels. // Lazy instance will be replaced if the unlabelled timeseries is removed. @@ -167,7 +168,16 @@ public void RemoveLabelled(params string[] labelValues) internal override void RemoveLabelled(LabelSequence labels) { - _labelledMetrics.TryRemove(labels, out _); + _childrenLock.EnterWriteLock(); + + try + { + _children.Remove(labels); + } + finally + { + _childrenLock.ExitWriteLock(); + } if (labels.Length == 0) { @@ -182,7 +192,22 @@ private Lazy GetUnlabelledLazyInitializer() return new Lazy(() => GetOrAddLabelled(LabelSequence.Empty)); } - internal override int ChildCount => _labelledMetrics.Count; + internal override int ChildCount + { + get + { + _childrenLock.EnterReadLock(); + + try + { + return _children.Count; + } + finally + { + _childrenLock.ExitReadLock(); + } + } + } /// /// Gets the instance-specific label values of all labelled instances of the collector. @@ -193,13 +218,38 @@ private Lazy GetUnlabelledLazyInitializer() /// public IEnumerable GetAllLabelValues() { - foreach (var labels in _labelledMetrics.Keys) - { - if (labels.Length == 0) - continue; // We do not return the "unlabelled" label set. + // We are yielding here so make a defensive copy so we do not hold locks for a long time. + // We reuse this buffer, so it should be relatively harmless in the long run. + LabelSequence[] buffer; + + _childrenLock.EnterReadLock(); - // Defensive copy. - yield return labels.Values.ToArray(); + var childCount = _children.Count; + buffer = ArrayPool.Shared.Rent(childCount); + + try + { + try + { + _children.Keys.CopyTo(buffer, 0); + } + finally + { + _childrenLock.ExitReadLock(); + } + + foreach (var labels in buffer) + { + if (labels.Length == 0) + continue; // We do not return the "unlabelled" label set. + + // Defensive copy. + yield return labels.Values.ToArray(); + } + } + finally + { + ArrayPool.Shared.Return(buffer); } } @@ -209,7 +259,36 @@ private TChild GetOrAddLabelled(LabelSequence instanceLabels) // Order of labels matterns in data creation, although does not matter when the exported data set is imported later. // If we somehow end up registering the same metric with the same label names in different order, we will publish it twice, in two orders... // That is not ideal but also not that big of a deal to justify a lookup every time a metric instance is registered. - return _labelledMetrics.GetOrAdd(instanceLabels, _createdLabelledChildFunc); + + // First try to find an existing instance. This is the fast path, if we are re-looking-up an existing one. + _childrenLock.EnterReadLock(); + + try + { + if (_children.TryGetValue(instanceLabels, out var existing)) + return existing; + } + finally + { + _childrenLock.ExitReadLock(); + } + + // If no existing one found, grab the write lock and create a new one if needed. + _childrenLock.EnterWriteLock(); + + try + { + if (_children.TryGetValue(instanceLabels, out var existing)) + return existing; + + var newChild = _createdLabelledChildFunc(instanceLabels); + _children.Add(instanceLabels, newChild); + return newChild; + } + finally + { + _childrenLock.ExitWriteLock(); + } } private TChild CreateLabelledChild(LabelSequence instanceLabels) @@ -225,8 +304,9 @@ private TChild CreateLabelledChild(LabelSequence instanceLabels) /// /// For tests that want to see what instance-level label values were used when metrics were created. + /// This is for testing only, so does not respect locks - do not use this in concurrent context. /// - internal LabelSequence[] GetAllInstanceLabels() => _labelledMetrics.Select(p => p.Key).ToArray(); + internal LabelSequence[] GetAllInstanceLabelsUnsafe() => _children.Keys.ToArray(); internal Collector(string name, string help, StringSequence instanceLabelNames, LabelSequence staticLabels, bool suppressInitialValue, ExemplarBehavior exemplarBehavior) : base(name, help, instanceLabelNames, staticLabels) @@ -252,10 +332,35 @@ internal override async Task CollectAndSerializeAsync(IMetricsSerializer seriali if (writeFamilyDeclaration) await serializer.WriteFamilyDeclarationAsync(Name, NameBytes, HelpBytes, Type, TypeBytes, cancel); - // We iterate the pairs to avoid allocating a temporary array from .Values. - foreach (var pair in _labelledMetrics) + // This could potentially take nontrivial time, as we are serializing to a stream (potentially, a network stream). + // Therefore we operate on a defensive copy in a reused buffer. + TChild[] buffer; + + _childrenLock.EnterReadLock(); + + var childCount = _children.Count; + buffer = ArrayPool.Shared.Rent(childCount); + + try + { + try + { + _children.Values.CopyTo(buffer, 0); + } + finally + { + _childrenLock.ExitReadLock(); + } + + for (var i = 0; i < childCount; i++) + { + var child = buffer[i]; + await child.CollectAndSerializeAsync(serializer, cancel); + } + } + finally { - await pair.Value.CollectAndSerializeAsync(serializer, cancel); + ArrayPool.Shared.Return(buffer, clearArray: true); } } diff --git a/Tests.NetCore/GrpcExporter/RequestCountMiddlewareTests.cs b/Tests.NetCore/GrpcExporter/RequestCountMiddlewareTests.cs index 24a0bc69..b20a7242 100644 --- a/Tests.NetCore/GrpcExporter/RequestCountMiddlewareTests.cs +++ b/Tests.NetCore/GrpcExporter/RequestCountMiddlewareTests.cs @@ -78,7 +78,7 @@ public async Task Given_request_populates_labels_correctly() await _sut.Invoke(_httpContext); - var labels = counter.GetAllInstanceLabels().Single(); + var labels = counter.GetAllInstanceLabelsUnsafe().Single(); Assert.AreEqual( expectedService, GetLabelValueOrDefault(labels, GrpcRequestLabelNames.Service) From a59dc7998ab597f62fe69725b7ce175d9aa7e545 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Fri, 24 Nov 2023 17:25:38 +0200 Subject: [PATCH 148/230] Lazy-initialize Collector members if they are not strictly required at creation time --- Prometheus/Collector.cs | 39 ++++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/Prometheus/Collector.cs b/Prometheus/Collector.cs index b59d249a..a7b9e551 100644 --- a/Prometheus/Collector.cs +++ b/Prometheus/Collector.cs @@ -7,6 +7,11 @@ namespace Prometheus; /// /// Base class for metrics, defining the basic informative API and the internal API. /// +/// +/// Many of the fields are lazy-initialized to ensure we only perform the memory allocation if and when we actually use them. +/// For some, it means rarely used members are never allocated at all (e.g. if you never inspect the set of label names, they are never allocated). +/// For others, it means they are allocated at first time of use (e.g. serialization-related fields are allocated when serializing the first time). +/// public abstract class Collector { /// @@ -14,21 +19,30 @@ public abstract class Collector /// public string Name { get; } - internal byte[] NameBytes { get; } + internal byte[] NameBytes => LazyInitializer.EnsureInitialized(ref _nameBytesValue, _nameBytesFunc)!; + private byte[]? _nameBytesValue; + private readonly Func _nameBytesFunc; + private byte[] NameBytesFactory() => PrometheusConstants.ExportEncoding.GetBytes(Name); /// /// The help text describing the metric for a human audience. /// public string Help { get; } - internal byte[] HelpBytes { get; } + internal byte[] HelpBytes => LazyInitializer.EnsureInitialized(ref _helpBytesValue, _helpBytesFunc)!; + private byte[]? _helpBytesValue; + private readonly Func _helpBytesFunc; + private byte[] HelpBytesFactory() => string.IsNullOrWhiteSpace(Help) ? Array.Empty() : PrometheusConstants.ExportEncoding.GetBytes(Help); /// /// Names of the instance-specific labels (name-value pairs) that apply to this metric. /// When the values are added to the names, you get a instance. /// Does not include any static label names (from metric configuration, factory or registry). /// - public string[] LabelNames => _instanceLabelNamesAsArrayLazy.Value; + public string[] LabelNames => LazyInitializer.EnsureInitialized(ref _labelNamesValue, _labelNamesFunc)!; + private string[]? _labelNamesValue; + private readonly Func _labelNamesFunc; + private string[] LabelNamesFactory() => InstanceLabelNames.ToArray(); internal StringSequence InstanceLabelNames; internal StringSequence FlattenedLabelNames; @@ -64,13 +78,13 @@ internal Collector(string name, string help, StringSequence instanceLabelNames, if (!MetricNameRegex.IsMatch(name)) throw new ArgumentException($"Metric name '{name}' does not match regex '{ValidMetricNameExpression}'."); + _nameBytesFunc = NameBytesFactory; + _helpBytesFunc = HelpBytesFactory; + _labelNamesFunc = LabelNamesFactory; + Name = name; - NameBytes = PrometheusConstants.ExportEncoding.GetBytes(Name); TypeBytes = TextSerializer.MetricTypeToBytes[Type]; Help = help; - HelpBytes = String.IsNullOrWhiteSpace(help) - ? Array.Empty() - : PrometheusConstants.ExportEncoding.GetBytes(help); InstanceLabelNames = instanceLabelNames; StaticLabels = staticLabels; @@ -98,15 +112,6 @@ internal Collector(string name, string help, StringSequence instanceLabelNames, // Here we check for label name collision, ensuring that the same label name is not defined twice on any label inheritance level. if (uniqueLabelNames.Count != FlattenedLabelNames.Length) throw new InvalidOperationException("The set of label names includes duplicates: " + string.Join(", ", FlattenedLabelNames.ToArray())); - - _instanceLabelNamesAsArrayLazy = new Lazy(GetInstanceLabelNamesAsStringArray); - } - - private readonly Lazy _instanceLabelNamesAsArrayLazy; - - private string[] GetInstanceLabelNamesAsStringArray() - { - return InstanceLabelNames.ToArray(); } internal static void ValidateLabelName(string labelName) @@ -374,7 +379,7 @@ private void EnsureUnlabelledMetricCreatedIfNoLabels() // If there are no label names then clearly this metric is supposed to be used unlabelled, so create it. // Otherwise, we allow unlabelled metrics to be used if the user explicitly does it but omit them by default. - if (!_unlabelledLazy.IsValueCreated && !LabelNames.Any()) + if (!_unlabelledLazy.IsValueCreated && InstanceLabelNames.Length == 0) GetOrAddLabelled(LabelSequence.Empty); } From 47623a6f311d7da275af035a89f17c30e4499774 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Fri, 24 Nov 2023 17:25:51 +0200 Subject: [PATCH 149/230] Remove LabelSequence.Length as it was duplicating an existing field --- Prometheus/LabelSequence.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Prometheus/LabelSequence.cs b/Prometheus/LabelSequence.cs index 346765ab..475ee048 100644 --- a/Prometheus/LabelSequence.cs +++ b/Prometheus/LabelSequence.cs @@ -10,7 +10,7 @@ public readonly StringSequence Names; public readonly StringSequence Values; - public int Length { get; } + public int Length => Names.Length; private LabelSequence(StringSequence names, StringSequence values) { @@ -20,8 +20,6 @@ private LabelSequence(StringSequence names, StringSequence values) Names = names; Values = values; - Length = names.Length; - _hashCode = CalculateHashCode(); } From ee49691179cd65ae3539b4a03b86bd684a7659c1 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Fri, 24 Nov 2023 17:34:24 +0200 Subject: [PATCH 150/230] Lazy-initialize more fields so the cost is paid during serialization, not in the code creating metrics --- Prometheus/ChildBase.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Prometheus/ChildBase.cs b/Prometheus/ChildBase.cs index 57a4e79d..ab181e9d 100644 --- a/Prometheus/ChildBase.cs +++ b/Prometheus/ChildBase.cs @@ -1,5 +1,3 @@ -using System.Diagnostics; - namespace Prometheus; /// @@ -9,10 +7,11 @@ public abstract class ChildBase : ICollectorChild, IDisposable { internal ChildBase(Collector parent, LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish, ExemplarBehavior exemplarBehavior) { + _flattenedLabelsFunc = FlattenedLabelsFactory; + Parent = parent; InstanceLabels = instanceLabels; FlattenedLabels = flattenedLabels; - FlattenedLabelsBytes = flattenedLabels.Serialize(); _publish = publish; _exemplarBehavior = exemplarBehavior; } @@ -66,7 +65,10 @@ public void Remove() /// internal LabelSequence FlattenedLabels { get; } - internal byte[] FlattenedLabelsBytes { get; } + internal byte[] FlattenedLabelsBytes => LazyInitializer.EnsureInitialized(ref _flattenedLabelsBytes, _flattenedLabelsFunc)!; + private byte[]? _flattenedLabelsBytes; + private readonly Func _flattenedLabelsFunc; + private byte[] FlattenedLabelsFactory() => FlattenedLabels.Serialize(); internal readonly Collector Parent; From fdb8793c2c7cc33a0cf53167b3fceb351e90c5bd Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Fri, 24 Nov 2023 22:42:57 +0200 Subject: [PATCH 151/230] Reduce Func allocations by using non-capturing lazy initialization --- Prometheus/ChildBase.cs | 10 +- Prometheus/Collector.cs | 36 +++--- Prometheus/NonCapturingLazyInitializer.cs | 140 ++++++++++++++++++++++ 3 files changed, 165 insertions(+), 21 deletions(-) create mode 100644 Prometheus/NonCapturingLazyInitializer.cs diff --git a/Prometheus/ChildBase.cs b/Prometheus/ChildBase.cs index ab181e9d..5ba5d35a 100644 --- a/Prometheus/ChildBase.cs +++ b/Prometheus/ChildBase.cs @@ -7,8 +7,6 @@ public abstract class ChildBase : ICollectorChild, IDisposable { internal ChildBase(Collector parent, LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish, ExemplarBehavior exemplarBehavior) { - _flattenedLabelsFunc = FlattenedLabelsFactory; - Parent = parent; InstanceLabels = instanceLabels; FlattenedLabels = flattenedLabels; @@ -65,10 +63,10 @@ public void Remove() /// internal LabelSequence FlattenedLabels { get; } - internal byte[] FlattenedLabelsBytes => LazyInitializer.EnsureInitialized(ref _flattenedLabelsBytes, _flattenedLabelsFunc)!; + internal byte[] FlattenedLabelsBytes => NonCapturingLazyInitializer.EnsureInitialized(ref _flattenedLabelsBytes, this, _assignFlattenedLabelsBytesFunc)!; private byte[]? _flattenedLabelsBytes; - private readonly Func _flattenedLabelsFunc; - private byte[] FlattenedLabelsFactory() => FlattenedLabels.Serialize(); + private static readonly Action _assignFlattenedLabelsBytesFunc; + private static void AssignFlattenedLabelsBytes(ChildBase instance) => instance._flattenedLabelsBytes = instance.FlattenedLabels.Serialize(); internal readonly Collector Parent; @@ -180,6 +178,8 @@ protected void MarkNewExemplarHasBeenRecorded() static ChildBase() { + _assignFlattenedLabelsBytesFunc = AssignFlattenedLabelsBytes; + Metrics.DefaultRegistry.OnStartCollectingRegistryMetrics(delegate { ExemplarsRecorded = Metrics.CreateCounter("prometheus_net_exemplars_recorded_total", "Number of exemplars that were accepted into in-memory storage in the prometheus-net SDK."); diff --git a/Prometheus/Collector.cs b/Prometheus/Collector.cs index a7b9e551..aa5e4de1 100644 --- a/Prometheus/Collector.cs +++ b/Prometheus/Collector.cs @@ -19,30 +19,31 @@ public abstract class Collector /// public string Name { get; } - internal byte[] NameBytes => LazyInitializer.EnsureInitialized(ref _nameBytesValue, _nameBytesFunc)!; - private byte[]? _nameBytesValue; - private readonly Func _nameBytesFunc; - private byte[] NameBytesFactory() => PrometheusConstants.ExportEncoding.GetBytes(Name); + internal byte[] NameBytes => NonCapturingLazyInitializer.EnsureInitialized(ref _nameBytes, this, _assignNameBytesFunc)!; + private byte[]? _nameBytes; + private static readonly Action _assignNameBytesFunc; + private static void AssignNameBytes(Collector instance) => instance._nameBytes = PrometheusConstants.ExportEncoding.GetBytes(instance.Name); /// /// The help text describing the metric for a human audience. /// public string Help { get; } - internal byte[] HelpBytes => LazyInitializer.EnsureInitialized(ref _helpBytesValue, _helpBytesFunc)!; - private byte[]? _helpBytesValue; - private readonly Func _helpBytesFunc; - private byte[] HelpBytesFactory() => string.IsNullOrWhiteSpace(Help) ? Array.Empty() : PrometheusConstants.ExportEncoding.GetBytes(Help); + internal byte[] HelpBytes => NonCapturingLazyInitializer.EnsureInitialized(ref _helpBytes, this, _assignHelpBytesFunc)!; + private byte[]? _helpBytes; + private static readonly Action _assignHelpBytesFunc; + private static void AssignHelpBytes(Collector instance) => + instance._helpBytes = string.IsNullOrWhiteSpace(instance.Help) ? [] : PrometheusConstants.ExportEncoding.GetBytes(instance.Help); /// /// Names of the instance-specific labels (name-value pairs) that apply to this metric. /// When the values are added to the names, you get a instance. /// Does not include any static label names (from metric configuration, factory or registry). /// - public string[] LabelNames => LazyInitializer.EnsureInitialized(ref _labelNamesValue, _labelNamesFunc)!; - private string[]? _labelNamesValue; - private readonly Func _labelNamesFunc; - private string[] LabelNamesFactory() => InstanceLabelNames.ToArray(); + public string[] LabelNames => NonCapturingLazyInitializer.EnsureInitialized(ref _labelNames, this, _assignLabelNamesFunc)!; + private string[]? _labelNames; + private static readonly Action _assignLabelNamesFunc; + private static void AssignLabelNames(Collector instance) => instance._labelNames = instance.InstanceLabelNames.ToArray(); internal StringSequence InstanceLabelNames; internal StringSequence FlattenedLabelNames; @@ -73,15 +74,18 @@ public abstract class Collector private static readonly Regex LabelNameRegex = new Regex(ValidLabelNameExpression, RegexOptions.Compiled); private static readonly Regex ReservedLabelRegex = new Regex(ReservedLabelNameExpression, RegexOptions.Compiled); + static Collector() + { + _assignNameBytesFunc = AssignNameBytes; + _assignHelpBytesFunc = AssignHelpBytes; + _assignLabelNamesFunc = AssignLabelNames; + } + internal Collector(string name, string help, StringSequence instanceLabelNames, LabelSequence staticLabels) { if (!MetricNameRegex.IsMatch(name)) throw new ArgumentException($"Metric name '{name}' does not match regex '{ValidMetricNameExpression}'."); - _nameBytesFunc = NameBytesFactory; - _helpBytesFunc = HelpBytesFactory; - _labelNamesFunc = LabelNamesFactory; - Name = name; TypeBytes = TextSerializer.MetricTypeToBytes[Type]; Help = help; diff --git a/Prometheus/NonCapturingLazyInitializer.cs b/Prometheus/NonCapturingLazyInitializer.cs new file mode 100644 index 00000000..67b23d40 --- /dev/null +++ b/Prometheus/NonCapturingLazyInitializer.cs @@ -0,0 +1,140 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; + +namespace Prometheus; + +// Copy-pasted from https://github.com/dotnet/efcore/blob/main/src/Shared/NonCapturingLazyInitializer.cs +// Crudely modified to inline dependencies and reduce functionality down to .NET Fx compatible level. +internal static class NonCapturingLazyInitializer +{ + public static TValue EnsureInitialized( + ref TValue? target, + TParam param, + Func valueFactory) + where TValue : class + { + var tmp = Volatile.Read(ref target); + if (tmp != null) + { + DebugAssert(target != null, $"target was null in {nameof(EnsureInitialized)} after check"); + return tmp; + } + + Interlocked.CompareExchange(ref target, valueFactory(param), null); + + return target; + } + + public static TValue EnsureInitialized( + ref TValue? target, + TParam1 param1, + TParam2 param2, + Func valueFactory) + where TValue : class + { + var tmp = Volatile.Read(ref target); + if (tmp != null) + { + DebugAssert(target != null, $"target was null in {nameof(EnsureInitialized)} after check"); + return tmp; + } + + Interlocked.CompareExchange(ref target, valueFactory(param1, param2), null); + + return target; + } + + public static TValue EnsureInitialized( + ref TValue? target, + TParam1 param1, + TParam2 param2, + TParam3 param3, + Func valueFactory) + where TValue : class + { + var tmp = Volatile.Read(ref target); + if (tmp != null) + { + DebugAssert(target != null, $"target was null in {nameof(EnsureInitialized)} after check"); + return tmp; + } + + Interlocked.CompareExchange(ref target, valueFactory(param1, param2, param3), null); + + return target; + } + + public static TValue EnsureInitialized( + ref TValue target, + ref bool initialized, + TParam param, + Func valueFactory) + where TValue : class? + { + var alreadyInitialized = Volatile.Read(ref initialized); + if (alreadyInitialized) + { + var value = Volatile.Read(ref target); + DebugAssert(target != null, $"target was null in {nameof(EnsureInitialized)} after check"); + DebugAssert(value != null, $"value was null in {nameof(EnsureInitialized)} after check"); + return value; + } + + Volatile.Write(ref target, valueFactory(param)); + Volatile.Write(ref initialized, true); + + return target; + } + + public static TValue EnsureInitialized( + ref TValue? target, + TValue value) + where TValue : class + { + var tmp = Volatile.Read(ref target); + if (tmp != null) + { + DebugAssert(target != null, $"target was null in {nameof(EnsureInitialized)} after check"); + return tmp; + } + + Interlocked.CompareExchange(ref target, value, null); + + return target; + } + + public static TValue EnsureInitialized( + ref TValue? target, + TParam param, + Action valueFactory) + where TValue : class + { + var tmp = Volatile.Read(ref target); + if (tmp != null) + { + DebugAssert(target != null, $"target was null in {nameof(EnsureInitialized)} after check"); + return tmp; + } + + valueFactory(param); + + var tmp2 = Volatile.Read(ref target); + DebugAssert( + target != null && tmp2 != null, + $"{nameof(valueFactory)} did not initialize {nameof(target)} in {nameof(EnsureInitialized)}"); +#pragma warning disable CS8603 // Possible null reference return. + return tmp2; +#pragma warning restore CS8603 // Possible null reference return. + } + + [Conditional("DEBUG")] + private static void DebugAssert(bool condition, string message) + { + if (!condition) + { + throw new Exception($"Check.DebugAssert failed: {message}"); + } + } +} From ea4a6bb1a5517d33ffed5c4576c9c1ea5798595e Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Sat, 25 Nov 2023 09:40:23 +0200 Subject: [PATCH 152/230] Use AVX-optimized histogram measurement algorithm if supported by CPU for 10-35% CPU time improvement --- Benchmark.NetCore/MeasurementBenchmarks.cs | 118 ++++++--------------- Prometheus/Histogram.cs | 64 +++++++++-- 2 files changed, 87 insertions(+), 95 deletions(-) diff --git a/Benchmark.NetCore/MeasurementBenchmarks.cs b/Benchmark.NetCore/MeasurementBenchmarks.cs index 9e640b21..7c3e86db 100644 --- a/Benchmark.NetCore/MeasurementBenchmarks.cs +++ b/Benchmark.NetCore/MeasurementBenchmarks.cs @@ -6,13 +6,7 @@ namespace Benchmark.NetCore; /// /// We take a bunch of measurements of each type of metric and show the cost. /// -/// -/// Total measurements = MeasurementCount * ThreadCount -/// [MemoryDiagnoser] -[ThreadingDiagnoser] -[InvocationCount(1)] // The implementation does not support multiple invocations. -[MinIterationCount(50), MaxIterationCount(200)] // Help stabilize measurements. public class MeasurementBenchmarks { public enum MetricType @@ -23,15 +17,9 @@ public enum MetricType Summary } - [Params(200_000)] + [Params(1_000_000)] public int MeasurementCount { get; set; } - [Params(1, 16)] - public int ThreadCount { get; set; } - - [Params(MetricType.Counter, MetricType.Gauge, MetricType.Histogram, MetricType.Summary)] - public MetricType TargetMetricType { get; set; } - [Params(ExemplarMode.Auto, ExemplarMode.None, ExemplarMode.Provided)] public ExemplarMode Exemplars { get; set; } @@ -60,6 +48,7 @@ public enum ExemplarMode private readonly Gauge.Child _gauge; private readonly Summary.Child _summary; private readonly Histogram.Child _histogram; + private readonly Histogram.Child _wideHistogram; private readonly Exemplar.LabelKey _traceIdKey = Exemplar.Key("trace_id"); private readonly Exemplar.LabelKey _spanIdKey = Exemplar.Key("span_id"); @@ -72,6 +61,12 @@ public enum ExemplarMode private Exemplar.LabelPair _traceIdLabel; private Exemplar.LabelPair _spanIdLabel; + /// + /// The max value we observe for histograms, to give us coverage of all the histogram buckets + /// but not waste 90% of the benchmark on incrementing the +Inf bucket. + /// + private const int HistogramMaxValue = 32 * 1024; + public MeasurementBenchmarks() { _registry = Metrics.NewCustomRegistry(); @@ -92,7 +87,12 @@ public MeasurementBenchmarks() var histogramTemplate = _factory.CreateHistogram("histogram", "test histogram", new[] { "label" }, new HistogramConfiguration { // 1 ms to 32K ms, 16 buckets. Same as used in HTTP metrics by default. - Buckets = Histogram.ExponentialBuckets(0.001, 2, 16) + Buckets = Prometheus.Histogram.ExponentialBuckets(0.001, 2, 16) + }); + + var wideHistogramTemplate = _factory.CreateHistogram("wide_histogram", "test histogram", new[] { "label" }, new HistogramConfiguration + { + Buckets = Prometheus.Histogram.LinearBuckets(1, HistogramMaxValue / 128, 128) }); // We cache the children, as is typical usage. @@ -100,12 +100,14 @@ public MeasurementBenchmarks() _gauge = gaugeTemplate.WithLabels("label value"); _summary = summaryTemplate.WithLabels("label value"); _histogram = histogramTemplate.WithLabels("label value"); + _wideHistogram = wideHistogramTemplate.WithLabels("label value"); // We take a single measurement, to warm things up and avoid any first-call impact. _counter.Inc(); _gauge.Set(1); _summary.Observe(1); _histogram.Observe(1); + _wideHistogram.Observe(1); } [GlobalSetup] @@ -118,113 +120,55 @@ public void GlobalSetup() _spanIdLabel = _spanIdKey.WithValue(_spanIdValue); } - [IterationSetup] - public void Setup() - { - // We reuse the same registry for each iteration, as this represents typical (warmed up) usage. - - _threadReadyToStart = new ManualResetEventSlim[ThreadCount]; - _startThreads = new ManualResetEventSlim(); - _threads = new Thread[ThreadCount]; - - for (var i = 0; i < ThreadCount; i++) - { - _threadReadyToStart[i] = new(); - _threads[i] = new Thread(GetBenchmarkThreadEntryPoint()); - _threads[i].Name = $"Measurements #{i}"; - _threads[i].Start(i); - } - - // Wait for all threads to get ready. We will give them the go signal in the actual benchmark method. - foreach (var e in _threadReadyToStart) - e.Wait(); - } - - private ParameterizedThreadStart GetBenchmarkThreadEntryPoint() => TargetMetricType switch - { - MetricType.Counter => MeasurementThreadCounter, - MetricType.Gauge => MeasurementThreadGauge, - MetricType.Histogram => MeasurementThreadHistogram, - MetricType.Summary => MeasurementThreadSummary, - _ => throw new NotSupportedException() - }; - - [IterationCleanup] - public void Cleanup() - { - _startThreads.Dispose(); - - foreach (var e in _threadReadyToStart) - e.Dispose(); - } - - private ManualResetEventSlim[] _threadReadyToStart; - private ManualResetEventSlim _startThreads; - private Thread[] _threads; - - private void MeasurementThreadCounter(object state) + [Benchmark] + public void Counter() { var exemplarProvider = GetExemplarProvider(); - var threadIndex = (int)state; - - _threadReadyToStart[threadIndex].Set(); - _startThreads.Wait(); - for (var i = 0; i < MeasurementCount; i++) { _counter.Inc(exemplarProvider()); } } - private void MeasurementThreadGauge(object state) + [Benchmark] + public void Gauge() { - var threadIndex = (int)state; - - _threadReadyToStart[threadIndex].Set(); - _startThreads.Wait(); - for (var i = 0; i < MeasurementCount; i++) { _gauge.Set(i); } } - private void MeasurementThreadHistogram(object state) + [Benchmark] + public void Histogram() { var exemplarProvider = GetExemplarProvider(); - var threadIndex = (int)state; - - _threadReadyToStart[threadIndex].Set(); - _startThreads.Wait(); - for (var i = 0; i < MeasurementCount; i++) { _histogram.Observe(i, exemplarProvider()); } } - private void MeasurementThreadSummary(object state) + [Benchmark] + public void WideHistogram() { - var threadIndex = (int)state; - - _threadReadyToStart[threadIndex].Set(); - _startThreads.Wait(); + var exemplarProvider = GetExemplarProvider(); for (var i = 0; i < MeasurementCount; i++) { - _summary.Observe(i); + _wideHistogram.Observe(i, exemplarProvider()); } } [Benchmark] - public void MeasurementPerformance() + public void Summary() { - _startThreads.Set(); - - for (var i = 0; i < _threads.Length; i++) - _threads[i].Join(); + for (var i = 0; i < MeasurementCount; i++) + { + _summary.Observe(i); + } } private Func GetExemplarProvider() => Exemplars switch diff --git a/Prometheus/Histogram.cs b/Prometheus/Histogram.cs index 5347d081..c00794a6 100644 --- a/Prometheus/Histogram.cs +++ b/Prometheus/Histogram.cs @@ -1,3 +1,9 @@ +using System.Numerics; +#if NET7_0_OR_GREATER +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +#endif + namespace Prometheus; /// @@ -155,23 +161,65 @@ private void ObserveInternal(double val, long count, Exemplar? exemplar) exemplar ??= GetDefaultExemplar(val); + var bucketIndex = GetBucketIndex(val); + + _bucketCounts[bucketIndex].Add(count); + + if (exemplar != null) + RecordExemplar(exemplar, ref _exemplars[bucketIndex], val); + + _sum.Add(val * count); + + Publish(); + } + + private int GetBucketIndex(double val) + { +#if NET7_0_OR_GREATER + if (Avx.IsSupported) + return GetBucketIndexAvx(val); +#endif + for (int i = 0; i < _upperBounds.Length; i++) { if (val <= _upperBounds[i]) - { - _bucketCounts[i].Add(count); + return i; + } - if (exemplar != null) - RecordExemplar(exemplar, ref _exemplars[i], val); + throw new Exception("Unreachable code reached."); + } - break; - } +#if NET7_0_OR_GREATER + private int GetBucketIndexAvx(double val) + { + var remaining = _upperBounds.Length % Vector256.Count; + + for (int i = 0; i < _upperBounds.Length - remaining; i += Vector256.Count) + { + var boundVector = Vector256.Create(_upperBounds, i); + var valVector = Vector256.Create(val); + var mask = Avx.CompareLessThanOrEqual(valVector, boundVector); + + if (mask.Equals(Vector256.Zero)) + continue; + + // Condenses the vector into a 32-bit integer where each bit represents one vector element (so 1111 means "all true"). + var moveMask = Avx.MoveMask(mask); + + var indexInBlock = BitOperations.TrailingZeroCount(moveMask); + + return i + indexInBlock; } - _sum.Add(val * count); + for (int i = _upperBounds.Length - remaining; i < _upperBounds.Length; i++) + { + if (val <= _upperBounds[i]) + return i; + } - Publish(); + throw new Exception("Unreachable code reached."); } +#endif } internal override MetricType Type => MetricType.Histogram; From 42c06850d725675e9dbe4e20622de15a131f4d68 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Sat, 25 Nov 2023 10:28:54 +0200 Subject: [PATCH 153/230] Pre-align AVX buffers for even more speedup --- Prometheus/Histogram.cs | 78 ++++++++++++++++++++++++++++-------- Prometheus/Prometheus.csproj | 4 +- 2 files changed, 64 insertions(+), 18 deletions(-) diff --git a/Prometheus/Histogram.cs b/Prometheus/Histogram.cs index c00794a6..d0162e6e 100644 --- a/Prometheus/Histogram.cs +++ b/Prometheus/Histogram.cs @@ -1,4 +1,6 @@ using System.Numerics; +using System.Runtime.CompilerServices; + #if NET7_0_OR_GREATER using System.Runtime.Intrinsics; using System.Runtime.Intrinsics.X86; @@ -16,6 +18,16 @@ public sealed class Histogram : Collector, IHistogram private readonly double[] _buckets; +#if NET7_0_OR_GREATER + // For AVX, we need to align on 32 bytes and pin the memory. This is a buffer + // with extra items that we can "skip" when using the data, for alignment purposes. + private readonly double[] _bucketsAlignmentBuffer; + // How many items from the start to skip. + private readonly int _bucketsAlignmentBufferOffset; + + private const int AvxAlignBytes = 32; +#endif + // These labels go together with the buckets, so we do not need to allocate them for every child. private readonly CanonicalLabel[] _leLabels; @@ -54,6 +66,31 @@ internal Histogram(string name, string help, StringSequence instanceLabelNames, { _leLabels[i] = TextSerializer.EncodeValueAsCanonicalLabel(LeLabelName, _buckets[i]); } + +#if NET7_0_OR_GREATER + if (Avx.IsSupported) + { + _bucketsAlignmentBuffer = GC.AllocateUninitializedArray(_buckets.Length + (AvxAlignBytes / sizeof(double)), pinned: true); + + unsafe + { + var pointer = (nuint)Unsafe.AsPointer(ref _bucketsAlignmentBuffer[0]); + var pointerTooFarByBytes = pointer % AvxAlignBytes; + var bytesUntilNextAlignedPosition = (AvxAlignBytes - pointerTooFarByBytes) % AvxAlignBytes; + + if (bytesUntilNextAlignedPosition % sizeof(double) != 0) + throw new Exception("Unreachable code reached - all double[] allocations are expected to be at least 8-aligned."); + + _bucketsAlignmentBufferOffset = (int)(pointerTooFarByBytes / sizeof(double)); + } + + Array.Copy(_buckets, 0, _bucketsAlignmentBuffer, _bucketsAlignmentBufferOffset, _buckets.Length); + } + else + { + _bucketsAlignmentBuffer = []; + } +#endif } private protected override Child NewChild(LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish, ExemplarBehavior exemplarBehavior) @@ -68,11 +105,10 @@ internal Child(Histogram parent, LabelSequence instanceLabels, LabelSequence fla { Parent = parent; - _upperBounds = Parent._buckets; - _bucketCounts = new ThreadSafeLong[_upperBounds.Length]; + _bucketCounts = new ThreadSafeLong[Parent._buckets.Length]; - _exemplars = new ObservedExemplar[_upperBounds.Length]; - for (var i = 0; i < _upperBounds.Length; i++) + _exemplars = new ObservedExemplar[Parent._buckets.Length]; + for (var i = 0; i < Parent._buckets.Length; i++) { _exemplars[i] = ObservedExemplar.Empty; } @@ -82,7 +118,6 @@ internal Child(Histogram parent, LabelSequence instanceLabels, LabelSequence fla private ThreadSafeDouble _sum = new ThreadSafeDouble(0.0D); private readonly ThreadSafeLong[] _bucketCounts; - private readonly double[] _upperBounds; private static readonly byte[] SumSuffix = PrometheusConstants.ExportEncoding.GetBytes("sum"); private static readonly byte[] CountSuffix = PrometheusConstants.ExportEncoding.GetBytes("count"); private static readonly byte[] BucketSuffix = PrometheusConstants.ExportEncoding.GetBytes("bucket"); @@ -180,9 +215,9 @@ private int GetBucketIndex(double val) return GetBucketIndexAvx(val); #endif - for (int i = 0; i < _upperBounds.Length; i++) + for (int i = 0; i < Parent._buckets.Length; i++) { - if (val <= _upperBounds[i]) + if (val <= Parent._buckets[i]) return i; } @@ -190,30 +225,39 @@ private int GetBucketIndex(double val) } #if NET7_0_OR_GREATER - private int GetBucketIndexAvx(double val) + /// + /// AVX allows us to perform 4 comparisons at the same time when finding the right bucket to increment. + /// The total speedup is not 4x due to various overheads but it's still 10-30% (more for wider histograms). + /// + private unsafe int GetBucketIndexAvx(double val) { - var remaining = _upperBounds.Length % Vector256.Count; + // AVX operates on vectors of N buckets, so if the total is not divisible by N we need to check some of them manually. + var remaining = Parent._buckets.Length % Vector256.Count; - for (int i = 0; i < _upperBounds.Length - remaining; i += Vector256.Count) + for (int i = 0; i < Parent._buckets.Length - remaining; i += Vector256.Count) { - var boundVector = Vector256.Create(_upperBounds, i); + // The buckets are permanently pinned, no need to re-pin them here. + var boundPointer = (double*)Unsafe.AsPointer(ref Parent._bucketsAlignmentBuffer[Parent._bucketsAlignmentBufferOffset + i]); + var boundVector = Avx.LoadAlignedVector256(boundPointer); + var valVector = Vector256.Create(val); - var mask = Avx.CompareLessThanOrEqual(valVector, boundVector); - if (mask.Equals(Vector256.Zero)) - continue; + var mask = Avx.CompareLessThanOrEqual(valVector, boundVector); - // Condenses the vector into a 32-bit integer where each bit represents one vector element (so 1111 means "all true"). + // Condenses the mask vector into a 32-bit integer where one bit represents one vector element (so 1111000.. means "first 4 items true"). var moveMask = Avx.MoveMask(mask); var indexInBlock = BitOperations.TrailingZeroCount(moveMask); + if (indexInBlock == sizeof(int) * 8) + continue; // All bits are zero, so we did not find a match. + return i + indexInBlock; } - for (int i = _upperBounds.Length - remaining; i < _upperBounds.Length; i++) + for (int i = Parent._buckets.Length - remaining; i < Parent._buckets.Length; i++) { - if (val <= _upperBounds[i]) + if (val <= Parent._buckets[i]) return i; } diff --git a/Prometheus/Prometheus.csproj b/Prometheus/Prometheus.csproj index 78ff6711..addc6192 100644 --- a/Prometheus/Prometheus.csproj +++ b/Prometheus/Prometheus.csproj @@ -27,6 +27,8 @@ True 1591 + true + preview 9999 @@ -81,7 +83,7 @@ - + From 1bff7522464297c98b53b6985665f3eee608113a Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Sun, 26 Nov 2023 00:33:37 +0200 Subject: [PATCH 154/230] Optimize serialization performance, especially in cases where the output stream needs to be awaited --- Benchmark.NetCore/SerializationBenchmarks.cs | 136 ++++++- Benchmark.NetCore/YieldStream.cs | 119 ++++++ Prometheus/ChildBase.cs | 6 +- Prometheus/Collector.cs | 8 +- Prometheus/CollectorFamily.cs | 67 +++- Prometheus/Counter.cs | 7 +- Prometheus/Gauge.cs | 4 +- Prometheus/Histogram.cs | 5 +- Prometheus/IMetricsSerializer.cs | 10 +- Prometheus/LabelSequence.cs | 22 ++ Prometheus/ScrapeFailedException.cs | 30 +- Prometheus/Summary.cs | 7 +- Prometheus/TextSerializer.Net.cs | 352 ++++++++++++++++++ ...zer.cs => TextSerializer.NetStandardFx.cs} | 107 ++---- 14 files changed, 749 insertions(+), 131 deletions(-) create mode 100644 Benchmark.NetCore/YieldStream.cs create mode 100644 Prometheus/TextSerializer.Net.cs rename Prometheus/{TextSerializer.cs => TextSerializer.NetStandardFx.cs} (76%) diff --git a/Benchmark.NetCore/SerializationBenchmarks.cs b/Benchmark.NetCore/SerializationBenchmarks.cs index 9aeb2021..825aa1fa 100644 --- a/Benchmark.NetCore/SerializationBenchmarks.cs +++ b/Benchmark.NetCore/SerializationBenchmarks.cs @@ -1,4 +1,5 @@ -using BenchmarkDotNet.Attributes; +using System.IO.Pipes; +using BenchmarkDotNet.Attributes; using Prometheus; namespace Benchmark.NetCore; @@ -6,6 +7,32 @@ namespace Benchmark.NetCore; [MemoryDiagnoser] public class SerializationBenchmarks { + public enum OutputStreamType + { + /// + /// A null stream that just does nothing and immediately returns. + /// Low overhead but also unrealistic in terms of asynchronous I/O behavior. + /// + Null, + + /// + /// A stream that does nothing except yielding the task/thread to take up nonzero time. + /// Tries to increase the overhead from async/await task management that might occur. + /// + Yield, + + /// + /// A named pipe connection. Something halfway between null and a real network connection. + /// Named pipes appear to be super slow when dealing with small amounts of data, so optimizing + /// this scenario is valuable to ensure that we perform well with real network connections that + /// may have similar limitations (depending on OS and network stack). + /// + NamedPipe + } + + [Params(OutputStreamType.Null, OutputStreamType.NamedPipe, OutputStreamType.Yield)] + public OutputStreamType StreamType { get; set; } + // Metric -> Variant -> Label values private static readonly string[][][] _labelValueRows; @@ -13,7 +40,7 @@ public class SerializationBenchmarks private const int _variantCount = 100; private const int _labelCount = 5; - private const string _help = "arbitrary help message for metric, not relevant for benchmarking"; + private const string _help = "arbitrary help message for metric lorem ipsum dolor golor bolem"; static SerializationBenchmarks() { @@ -58,11 +85,8 @@ public SerializationBenchmarks() _summaries[metricIndex] = factory.CreateSummary($"summary{metricIndex:D2}", _help, _labelValueRows[metricIndex][0]); _histograms[metricIndex] = factory.CreateHistogram($"histogram{metricIndex:D2}", _help, _labelValueRows[metricIndex][0]); } - } - [GlobalSetup] - public void GenerateData() - { + // Genmerate some sample data so the metrics are not all zero-initialized. var exemplarLabelPair = Exemplar.Key("traceID").WithValue("bar"); for (var metricIndex = 0; metricIndex < _metricCount; metricIndex++) for (var variantIndex = 0; variantIndex < _variantCount; variantIndex++) @@ -74,15 +98,107 @@ public void GenerateData() } } + [GlobalSetup] + public void Setup() + { + if (StreamType == OutputStreamType.Null) + { + _outputStream = Stream.Null; + } + else if (StreamType == OutputStreamType.Yield) + { + _outputStream = YieldStream.Default; + } + else if (StreamType == OutputStreamType.NamedPipe) + { + var pipeName = StartStreamReader(); + + var pipeStream = new NamedPipeClientStream(".", pipeName.ToString(), PipeDirection.Out, PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly); + pipeStream.Connect(TimeSpan.FromSeconds(1)); + + _outputStream = pipeStream; + } + else + throw new NotSupportedException(); + } + + // Will be reused by all the iterations - the serializer does not take ownership nor close the stream. + private Stream _outputStream; + + // When this is cancelled, the output stream reader will stop listening for new connections and reading data from existing ones. + private readonly CancellationTokenSource _outputStreamReaderCts = new(); + + // We just read data into it, we do not care about the contents. + // While we do not expect concurrent access, it is fine if it does happen because this data is never consumed + private static readonly byte[] _readBuffer = new byte[1024]; + + /// + /// Starts listening on a random port on the loopback interface and returns the name of the created pipe stream. + /// + private Guid StartStreamReader() + { + var name = Guid.NewGuid(); + var server = new NamedPipeServerStream(name.ToString(), PipeDirection.In, NamedPipeServerStream.MaxAllowedServerInstances, PipeTransmissionMode.Byte, PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly); + var cancel = _outputStreamReaderCts.Token; + + _ = Task.Run(async delegate + { + try + { + while (!cancel.IsCancellationRequested) + { + await server.WaitForConnectionAsync(cancel); + Console.WriteLine("Received a connection."); + + try + { + while (!cancel.IsCancellationRequested) + { + var bytesRead = await server.ReadAsync(_readBuffer, cancel); + + if (bytesRead == 0) + break; + } + } + finally + { + server.Disconnect(); + } + } + } + catch (OperationCanceledException) when (cancel.IsCancellationRequested) + { + // Expected + } + catch (Exception ex) + { + Console.Error.WriteLine($"Unexpected exception in output stream reader: {ex}"); + } + finally + { + server.Dispose(); + } + }); + + return name; + } + [Benchmark] public async Task CollectAndSerialize() { - await _registry.CollectAndSerializeAsync(new TextSerializer(Stream.Null), default); + await _registry.CollectAndSerializeAsync(new TextSerializer(_outputStream), default); } - - [Benchmark] + + //[Benchmark] public async Task CollectAndSerializeOpenMetrics() { - await _registry.CollectAndSerializeAsync(new TextSerializer(Stream.Null, ExpositionFormat.OpenMetricsText), default); + await _registry.CollectAndSerializeAsync(new TextSerializer(_outputStream, ExpositionFormat.OpenMetricsText), default); + } + + [GlobalCleanup] + public void Cleanup() + { + _outputStreamReaderCts.Cancel(); + _outputStreamReaderCts.Dispose(); } } diff --git a/Benchmark.NetCore/YieldStream.cs b/Benchmark.NetCore/YieldStream.cs new file mode 100644 index 00000000..7670c59e --- /dev/null +++ b/Benchmark.NetCore/YieldStream.cs @@ -0,0 +1,119 @@ +using System.Runtime.CompilerServices; + +namespace Benchmark.NetCore; + +/// +/// A stream that does nothing except yielding the task/thread to take up nonzero time. Modeled after NullStream. +/// +internal sealed class YieldStream : Stream +{ + public static readonly YieldStream Default = new(); + + private YieldStream() { } + + public override bool CanRead => true; + public override bool CanWrite => true; + public override bool CanSeek => true; + public override long Length => 0; + public override long Position { get => 0; set { } } + + public override void CopyTo(Stream destination, int bufferSize) + { + Thread.Yield(); + } + + public override async Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + await Task.Yield(); + } + + protected override void Dispose(bool disposing) + { + // Do nothing - we don't want this stream to be closable. + } + + public override void Flush() + { + Thread.Yield(); + } + + public override async Task FlushAsync(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + await Task.Yield(); + } + + + public override int Read(byte[] buffer, int offset, int count) + { + Thread.Yield(); + return 0; + } + + public override int Read(Span buffer) + { + Thread.Yield(); + return 0; + } + + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + await Task.Yield(); + return 0; + } + + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder<>))] + public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + await Task.Yield(); + return 0; + } + + public override int ReadByte() + { + Thread.Yield(); + return -1; + } + + public override void Write(byte[] buffer, int offset, int count) + { + Thread.Yield(); + } + + public override void Write(ReadOnlySpan buffer) + { + Thread.Yield(); + } + + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + await Task.Yield(); + } + + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + public override async ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + await Task.Yield(); + } + + public override void WriteByte(byte value) + { + Thread.Yield(); + } + + public override long Seek(long offset, SeekOrigin origin) + { + Thread.Yield(); + return 0; + } + + public override void SetLength(long length) + { + Thread.Yield(); + } +} diff --git a/Prometheus/ChildBase.cs b/Prometheus/ChildBase.cs index 5ba5d35a..a34a36b3 100644 --- a/Prometheus/ChildBase.cs +++ b/Prometheus/ChildBase.cs @@ -78,16 +78,16 @@ public void Remove() /// /// Subclass must check _publish and suppress output if it is false. /// - internal Task CollectAndSerializeAsync(IMetricsSerializer serializer, CancellationToken cancel) + internal ValueTask CollectAndSerializeAsync(IMetricsSerializer serializer, CancellationToken cancel) { if (!Volatile.Read(ref _publish)) - return Task.CompletedTask; + return default; return CollectAndSerializeImplAsync(serializer, cancel); } // Same as above, just only called if we really need to serialize this metric (if publish is true). - private protected abstract Task CollectAndSerializeImplAsync(IMetricsSerializer serializer, CancellationToken cancel); + private protected abstract ValueTask CollectAndSerializeImplAsync(IMetricsSerializer serializer, CancellationToken cancel); /// /// Borrows an exemplar temporarily, to be later returned via ReturnBorrowedExemplar. diff --git a/Prometheus/Collector.cs b/Prometheus/Collector.cs index aa5e4de1..a23aa281 100644 --- a/Prometheus/Collector.cs +++ b/Prometheus/Collector.cs @@ -1,5 +1,6 @@ using System.Buffers; using System.ComponentModel; +using System.Runtime.CompilerServices; using System.Text.RegularExpressions; namespace Prometheus; @@ -61,7 +62,7 @@ private static void AssignHelpBytes(Collector instance) => internal abstract int ChildCount { get; } internal abstract int TimeseriesCount { get; } - internal abstract Task CollectAndSerializeAsync(IMetricsSerializer serializer, bool writeFamilyDeclaration, CancellationToken cancel); + internal abstract ValueTask CollectAndSerializeAsync(IMetricsSerializer serializer, bool writeFamilyDeclaration, CancellationToken cancel); // Used by ChildBase.Remove() internal abstract void RemoveLabelled(LabelSequence instanceLabels); @@ -333,7 +334,10 @@ internal Collector(string name, string help, StringSequence instanceLabelNames, /// private protected abstract TChild NewChild(LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish, ExemplarBehavior exemplarBehavior); - internal override async Task CollectAndSerializeAsync(IMetricsSerializer serializer, bool writeFamilyDeclaration, CancellationToken cancel) +#if NET + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] +#endif + internal override async ValueTask CollectAndSerializeAsync(IMetricsSerializer serializer, bool writeFamilyDeclaration, CancellationToken cancel) { EnsureUnlabelledMetricCreatedIfNoLabels(); diff --git a/Prometheus/CollectorFamily.cs b/Prometheus/CollectorFamily.cs index e37c92b9..651b0ec3 100644 --- a/Prometheus/CollectorFamily.cs +++ b/Prometheus/CollectorFamily.cs @@ -1,4 +1,6 @@ using System.Buffers; +using System.Runtime.CompilerServices; +using Microsoft.Extensions.ObjectPool; namespace Prometheus; @@ -12,20 +14,65 @@ internal sealed class CollectorFamily public CollectorFamily(Type collectorType) { CollectorType = collectorType; + _collectAndSerializeFunc = CollectAndSerialize; } - internal async Task CollectAndSerializeAsync(IMetricsSerializer serializer, CancellationToken cancel) +#if NET + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] +#endif + internal async ValueTask CollectAndSerializeAsync(IMetricsSerializer serializer, CancellationToken cancel) + { + var operation = _serializeFamilyOperationPool.Get(); + operation.Serializer = serializer; + + await ForEachCollectorAsync(CollectAndSerialize, operation, cancel); + + _serializeFamilyOperationPool.Return(operation); + } + + /// + /// We use these reusable operation wrappers to avoid capturing variables when serializing, to keep memory usage down while serializing. + /// + private sealed class SerializeFamilyOperation { // The first family member we serialize requires different serialization from the others. - bool isFirst = true; + public bool IsFirst; + public IMetricsSerializer? Serializer; + + public SerializeFamilyOperation() => Reset(); + + public void Reset() + { + IsFirst = true; + Serializer = null; + } + } + + // We have a bunch of families that get serialized often - no reason to churn the GC with a bunch of allocations if we can easily reuse it. + private static readonly ObjectPool _serializeFamilyOperationPool = ObjectPool.Create(new SerializeFamilyOperationPoolingPolicy()); - await ForEachCollectorAsync(async (collector, c) => + private sealed class SerializeFamilyOperationPoolingPolicy : PooledObjectPolicy + { + public override SerializeFamilyOperation Create() => new(); + + public override bool Return(SerializeFamilyOperation obj) { - await collector.CollectAndSerializeAsync(serializer, isFirst, cancel); - isFirst = false; - }, cancel); + obj.Reset(); + return true; + } } +#if NET + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] +#endif + private async ValueTask CollectAndSerialize(Collector collector, SerializeFamilyOperation operation, CancellationToken cancel) + { + await collector.CollectAndSerializeAsync(operation.Serializer!, operation.IsFirst, cancel); + operation.IsFirst = false; + } + + private readonly Func _collectAndSerializeFunc; + internal Collector GetOrAdd(CollectorIdentity identity, in CollectorRegistry.CollectorInitializer initializer) where TCollector : Collector where TConfiguration : MetricConfiguration @@ -77,7 +124,11 @@ internal void ForEachCollector(Action action) } } - internal async Task ForEachCollectorAsync(Func func, CancellationToken cancel) +#if NET + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] +#endif + internal async ValueTask ForEachCollectorAsync(Func func, TArg arg, CancellationToken cancel) + where TArg : class { // This could potentially take nontrivial time, as we are serializing to a stream (potentially, a network stream). // Therefore we operate on a defensive copy in a reused buffer. @@ -102,7 +153,7 @@ internal async Task ForEachCollectorAsync(Func, ICounter @@ -12,7 +14,10 @@ internal Child(Collector parent, LabelSequence instanceLabels, LabelSequence fla private ThreadSafeDouble _value; private ObservedExemplar _observedExemplar = ObservedExemplar.Empty; - private protected override async Task CollectAndSerializeImplAsync(IMetricsSerializer serializer, CancellationToken cancel) +#if NET + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] +#endif + private protected override async ValueTask CollectAndSerializeImplAsync(IMetricsSerializer serializer, CancellationToken cancel) { var exemplar = BorrowExemplar(ref _observedExemplar); diff --git a/Prometheus/Gauge.cs b/Prometheus/Gauge.cs index 0f3f0541..a3e97ce8 100644 --- a/Prometheus/Gauge.cs +++ b/Prometheus/Gauge.cs @@ -11,9 +11,9 @@ internal Child(Collector parent, LabelSequence instanceLabels, LabelSequence fla private ThreadSafeDouble _value; - private protected override async Task CollectAndSerializeImplAsync(IMetricsSerializer serializer, CancellationToken cancel) + private protected override ValueTask CollectAndSerializeImplAsync(IMetricsSerializer serializer, CancellationToken cancel) { - await serializer.WriteMetricPointAsync( + return serializer.WriteMetricPointAsync( Parent.NameBytes, FlattenedLabelsBytes, CanonicalLabel.Empty, cancel, Value, ObservedExemplar.Empty); } diff --git a/Prometheus/Histogram.cs b/Prometheus/Histogram.cs index d0162e6e..c523bade 100644 --- a/Prometheus/Histogram.cs +++ b/Prometheus/Histogram.cs @@ -123,7 +123,10 @@ internal Child(Histogram parent, LabelSequence instanceLabels, LabelSequence fla private static readonly byte[] BucketSuffix = PrometheusConstants.ExportEncoding.GetBytes("bucket"); private readonly ObservedExemplar[] _exemplars; - private protected override async Task CollectAndSerializeImplAsync(IMetricsSerializer serializer, +#if NET + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] +#endif + private protected override async ValueTask CollectAndSerializeImplAsync(IMetricsSerializer serializer, CancellationToken cancel) { // We output sum. diff --git a/Prometheus/IMetricsSerializer.cs b/Prometheus/IMetricsSerializer.cs index eef010b1..a6ae7d3b 100644 --- a/Prometheus/IMetricsSerializer.cs +++ b/Prometheus/IMetricsSerializer.cs @@ -10,26 +10,26 @@ internal interface IMetricsSerializer /// /// Writes the lines that declare the metric family. /// - Task WriteFamilyDeclarationAsync(string name, byte[] nameBytes, byte[] helpBytes, MetricType type, + ValueTask WriteFamilyDeclarationAsync(string name, byte[] nameBytes, byte[] helpBytes, MetricType type, byte[] typeBytes, CancellationToken cancel); /// /// Writes out a single metric point with a floating point value. /// - Task WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, CanonicalLabel canonicalLabel, + ValueTask WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, CanonicalLabel canonicalLabel, CancellationToken cancel, double value, ObservedExemplar exemplar, byte[]? suffix = null); /// /// Writes out a single metric point with an integer value. /// - Task WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, CanonicalLabel canonicalLabel, + ValueTask WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, CanonicalLabel canonicalLabel, CancellationToken cancel, long value, ObservedExemplar exemplar, byte[]? suffix = null); /// /// Writes out terminal lines /// - Task WriteEnd(CancellationToken cancel); - + ValueTask WriteEnd(CancellationToken cancel); + /// /// Flushes any pending buffers. Always call this after all your write calls. /// diff --git a/Prometheus/LabelSequence.cs b/Prometheus/LabelSequence.cs index 475ee048..89a2a315 100644 --- a/Prometheus/LabelSequence.cs +++ b/Prometheus/LabelSequence.cs @@ -135,6 +135,27 @@ public byte[] Serialize() if (!nameEnumerator.MoveNext()) throw new Exception("API contract violation."); if (!valueEnumerator.MoveNext()) throw new Exception("API contract violation."); +#if NET + if (i != 0) + { + TextSerializer.Comma.CopyTo(bytes.AsMemory(index)); + index += TextSerializer.Comma.Length; + } + + index += PrometheusConstants.ExportEncoding.GetBytes(nameEnumerator.Current, 0, nameEnumerator.Current.Length, bytes, index); + + TextSerializer.Equal.CopyTo(bytes.AsMemory(index)); + index += TextSerializer.Equal.Length; + + TextSerializer.Quote.CopyTo(bytes.AsMemory(index)); + index += TextSerializer.Quote.Length; + + var escapedLabelValue = EscapeLabelValue(valueEnumerator.Current); + index += PrometheusConstants.ExportEncoding.GetBytes(escapedLabelValue, 0, escapedLabelValue.Length, bytes, index); + + TextSerializer.Quote.CopyTo(bytes.AsMemory(index)); + index += TextSerializer.Quote.Length; +#else if (i != 0) { Array.Copy(TextSerializer.Comma, 0, bytes, index, TextSerializer.Comma.Length); @@ -154,6 +175,7 @@ public byte[] Serialize() Array.Copy(TextSerializer.Quote, 0, bytes, index, TextSerializer.Quote.Length); index += TextSerializer.Quote.Length; +#endif } if (index != byteCount) throw new Exception("API contract violation - we counted the same bytes twice but got different numbers."); diff --git a/Prometheus/ScrapeFailedException.cs b/Prometheus/ScrapeFailedException.cs index fb803c2d..833adc48 100644 --- a/Prometheus/ScrapeFailedException.cs +++ b/Prometheus/ScrapeFailedException.cs @@ -1,19 +1,15 @@ -namespace Prometheus +namespace Prometheus; + +/// +/// Signals to the metrics server that metrics are currently unavailable. Thrown from "before collect" callbacks. +/// This causes the entire export operation to fail - even if some metrics are available, they will not be exported. +/// +/// The exception message will be delivered as the HTTP response body by the exporter. +/// +[Serializable] +public class ScrapeFailedException : Exception { - /// - /// Signals to the metrics server that metrics are currently unavailable. Thrown from "before collect" callbacks. - /// This causes the entire export operation to fail - even if some metrics are available, they will not be exported. - /// - /// The exception message will be delivered as the HTTP response body by the exporter. - /// - [Serializable] - public class ScrapeFailedException : Exception - { - public ScrapeFailedException() { } - public ScrapeFailedException(string message) : base(message) { } - public ScrapeFailedException(string message, Exception inner) : base(message, inner) { } - protected ScrapeFailedException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } - } + public ScrapeFailedException() { } + public ScrapeFailedException(string message) : base(message) { } + public ScrapeFailedException(string message, Exception inner) : base(message, inner) { } } diff --git a/Prometheus/Summary.cs b/Prometheus/Summary.cs index 9b441bb9..de607986 100644 --- a/Prometheus/Summary.cs +++ b/Prometheus/Summary.cs @@ -1,4 +1,5 @@ using System.Buffers; +using System.Runtime.CompilerServices; using Prometheus.SummaryImpl; namespace Prometheus @@ -110,7 +111,10 @@ internal Child(Summary parent, LabelSequence instanceLabels, LabelSequence flatt private static readonly byte[] CountSuffix = PrometheusConstants.ExportEncoding.GetBytes("count"); private static readonly byte[] QuantileLabelName = PrometheusConstants.ExportEncoding.GetBytes("quantile"); - private protected override async Task CollectAndSerializeImplAsync(IMetricsSerializer serializer, +#if NET + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] +#endif + private protected override async ValueTask CollectAndSerializeImplAsync(IMetricsSerializer serializer, CancellationToken cancel) { // We output sum. @@ -127,7 +131,6 @@ private protected override async Task CollectAndSerializeImplAsync(IMetricsSeria try { - lock (_bufLock) { lock (_lock) diff --git a/Prometheus/TextSerializer.Net.cs b/Prometheus/TextSerializer.Net.cs new file mode 100644 index 00000000..e98fbeac --- /dev/null +++ b/Prometheus/TextSerializer.Net.cs @@ -0,0 +1,352 @@ +#if NET +using System.Globalization; +using System.Runtime.CompilerServices; + +namespace Prometheus; + +/// +/// Does NOT take ownership of the stream - caller remains the boss. +/// +internal sealed class TextSerializer : IMetricsSerializer +{ + internal static readonly ReadOnlyMemory NewLine = new byte[] { (byte)'\n' }; + internal static readonly ReadOnlyMemory Quote = new byte[] { (byte)'"' }; + internal static readonly ReadOnlyMemory Equal = new byte[] { (byte)'=' }; + internal static readonly ReadOnlyMemory Comma = new byte[] { (byte)',' }; + internal static readonly ReadOnlyMemory Underscore = new byte[] { (byte)'_' }; + internal static readonly ReadOnlyMemory LeftBrace = new byte[] { (byte)'{' }; + internal static readonly ReadOnlyMemory RightBraceSpace = new byte[] { (byte)'}', (byte)' ' }; + internal static readonly ReadOnlyMemory Space = new byte[] { (byte)' ' }; + internal static readonly ReadOnlyMemory SpaceHashSpaceLeftBrace = new byte[] { (byte)' ', (byte)'#', (byte)' ', (byte)'{' }; + internal static readonly ReadOnlyMemory PositiveInfinity = PrometheusConstants.ExportEncoding.GetBytes("+Inf"); + internal static readonly ReadOnlyMemory NegativeInfinity = PrometheusConstants.ExportEncoding.GetBytes("-Inf"); + internal static readonly ReadOnlyMemory NotANumber = PrometheusConstants.ExportEncoding.GetBytes("NaN"); + internal static readonly ReadOnlyMemory DotZero = PrometheusConstants.ExportEncoding.GetBytes(".0"); + internal static readonly ReadOnlyMemory FloatPositiveOne = PrometheusConstants.ExportEncoding.GetBytes("1.0"); + internal static readonly ReadOnlyMemory FloatZero = PrometheusConstants.ExportEncoding.GetBytes("0.0"); + internal static readonly ReadOnlyMemory FloatNegativeOne = PrometheusConstants.ExportEncoding.GetBytes("-1.0"); + internal static readonly ReadOnlyMemory IntPositiveOne = PrometheusConstants.ExportEncoding.GetBytes("1"); + internal static readonly ReadOnlyMemory IntZero = PrometheusConstants.ExportEncoding.GetBytes("0"); + internal static readonly ReadOnlyMemory IntNegativeOne = PrometheusConstants.ExportEncoding.GetBytes("-1"); + internal static readonly ReadOnlyMemory EofNewLine = PrometheusConstants.ExportEncoding.GetBytes("# EOF\n"); + internal static readonly ReadOnlyMemory HashHelpSpace = PrometheusConstants.ExportEncoding.GetBytes("# HELP "); + internal static readonly ReadOnlyMemory NewlineHashTypeSpace = PrometheusConstants.ExportEncoding.GetBytes("\n# TYPE "); + internal static readonly ReadOnlyMemory Unknown = PrometheusConstants.ExportEncoding.GetBytes("unknown"); + + internal static readonly byte[] PositiveInfinityBytes = PrometheusConstants.ExportEncoding.GetBytes("+Inf"); + + internal static readonly Dictionary MetricTypeToBytes = new() + { + { MetricType.Gauge, PrometheusConstants.ExportEncoding.GetBytes("gauge") }, + { MetricType.Counter, PrometheusConstants.ExportEncoding.GetBytes("counter") }, + { MetricType.Histogram, PrometheusConstants.ExportEncoding.GetBytes("histogram") }, + { MetricType.Summary, PrometheusConstants.ExportEncoding.GetBytes("summary") }, + }; + + private static readonly char[] DotEChar = { '.', 'e' }; + + public TextSerializer(Stream stream, ExpositionFormat fmt = ExpositionFormat.PrometheusText) + { + _expositionFormat = fmt; + _stream = new Lazy(() => AddStreamBuffering(stream)); + } + + // Enables delay-loading of the stream, because touching stream in HTTP handler triggers some behavior. + public TextSerializer(Func streamFactory, + ExpositionFormat fmt = ExpositionFormat.PrometheusText) + { + _expositionFormat = fmt; + _stream = new Lazy(() => AddStreamBuffering(streamFactory())); + } + + /// + /// Ensures that writes to the stream are buffered, meaning we do not emit individual "write 1 byte" calls to the stream. + /// This has been rumored by some users to be relevant in their scenarios (though never with solid evidence or repro steps). + /// However, we can easily simulate this via the serialization benchmark through named pipes - they are super slow if writing + /// individual characters. It is a reasonable assumption that this limitation is also true elsewhere, at least on some OS/platform. + /// + private Stream AddStreamBuffering(Stream inner) + { + return new BufferedStream(inner, bufferSize: 16 * 1024); + } + + public async Task FlushAsync(CancellationToken cancel) + { + // If we never opened the stream, we don't touch it on flush. + if (!_stream.IsValueCreated) + return; + + await _stream.Value.FlushAsync(cancel); + } + + private readonly Lazy _stream; + + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + public async ValueTask WriteFamilyDeclarationAsync(string name, byte[] nameBytes, byte[] helpBytes, MetricType type, + byte[] typeBytes, CancellationToken cancel) + { + var nameLen = nameBytes.Length; + if (_expositionFormat == ExpositionFormat.OpenMetricsText && type == MetricType.Counter) + { + if (name.EndsWith("_total")) + { + nameLen -= 6; // in OpenMetrics the counter name does not include the _total prefix. + } + } + + await _stream.Value.WriteAsync(HashHelpSpace, cancel); + await _stream.Value.WriteAsync(nameBytes.AsMemory(0, nameLen), cancel); + // The space after the name in "HELP" is mandatory as per ABNF, even if there is no help text. + await _stream.Value.WriteAsync(Space, cancel); + if (helpBytes.Length > 0) + { + await _stream.Value.WriteAsync(helpBytes, cancel); + } + await _stream.Value.WriteAsync(NewlineHashTypeSpace, cancel); + await _stream.Value.WriteAsync(nameBytes.AsMemory(0, nameLen), cancel); + await _stream.Value.WriteAsync(Space, cancel); + await _stream.Value.WriteAsync(typeBytes, cancel); + await _stream.Value.WriteAsync(NewLine, cancel); + } + + public async ValueTask WriteEnd(CancellationToken cancel) + { + if (_expositionFormat == ExpositionFormat.OpenMetricsText) + await _stream.Value.WriteAsync(EofNewLine, cancel); + } + + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + public async ValueTask WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, CanonicalLabel canonicalLabel, + CancellationToken cancel, double value, ObservedExemplar exemplar, byte[]? suffix = null) + { + await WriteIdentifierPartAsync(name, flattenedLabels, cancel, canonicalLabel, suffix); + + await WriteValue(value, cancel); + if (_expositionFormat == ExpositionFormat.OpenMetricsText && exemplar.IsValid) + { + await WriteExemplarAsync(cancel, exemplar); + } + + await _stream.Value.WriteAsync(NewLine, cancel); + } + + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + public async ValueTask WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, CanonicalLabel canonicalLabel, + CancellationToken cancel, long value, ObservedExemplar exemplar, byte[]? suffix = null) + { + await WriteIdentifierPartAsync(name, flattenedLabels, cancel, canonicalLabel, suffix); + + await WriteValue(value, cancel); + if (_expositionFormat == ExpositionFormat.OpenMetricsText && exemplar.IsValid) + { + await WriteExemplarAsync(cancel, exemplar); + } + + await _stream.Value.WriteAsync(NewLine, cancel); + } + + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + private async ValueTask WriteExemplarAsync(CancellationToken cancel, ObservedExemplar exemplar) + { + await _stream.Value.WriteAsync(SpaceHashSpaceLeftBrace, cancel); + for (var i = 0; i < exemplar.Labels!.Length; i++) + { + if (i > 0) + await _stream.Value.WriteAsync(Comma, cancel); + await WriteLabel(exemplar.Labels!.Buffer[i].KeyBytes, exemplar.Labels!.Buffer[i].ValueBytes, cancel); + } + + await _stream.Value.WriteAsync(RightBraceSpace, cancel); + await WriteValue(exemplar.Value, cancel); + await _stream.Value.WriteAsync(Space, cancel); + await WriteValue(exemplar.Timestamp, cancel); + } + + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + private async ValueTask WriteLabel(byte[] label, byte[] value, CancellationToken cancel) + { + await _stream.Value.WriteAsync(label, cancel); + await _stream.Value.WriteAsync(Equal, cancel); + await _stream.Value.WriteAsync(Quote, cancel); + await _stream.Value.WriteAsync(value, cancel); + await _stream.Value.WriteAsync(Quote, cancel); + } + + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + private async ValueTask WriteValue(double value, CancellationToken cancel) + { + if (_expositionFormat == ExpositionFormat.OpenMetricsText) + { + switch (value) + { + case 0: + await _stream.Value.WriteAsync(FloatZero, cancel); + return; + case 1: + await _stream.Value.WriteAsync(FloatPositiveOne, cancel); + return; + case -1: + await _stream.Value.WriteAsync(FloatNegativeOne, cancel); + return; + case double.PositiveInfinity: + await _stream.Value.WriteAsync(PositiveInfinity, cancel); + return; + case double.NegativeInfinity: + await _stream.Value.WriteAsync(NegativeInfinity, cancel); + return; + case double.NaN: + await _stream.Value.WriteAsync(NotANumber, cancel); + return; + } + } + + static bool RequiresDotZero(char[] buffer, int length) + { + return buffer.AsSpan(0..length).IndexOfAny(DotEChar) == -1; /* did not contain .|e */ + } + + // Size limit guided by https://stackoverflow.com/questions/21146544/what-is-the-maximum-length-of-double-tostringd + if (!value.TryFormat(_stringCharsBuffer, out var charsWritten, "g", CultureInfo.InvariantCulture)) + throw new Exception("Failed to encode floating point value as string."); + + var encodedBytes = PrometheusConstants.ExportEncoding.GetBytes(_stringCharsBuffer, 0, charsWritten, _stringBytesBuffer, 0); + await _stream.Value.WriteAsync(_stringBytesBuffer.AsMemory(0, encodedBytes), cancel); + + // In certain places (e.g. "le" label) we need floating point values to actually have the decimal point in them for OpenMetrics. + if (_expositionFormat == ExpositionFormat.OpenMetricsText && RequiresDotZero(_stringCharsBuffer, charsWritten)) + await _stream.Value.WriteAsync(DotZero, cancel); + } + + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + private async ValueTask WriteValue(long value, CancellationToken cancel) + { + if (_expositionFormat == ExpositionFormat.OpenMetricsText) + { + switch (value) + { + case 0: + await _stream.Value.WriteAsync(IntZero, cancel); + return; + case 1: + await _stream.Value.WriteAsync(IntPositiveOne, cancel); + return; + case -1: + await _stream.Value.WriteAsync(IntNegativeOne, cancel); + return; + } + } + + if (!value.TryFormat(_stringCharsBuffer, out var charsWritten, "D", CultureInfo.InvariantCulture)) + throw new Exception("Failed to encode integer value as string."); + + var encodedBytes = PrometheusConstants.ExportEncoding.GetBytes(_stringCharsBuffer, 0, charsWritten, _stringBytesBuffer, 0); + await _stream.Value.WriteAsync(_stringBytesBuffer.AsMemory(0, encodedBytes), cancel); + } + + // Reuse a buffer to do the serialization and UTF-8 encoding. + // Size limit guided by https://stackoverflow.com/questions/21146544/what-is-the-maximum-length-of-double-tostringd + private readonly char[] _stringCharsBuffer = new char[32]; + private readonly byte[] _stringBytesBuffer = new byte[32]; + + private readonly ExpositionFormat _expositionFormat; + + /// + /// Creates a metric identifier, with an optional name postfix and an optional extra label to append to the end. + /// familyname_postfix{labelkey1="labelvalue1",labelkey2="labelvalue2"} + /// Note: Terminates with a SPACE + /// + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + private async ValueTask WriteIdentifierPartAsync(byte[] name, byte[] flattenedLabels, CancellationToken cancel, + CanonicalLabel canonicalLabel, byte[]? suffix = null) + { + await _stream.Value.WriteAsync(name, cancel); + if (suffix != null && suffix.Length > 0) + { + await _stream.Value.WriteAsync(Underscore, cancel); + await _stream.Value.WriteAsync(suffix, cancel); + } + + if (flattenedLabels.Length > 0 || canonicalLabel.IsNotEmpty) + { + await _stream.Value.WriteAsync(LeftBrace, cancel); + if (flattenedLabels.Length > 0) + { + await _stream.Value.WriteAsync(flattenedLabels, cancel); + } + + // Extra labels go to the end (i.e. they are deepest to inherit from). + if (canonicalLabel.IsNotEmpty) + { + if (flattenedLabels.Length > 0) + { + await _stream.Value.WriteAsync(Comma, cancel); + } + + await _stream.Value.WriteAsync(canonicalLabel.Name.AsMemory(0, canonicalLabel.Name.Length), cancel); + await _stream.Value.WriteAsync(Equal, cancel); + await _stream.Value.WriteAsync(Quote, cancel); + if (_expositionFormat == ExpositionFormat.OpenMetricsText) + await _stream.Value.WriteAsync(canonicalLabel.OpenMetrics.AsMemory(0, canonicalLabel.OpenMetrics.Length), cancel); + else + await _stream.Value.WriteAsync(canonicalLabel.Prometheus.AsMemory(0, canonicalLabel.Prometheus.Length), cancel); + await _stream.Value.WriteAsync(Quote, cancel); + } + + await _stream.Value.WriteAsync(RightBraceSpace, cancel); + } + else + { + await _stream.Value.WriteAsync(Space, cancel); + } + } + + /// + /// Encode the special variable in regular Prometheus form and also return a OpenMetrics variant, these can be + /// the same. + /// see: https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#considerations-canonical-numbers + /// + internal static CanonicalLabel EncodeValueAsCanonicalLabel(byte[] name, double value) + { + if (double.IsPositiveInfinity(value)) + return new CanonicalLabel(name, PositiveInfinityBytes, PositiveInfinityBytes); + + // Size limit guided by https://stackoverflow.com/questions/21146544/what-is-the-maximum-length-of-double-tostringd + Span buffer = stackalloc char[32]; + + if (!value.TryFormat(buffer, out var charsWritten, "g", CultureInfo.InvariantCulture)) + throw new Exception("Failed to encode floating point value as string."); + + var prometheusChars = buffer[0..charsWritten]; + + var prometheusByteCount = PrometheusConstants.ExportEncoding.GetByteCount(prometheusChars); + var prometheusBytes = new byte[prometheusByteCount]; + + if (PrometheusConstants.ExportEncoding.GetBytes(prometheusChars, prometheusBytes) != prometheusByteCount) + throw new Exception("Internal error: counting the same bytes twice got us a different value."); + + var openMetricsByteCount = prometheusByteCount; + byte[] openMetricsBytes; + + // Identify whether the written characters are expressed as floating-point, by checking for presence of the 'e' or '.' characters. + if (prometheusChars.IndexOfAny(DotEChar) == -1) + { + // Prometheus defaults to integer-formatting without a decimal point, if possible. + // OpenMetrics requires labels containing numeric values to be expressed in floating point format. + // If all we find is an integer, we add a ".0" to the end to make it a floating point value. + openMetricsByteCount += 2; + + openMetricsBytes = new byte[openMetricsByteCount]; + Array.Copy(prometheusBytes, openMetricsBytes, prometheusByteCount); + + DotZero.CopyTo(openMetricsBytes.AsMemory(prometheusByteCount)); + } + else + { + // It is already a floating-point value in Prometheus representation - reuse same bytes for OpenMetrics. + openMetricsBytes = prometheusBytes; + } + + return new CanonicalLabel(name, prometheusBytes, openMetricsBytes); + } +} +#endif \ No newline at end of file diff --git a/Prometheus/TextSerializer.cs b/Prometheus/TextSerializer.NetStandardFx.cs similarity index 76% rename from Prometheus/TextSerializer.cs rename to Prometheus/TextSerializer.NetStandardFx.cs index cce166de..ada12d60 100644 --- a/Prometheus/TextSerializer.cs +++ b/Prometheus/TextSerializer.NetStandardFx.cs @@ -1,4 +1,5 @@ -using System.Globalization; +#if !NET +using System.Globalization; namespace Prometheus; @@ -31,6 +32,8 @@ internal sealed class TextSerializer : IMetricsSerializer internal static readonly byte[] NewlineHashTypeSpace = PrometheusConstants.ExportEncoding.GetBytes("\n# TYPE "); internal static readonly byte[] Unknown = PrometheusConstants.ExportEncoding.GetBytes("unknown"); + internal static readonly byte[] PositiveInfinityBytes = PrometheusConstants.ExportEncoding.GetBytes("+Inf"); + internal static readonly Dictionary MetricTypeToBytes = new() { { MetricType.Gauge, PrometheusConstants.ExportEncoding.GetBytes("gauge") }, @@ -44,7 +47,7 @@ internal sealed class TextSerializer : IMetricsSerializer public TextSerializer(Stream stream, ExpositionFormat fmt = ExpositionFormat.PrometheusText) { _expositionFormat = fmt; - _stream = new Lazy(() => stream); + _stream = new Lazy(() => AddStreamBuffering(stream)); } // Enables delay-loading of the stream, because touching stream in HTTP handler triggers some behavior. @@ -52,7 +55,18 @@ public TextSerializer(Func streamFactory, ExpositionFormat fmt = ExpositionFormat.PrometheusText) { _expositionFormat = fmt; - _stream = new Lazy(streamFactory); + _stream = new Lazy(() => AddStreamBuffering(streamFactory())); + } + + /// + /// Ensures that writes to the stream are buffered, meaning we do not emit individual "write 1 byte" calls to the stream. + /// This has been rumored by some users to be relevant in their scenarios (though never with solid evidence or repro steps). + /// However, we can easily simulate this via the serialization benchmark through named pipes - they are super slow if writing + /// individual characters. It is a reasonable assumption that this limitation is also true elsewhere, at least on some OS/platform. + /// + private Stream AddStreamBuffering(Stream inner) + { + return new BufferedStream(inner); } public async Task FlushAsync(CancellationToken cancel) @@ -66,7 +80,7 @@ public async Task FlushAsync(CancellationToken cancel) private readonly Lazy _stream; - public async Task WriteFamilyDeclarationAsync(string name, byte[] nameBytes, byte[] helpBytes, MetricType type, + public async ValueTask WriteFamilyDeclarationAsync(string name, byte[] nameBytes, byte[] helpBytes, MetricType type, byte[] typeBytes, CancellationToken cancel) { var nameLen = nameBytes.Length; @@ -76,10 +90,6 @@ public async Task WriteFamilyDeclarationAsync(string name, byte[] nameBytes, byt { nameLen -= 6; // in OpenMetrics the counter name does not include the _total prefix. } - else - { - typeBytes = Unknown; // if the total prefix is missing the _total prefix it is out of spec - } } await _stream.Value.WriteAsync(HashHelpSpace, 0, HashHelpSpace.Length, cancel); @@ -97,16 +107,16 @@ public async Task WriteFamilyDeclarationAsync(string name, byte[] nameBytes, byt await _stream.Value.WriteAsync(NewLine, 0, NewLine.Length, cancel); } - public async Task WriteEnd(CancellationToken cancel) + public async ValueTask WriteEnd(CancellationToken cancel) { if (_expositionFormat == ExpositionFormat.OpenMetricsText) await _stream.Value.WriteAsync(EofNewLine, 0, EofNewLine.Length, cancel); } - public async Task WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, CanonicalLabel canonicalLabel, + public async ValueTask WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, CanonicalLabel canonicalLabel, CancellationToken cancel, double value, ObservedExemplar exemplar, byte[]? suffix = null) { - await WriteIdentifierPartAsync(name, flattenedLabels, cancel, canonicalLabel, exemplar, suffix); + await WriteIdentifierPartAsync(name, flattenedLabels, cancel, canonicalLabel, suffix); await WriteValue(value, cancel); if (_expositionFormat == ExpositionFormat.OpenMetricsText && exemplar.IsValid) @@ -117,10 +127,10 @@ public async Task WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, Can await _stream.Value.WriteAsync(NewLine, 0, NewLine.Length, cancel); } - public async Task WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, CanonicalLabel canonicalLabel, + public async ValueTask WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, CanonicalLabel canonicalLabel, CancellationToken cancel, long value, ObservedExemplar exemplar, byte[]? suffix = null) { - await WriteIdentifierPartAsync(name, flattenedLabels, cancel, canonicalLabel, exemplar, suffix); + await WriteIdentifierPartAsync(name, flattenedLabels, cancel, canonicalLabel, suffix); await WriteValue(value, cancel); if (_expositionFormat == ExpositionFormat.OpenMetricsText && exemplar.IsValid) @@ -183,23 +193,6 @@ private async Task WriteValue(double value, CancellationToken cancel) } } -#if NET - static bool RequiresDotZero(char[] buffer, int length) - { - return buffer.AsSpan(0..length).IndexOfAny(DotEChar) == -1; /* did not contain .|e */ - } - - // Size limit guided by https://stackoverflow.com/questions/21146544/what-is-the-maximum-length-of-double-tostringd - if (!value.TryFormat(_stringCharsBuffer, out var charsWritten, "g", CultureInfo.InvariantCulture)) - throw new Exception("Failed to encode floating point value as string."); - - var encodedBytes = PrometheusConstants.ExportEncoding.GetBytes(_stringCharsBuffer, 0, charsWritten, _stringBytesBuffer, 0); - await _stream.Value.WriteAsync(_stringBytesBuffer, 0, encodedBytes, cancel); - - // In certain places (e.g. "le" label) we need floating point values to actually have the decimal point in them for OpenMetrics. - if (_expositionFormat == ExpositionFormat.OpenMetricsText && RequiresDotZero(_stringCharsBuffer, charsWritten)) - await _stream.Value.WriteAsync(DotZero, 0, DotZero.Length, cancel); -#else var valueAsString = value.ToString("g", CultureInfo.InvariantCulture); var numBytes = PrometheusConstants.ExportEncoding.GetBytes(valueAsString, 0, valueAsString.Length, _stringBytesBuffer, 0); @@ -208,7 +201,6 @@ static bool RequiresDotZero(char[] buffer, int length) // In certain places (e.g. "le" label) we need floating point values to actually have the decimal point in them for OpenMetrics. if (_expositionFormat == ExpositionFormat.OpenMetricsText && valueAsString.IndexOfAny(DotEChar) == -1 /* did not contain .|e */) await _stream.Value.WriteAsync(DotZero, 0, DotZero.Length, cancel); -#endif } private async Task WriteValue(long value, CancellationToken cancel) @@ -229,17 +221,9 @@ private async Task WriteValue(long value, CancellationToken cancel) } } -#if NET - if (!value.TryFormat(_stringCharsBuffer, out var charsWritten, "D", CultureInfo.InvariantCulture)) - throw new Exception("Failed to encode integer value as string."); - - var encodedBytes = PrometheusConstants.ExportEncoding.GetBytes(_stringCharsBuffer, 0, charsWritten, _stringBytesBuffer, 0); - await _stream.Value.WriteAsync(_stringBytesBuffer, 0, encodedBytes, cancel); -#else var valueAsString = value.ToString("D", CultureInfo.InvariantCulture); var numBytes = PrometheusConstants.ExportEncoding.GetBytes(valueAsString, 0, valueAsString.Length, _stringBytesBuffer, 0); await _stream.Value.WriteAsync(_stringBytesBuffer, 0, numBytes, cancel); -#endif } // Reuse a buffer to do the serialization and UTF-8 encoding. @@ -255,7 +239,7 @@ private async Task WriteValue(long value, CancellationToken cancel) /// Note: Terminates with a SPACE /// private async Task WriteIdentifierPartAsync(byte[] name, byte[] flattenedLabels, CancellationToken cancel, - CanonicalLabel canonicalLabel, ObservedExemplar observedExemplar, byte[]? suffix = null) + CanonicalLabel canonicalLabel, byte[]? suffix = null) { await _stream.Value.WriteAsync(name, 0, name.Length, cancel); if (suffix != null && suffix.Length > 0) @@ -308,45 +292,8 @@ await _stream.Value.WriteAsync( internal static CanonicalLabel EncodeValueAsCanonicalLabel(byte[] name, double value) { if (double.IsPositiveInfinity(value)) - return new CanonicalLabel(name, PositiveInfinity, PositiveInfinity); - -#if NET - // Size limit guided by https://stackoverflow.com/questions/21146544/what-is-the-maximum-length-of-double-tostringd - Span buffer = stackalloc char[32]; - - if (!value.TryFormat(buffer, out var charsWritten, "g", CultureInfo.InvariantCulture)) - throw new Exception("Failed to encode floating point value as string."); - - var prometheusChars = buffer[0..charsWritten]; - - var prometheusByteCount = PrometheusConstants.ExportEncoding.GetByteCount(prometheusChars); - var prometheusBytes = new byte[prometheusByteCount]; - - if (PrometheusConstants.ExportEncoding.GetBytes(prometheusChars, prometheusBytes) != prometheusByteCount) - throw new Exception("Internal error: counting the same bytes twice got us a different value."); - - var openMetricsByteCount = prometheusByteCount; - byte[] openMetricsBytes; - - // Identify whether the written characters are expressed as floating-point, by checking for presence of the 'e' or '.' characters. - if (prometheusChars.IndexOfAny(DotEChar) == -1) - { - // Prometheus defaults to integer-formatting without a decimal point, if possible. - // OpenMetrics requires labels containing numeric values to be expressed in floating point format. - // If all we find is an integer, we add a ".0" to the end to make it a floating point value. - openMetricsByteCount += 2; - - openMetricsBytes = new byte[openMetricsByteCount]; - Array.Copy(prometheusBytes, openMetricsBytes, prometheusByteCount); - Array.Copy(DotZero, 0, openMetricsBytes, prometheusByteCount, DotZero.Length); - } - else - { - // It is already a floating-point value in Prometheus representation - reuse same bytes for OpenMetrics. - openMetricsBytes = prometheusBytes; - } + return new CanonicalLabel(name, PositiveInfinityBytes, PositiveInfinityBytes); -#else var valueAsString = value.ToString("g", CultureInfo.InvariantCulture); var prometheusBytes = PrometheusConstants.ExportEncoding.GetBytes(valueAsString); @@ -359,8 +306,8 @@ internal static CanonicalLabel EncodeValueAsCanonicalLabel(byte[] name, double v // If all we find is an integer, we add a ".0" to the end to make it a floating point value. openMetricsBytes = PrometheusConstants.ExportEncoding.GetBytes(valueAsString + ".0"); } -#endif return new CanonicalLabel(name, prometheusBytes, openMetricsBytes); } -} \ No newline at end of file +} +#endif \ No newline at end of file From e3f6a4fe5ff4c0a3fc8c553110605105fb488ae8 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Sun, 26 Nov 2023 00:36:40 +0200 Subject: [PATCH 155/230] Restore "if the total prefix is missing the _total prefix it is out of spec" functionality that somehow went missing --- Prometheus/TextSerializer.Net.cs | 6 +++++- Prometheus/TextSerializer.NetStandardFx.cs | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Prometheus/TextSerializer.Net.cs b/Prometheus/TextSerializer.Net.cs index e98fbeac..3dec70f1 100644 --- a/Prometheus/TextSerializer.Net.cs +++ b/Prometheus/TextSerializer.Net.cs @@ -31,7 +31,7 @@ internal sealed class TextSerializer : IMetricsSerializer internal static readonly ReadOnlyMemory EofNewLine = PrometheusConstants.ExportEncoding.GetBytes("# EOF\n"); internal static readonly ReadOnlyMemory HashHelpSpace = PrometheusConstants.ExportEncoding.GetBytes("# HELP "); internal static readonly ReadOnlyMemory NewlineHashTypeSpace = PrometheusConstants.ExportEncoding.GetBytes("\n# TYPE "); - internal static readonly ReadOnlyMemory Unknown = PrometheusConstants.ExportEncoding.GetBytes("unknown"); + internal static readonly byte[] Unknown = PrometheusConstants.ExportEncoding.GetBytes("unknown"); internal static readonly byte[] PositiveInfinityBytes = PrometheusConstants.ExportEncoding.GetBytes("+Inf"); @@ -92,6 +92,10 @@ public async ValueTask WriteFamilyDeclarationAsync(string name, byte[] nameBytes { nameLen -= 6; // in OpenMetrics the counter name does not include the _total prefix. } + else + { + typeBytes = Unknown; // if the total prefix is missing the _total prefix it is out of spec + } } await _stream.Value.WriteAsync(HashHelpSpace, cancel); diff --git a/Prometheus/TextSerializer.NetStandardFx.cs b/Prometheus/TextSerializer.NetStandardFx.cs index ada12d60..6015e6c2 100644 --- a/Prometheus/TextSerializer.NetStandardFx.cs +++ b/Prometheus/TextSerializer.NetStandardFx.cs @@ -90,6 +90,10 @@ public async ValueTask WriteFamilyDeclarationAsync(string name, byte[] nameBytes { nameLen -= 6; // in OpenMetrics the counter name does not include the _total prefix. } + else + { + typeBytes = Unknown; // if the total prefix is missing the _total prefix it is out of spec + } } await _stream.Value.WriteAsync(HashHelpSpace, 0, HashHelpSpace.Length, cancel); From c3f045ad436d40fc2b40fa66b602cc13399ce2f7 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Sun, 26 Nov 2023 00:43:40 +0200 Subject: [PATCH 156/230] Fix some bad math in AVX buffer alignment logic --- Prometheus/Histogram.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Prometheus/Histogram.cs b/Prometheus/Histogram.cs index c523bade..655a9519 100644 --- a/Prometheus/Histogram.cs +++ b/Prometheus/Histogram.cs @@ -81,7 +81,7 @@ internal Histogram(string name, string help, StringSequence instanceLabelNames, if (bytesUntilNextAlignedPosition % sizeof(double) != 0) throw new Exception("Unreachable code reached - all double[] allocations are expected to be at least 8-aligned."); - _bucketsAlignmentBufferOffset = (int)(pointerTooFarByBytes / sizeof(double)); + _bucketsAlignmentBufferOffset = (int)(bytesUntilNextAlignedPosition / sizeof(double)); } Array.Copy(_buckets, 0, _bucketsAlignmentBuffer, _bucketsAlignmentBufferOffset, _buckets.Length); From 30b0058d4cc6430dc21d78bfd24dd5809bbb8a4f Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Sun, 26 Nov 2023 10:26:26 +0200 Subject: [PATCH 157/230] Does not matter --- Prometheus/TextSerializer.Net.cs | 3 ++- Prometheus/TextSerializer.NetStandardFx.cs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Prometheus/TextSerializer.Net.cs b/Prometheus/TextSerializer.Net.cs index 3dec70f1..0bf6c8ad 100644 --- a/Prometheus/TextSerializer.Net.cs +++ b/Prometheus/TextSerializer.Net.cs @@ -31,6 +31,7 @@ internal sealed class TextSerializer : IMetricsSerializer internal static readonly ReadOnlyMemory EofNewLine = PrometheusConstants.ExportEncoding.GetBytes("# EOF\n"); internal static readonly ReadOnlyMemory HashHelpSpace = PrometheusConstants.ExportEncoding.GetBytes("# HELP "); internal static readonly ReadOnlyMemory NewlineHashTypeSpace = PrometheusConstants.ExportEncoding.GetBytes("\n# TYPE "); + internal static readonly byte[] Unknown = PrometheusConstants.ExportEncoding.GetBytes("unknown"); internal static readonly byte[] PositiveInfinityBytes = PrometheusConstants.ExportEncoding.GetBytes("+Inf"); @@ -65,7 +66,7 @@ public TextSerializer(Func streamFactory, /// However, we can easily simulate this via the serialization benchmark through named pipes - they are super slow if writing /// individual characters. It is a reasonable assumption that this limitation is also true elsewhere, at least on some OS/platform. ///
- private Stream AddStreamBuffering(Stream inner) + private static Stream AddStreamBuffering(Stream inner) { return new BufferedStream(inner, bufferSize: 16 * 1024); } diff --git a/Prometheus/TextSerializer.NetStandardFx.cs b/Prometheus/TextSerializer.NetStandardFx.cs index 6015e6c2..fa262ca0 100644 --- a/Prometheus/TextSerializer.NetStandardFx.cs +++ b/Prometheus/TextSerializer.NetStandardFx.cs @@ -30,6 +30,7 @@ internal sealed class TextSerializer : IMetricsSerializer internal static readonly byte[] EofNewLine = PrometheusConstants.ExportEncoding.GetBytes("# EOF\n"); internal static readonly byte[] HashHelpSpace = PrometheusConstants.ExportEncoding.GetBytes("# HELP "); internal static readonly byte[] NewlineHashTypeSpace = PrometheusConstants.ExportEncoding.GetBytes("\n# TYPE "); + internal static readonly byte[] Unknown = PrometheusConstants.ExportEncoding.GetBytes("unknown"); internal static readonly byte[] PositiveInfinityBytes = PrometheusConstants.ExportEncoding.GetBytes("+Inf"); @@ -64,7 +65,7 @@ public TextSerializer(Func streamFactory, /// However, we can easily simulate this via the serialization benchmark through named pipes - they are super slow if writing /// individual characters. It is a reasonable assumption that this limitation is also true elsewhere, at least on some OS/platform. /// - private Stream AddStreamBuffering(Stream inner) + private static Stream AddStreamBuffering(Stream inner) { return new BufferedStream(inner); } From cfbc055d1eda1e1be4cbedc61dfb6bd36adb153b Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Sun, 26 Nov 2023 10:26:45 +0200 Subject: [PATCH 158/230] Use object pooling to reuse the HashSets used to detect label name collisions on metric creation --- Prometheus/Collector.cs | 58 ++++++++++++++++++++++++++++++----------- 1 file changed, 43 insertions(+), 15 deletions(-) diff --git a/Prometheus/Collector.cs b/Prometheus/Collector.cs index a23aa281..d57d5235 100644 --- a/Prometheus/Collector.cs +++ b/Prometheus/Collector.cs @@ -2,6 +2,7 @@ using System.ComponentModel; using System.Runtime.CompilerServices; using System.Text.RegularExpressions; +using Microsoft.Extensions.ObjectPool; namespace Prometheus; @@ -95,28 +96,55 @@ internal Collector(string name, string help, StringSequence instanceLabelNames, FlattenedLabelNames = instanceLabelNames.Concat(staticLabels.Names); - // Used to check uniqueness. + // Used to check uniqueness of label names, to catch any label layering mistakes early. + var uniqueLabelNames = LabelValidationHashSetPool.Get(); + + try + { + var labelNameEnumerator = FlattenedLabelNames.GetEnumerator(); + while (labelNameEnumerator.MoveNext()) + { + var labelName = labelNameEnumerator.Current; + + if (labelName == null) + throw new ArgumentNullException("Label name was null."); + + ValidateLabelName(labelName); + uniqueLabelNames.Add(labelName); + } + + // Here we check for label name collision, ensuring that the same label name is not defined twice on any label inheritance level. + if (uniqueLabelNames.Count != FlattenedLabelNames.Length) + throw new InvalidOperationException("The set of label names includes duplicates: " + string.Join(", ", FlattenedLabelNames.ToArray())); + } + finally + { + LabelValidationHashSetPool.Return(uniqueLabelNames); + } + } + + private static readonly ObjectPool> LabelValidationHashSetPool = ObjectPool.Create(new LabelValidationHashSetPoolPolicy()); + + private sealed class LabelValidationHashSetPoolPolicy : PooledObjectPolicy> + { + // If something should explode the size, we do not return it to the pool. + // This should be more than generous even for the most verbosely labeled scenarios. + private const int PooledHashSetMaxSize = 50; + #if NET - var uniqueLabelNames = new HashSet(FlattenedLabelNames.Length, StringComparer.Ordinal); + public override HashSet Create() => new(PooledHashSetMaxSize, StringComparer.Ordinal); #else - var uniqueLabelNames = new HashSet(StringComparer.Ordinal); + public override HashSet Create() => new(StringComparer.Ordinal); #endif - var labelNameEnumerator = FlattenedLabelNames.GetEnumerator(); - while (labelNameEnumerator.MoveNext()) + public override bool Return(HashSet obj) { - var labelName = labelNameEnumerator.Current; + if (obj.Count > PooledHashSetMaxSize) + return false; - if (labelName == null) - throw new ArgumentNullException("Label name was null."); - - ValidateLabelName(labelName); - uniqueLabelNames.Add(labelName); + obj.Clear(); + return true; } - - // Here we check for label name collision, ensuring that the same label name is not defined twice on any label inheritance level. - if (uniqueLabelNames.Count != FlattenedLabelNames.Length) - throw new InvalidOperationException("The set of label names includes duplicates: " + string.Join(", ", FlattenedLabelNames.ToArray())); } internal static void ValidateLabelName(string labelName) From 61aca7cce00a4c4ed188c2b613e6e42d639d9faf Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Sun, 26 Nov 2023 11:32:26 +0200 Subject: [PATCH 159/230] MetricExpirationBenchmarks tuning --- Benchmark.NetCore/MetricExpirationBenchmarks.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Benchmark.NetCore/MetricExpirationBenchmarks.cs b/Benchmark.NetCore/MetricExpirationBenchmarks.cs index a8d5cae4..717084cb 100644 --- a/Benchmark.NetCore/MetricExpirationBenchmarks.cs +++ b/Benchmark.NetCore/MetricExpirationBenchmarks.cs @@ -12,7 +12,7 @@ public class MetricExpirationBenchmarks /// /// Just to ensure that a benchmark iteration has enough to do for stable and meaningful results. /// - private const int _metricCount = 100; + private const int _metricCount = 1_000; /// /// Some benchmarks try to register metrics that already exist. From e8d413e23103ae9f659136f234eb83809063f135 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Sun, 26 Nov 2023 11:32:47 +0200 Subject: [PATCH 160/230] ReaderWriterLockSlim for MLMF --- Prometheus/ManagedLifetimeMetricFactory.cs | 219 ++++++++++---------- Prometheus/ManagedLifetimeMetricIdentity.cs | 7 +- Prometheus/StringSequence.cs | 3 + 3 files changed, 123 insertions(+), 106 deletions(-) diff --git a/Prometheus/ManagedLifetimeMetricFactory.cs b/Prometheus/ManagedLifetimeMetricFactory.cs index 9616a9d1..2daf53d4 100644 --- a/Prometheus/ManagedLifetimeMetricFactory.cs +++ b/Prometheus/ManagedLifetimeMetricFactory.cs @@ -1,6 +1,4 @@ -using System.Collections.Concurrent; - -namespace Prometheus; +namespace Prometheus; internal sealed class ManagedLifetimeMetricFactory : IManagedLifetimeMetricFactory { @@ -27,148 +25,159 @@ public IManagedLifetimeMetricHandle CreateCounter(string name, string { var identity = new ManagedLifetimeMetricIdentity(name, StringSequence.From(instanceLabelNames ?? Array.Empty())); - // Let's be optimistic and assume that in the typical case, the metric will already exist. - if (_counters.TryGetValue(identity, out var existing)) - return existing; + _countersLock.EnterReadLock(); - var initializer = new CounterInitializer(_inner, _expiresAfter, help, configuration); - return _counters.GetOrAdd(identity, initializer.CreateInstance); - } + try + { + // Let's be optimistic and assume that in the typical case, the metric will already exist. + if (_counters.TryGetValue(identity, out var existing)) + return existing; + } + finally + { + _countersLock.ExitReadLock(); + } - public IManagedLifetimeMetricHandle CreateGauge(string name, string help, string[]? instanceLabelNames, GaugeConfiguration? configuration) - { - var identity = new ManagedLifetimeMetricIdentity(name, StringSequence.From(instanceLabelNames ?? Array.Empty())); + _countersLock.EnterWriteLock(); - // Let's be optimistic and assume that in the typical case, the metric will already exist. - if (_gauges.TryGetValue(identity, out var existing)) - return existing; + try + { + if (_counters.TryGetValue(identity, out var existing)) + return existing; - var initializer = new GaugeInitializer(_inner, _expiresAfter, help, configuration); - return _gauges.GetOrAdd(identity, initializer.CreateInstance); + var metric = _inner.CreateCounter(identity.MetricFamilyName, help, identity.InstanceLabelNames, configuration); + var instance = new ManagedLifetimeCounter(metric, _expiresAfter); + _counters.Add(identity, instance); + return instance; + } + finally + { + _countersLock.ExitWriteLock(); + } } - public IManagedLifetimeMetricHandle CreateHistogram(string name, string help, string[]? instanceLabelNames, HistogramConfiguration? configuration) + public IManagedLifetimeMetricHandle CreateGauge(string name, string help, string[]? instanceLabelNames, GaugeConfiguration? configuration) { var identity = new ManagedLifetimeMetricIdentity(name, StringSequence.From(instanceLabelNames ?? Array.Empty())); - // Let's be optimistic and assume that in the typical case, the metric will already exist. - if (_histograms.TryGetValue(identity, out var existing)) - return existing; + _gaugesLock.EnterReadLock(); - var initializer = new HistogramInitializer(_inner, _expiresAfter, help, configuration); - return _histograms.GetOrAdd(identity, initializer.CreateInstance); - } + try + { + // Let's be optimistic and assume that in the typical case, the metric will already exist. + if (_gauges.TryGetValue(identity, out var existing)) + return existing; + } + finally + { + _gaugesLock.ExitReadLock(); + } - public IManagedLifetimeMetricHandle CreateSummary(string name, string help, string[]? instanceLabelNames, SummaryConfiguration? configuration) - { - var identity = new ManagedLifetimeMetricIdentity(name, StringSequence.From(instanceLabelNames ?? Array.Empty())); + _gaugesLock.EnterWriteLock(); - // Let's be optimistic and assume that in the typical case, the metric will already exist. - if (_summaries.TryGetValue(identity, out var existing)) - return existing; + try + { + if (_gauges.TryGetValue(identity, out var existing)) + return existing; - var initializer = new SummaryInitializer(_inner, _expiresAfter, help, configuration); - return _summaries.GetOrAdd(identity, initializer.CreateInstance); + var metric = _inner.CreateGauge(identity.MetricFamilyName, help, identity.InstanceLabelNames, configuration); + var instance = new ManagedLifetimeGauge(metric, _expiresAfter); + _gauges.Add(identity, instance); + return instance; + } + finally + { + _gaugesLock.ExitWriteLock(); + } } - /// - /// Gets all the existing label names predefined either in the factory or in the registry. - /// - internal StringSequence GetAllStaticLabelNames() => _inner.GetAllStaticLabelNames(); - - // We need to reuse existing instances of lifetime-managed metrics because the user might not want to cache it. - // This somewhat duplicates the metric identity tracking logic in CollectorRegistry but this is intentional, as we really do need to do this work on two layers. - // We never remove collectors from here as long as the factory is alive. The expectation is that there is not an unbounded set of label names, so this set is non-gigantic. - private readonly ConcurrentDictionary _counters = new(); - private readonly ConcurrentDictionary _gauges = new(); - private readonly ConcurrentDictionary _histograms = new(); - private readonly ConcurrentDictionary _summaries = new(); - - private readonly struct CounterInitializer + public IManagedLifetimeMetricHandle CreateHistogram(string name, string help, string[]? instanceLabelNames, HistogramConfiguration? configuration) { - public readonly MetricFactory Inner; - public readonly TimeSpan ExpiresAfter; - public readonly string Help; - public readonly CounterConfiguration? Configuration; + var identity = new ManagedLifetimeMetricIdentity(name, StringSequence.From(instanceLabelNames ?? Array.Empty())); - public CounterInitializer(MetricFactory inner, TimeSpan expiresAfter, string help, CounterConfiguration? configuration) + _histogramsLock.EnterReadLock(); + + try { - Inner = inner; - ExpiresAfter = expiresAfter; - Help = help; - Configuration = configuration; + // Let's be optimistic and assume that in the typical case, the metric will already exist. + if (_histograms.TryGetValue(identity, out var existing)) + return existing; } - - public ManagedLifetimeCounter CreateInstance(ManagedLifetimeMetricIdentity identity) + finally { - var metric = Inner.CreateCounter(identity.MetricFamilyName, Help, identity.InstanceLabelNames, Configuration); - return new ManagedLifetimeCounter(metric, ExpiresAfter); + _histogramsLock.ExitReadLock(); } - } - private readonly struct GaugeInitializer - { - public readonly MetricFactory Inner; - public readonly TimeSpan ExpiresAfter; - public readonly string Help; - public readonly GaugeConfiguration? Configuration; + _histogramsLock.EnterWriteLock(); - public GaugeInitializer(MetricFactory inner, TimeSpan expiresAfter, string help, GaugeConfiguration? configuration) + try { - Inner = inner; - ExpiresAfter = expiresAfter; - Help = help; - Configuration = configuration; - } + if (_histograms.TryGetValue(identity, out var existing)) + return existing; - public ManagedLifetimeGauge CreateInstance(ManagedLifetimeMetricIdentity identity) + var metric = _inner.CreateHistogram(identity.MetricFamilyName, help, identity.InstanceLabelNames, configuration); + var instance = new ManagedLifetimeHistogram(metric, _expiresAfter); + _histograms.Add(identity, instance); + return instance; + } + finally { - var metric = Inner.CreateGauge(identity.MetricFamilyName, Help, identity.InstanceLabelNames, Configuration); - return new ManagedLifetimeGauge(metric, ExpiresAfter); + _histogramsLock.ExitWriteLock(); } } - private readonly struct HistogramInitializer + public IManagedLifetimeMetricHandle CreateSummary(string name, string help, string[]? instanceLabelNames, SummaryConfiguration? configuration) { - public readonly MetricFactory Inner; - public readonly TimeSpan ExpiresAfter; - public readonly string Help; - public readonly HistogramConfiguration? Configuration; + var identity = new ManagedLifetimeMetricIdentity(name, StringSequence.From(instanceLabelNames ?? Array.Empty())); - public HistogramInitializer(MetricFactory inner, TimeSpan expiresAfter, string help, HistogramConfiguration? configuration) + _summariesLock.EnterReadLock(); + + try { - Inner = inner; - ExpiresAfter = expiresAfter; - Help = help; - Configuration = configuration; + // Let's be optimistic and assume that in the typical case, the metric will already exist. + if (_summaries.TryGetValue(identity, out var existing)) + return existing; } - - public ManagedLifetimeHistogram CreateInstance(ManagedLifetimeMetricIdentity identity) + finally { - var metric = Inner.CreateHistogram(identity.MetricFamilyName, Help, identity.InstanceLabelNames, Configuration); - return new ManagedLifetimeHistogram(metric, ExpiresAfter); + _summariesLock.ExitReadLock(); } - } - private readonly struct SummaryInitializer - { - public readonly MetricFactory Inner; - public readonly TimeSpan ExpiresAfter; - public readonly string Help; - public readonly SummaryConfiguration? Configuration; + _summariesLock.EnterWriteLock(); - public SummaryInitializer(MetricFactory inner, TimeSpan expiresAfter, string help, SummaryConfiguration? configuration) + try { - Inner = inner; - ExpiresAfter = expiresAfter; - Help = help; - Configuration = configuration; - } + if (_summaries.TryGetValue(identity, out var existing)) + return existing; - public ManagedLifetimeSummary CreateInstance(ManagedLifetimeMetricIdentity identity) + var metric = _inner.CreateSummary(identity.MetricFamilyName, help, identity.InstanceLabelNames, configuration); + var instance = new ManagedLifetimeSummary(metric, _expiresAfter); + _summaries.Add(identity, instance); + return instance; + } + finally { - var metric = Inner.CreateSummary(identity.MetricFamilyName, Help, identity.InstanceLabelNames, Configuration); - return new ManagedLifetimeSummary(metric, ExpiresAfter); + _summariesLock.ExitWriteLock(); } } + + /// + /// Gets all the existing label names predefined either in the factory or in the registry. + /// + internal StringSequence GetAllStaticLabelNames() => _inner.GetAllStaticLabelNames(); + + // We need to reuse existing instances of lifetime-managed metrics because the user might not want to cache it. + // This somewhat duplicates the metric identity tracking logic in CollectorRegistry but this is intentional, as we really do need to do this work on two layers. + // We never remove collectors from here as long as the factory is alive. The expectation is that there is not an unbounded set of label names, so this set is non-gigantic. + private readonly Dictionary _counters = new(); + private readonly ReaderWriterLockSlim _countersLock = new(); + + private readonly Dictionary _gauges = new(); + private readonly ReaderWriterLockSlim _gaugesLock = new(); + + private readonly Dictionary _histograms = new(); + private readonly ReaderWriterLockSlim _histogramsLock = new(); + + private readonly Dictionary _summaries = new(); + private readonly ReaderWriterLockSlim _summariesLock = new(); } diff --git a/Prometheus/ManagedLifetimeMetricIdentity.cs b/Prometheus/ManagedLifetimeMetricIdentity.cs index 3e9b1017..2b4babab 100644 --- a/Prometheus/ManagedLifetimeMetricIdentity.cs +++ b/Prometheus/ManagedLifetimeMetricIdentity.cs @@ -7,7 +7,7 @@ /// Managed lifetime metrics are not differentiated by static labels because the static labels are applied /// in a lower layer (the underlying MetricFactory) and cannot differ within a single ManagedLifetimeMetricFactory. /// -internal struct ManagedLifetimeMetricIdentity : IEquatable +internal readonly struct ManagedLifetimeMetricIdentity : IEquatable { public readonly string MetricFamilyName; public readonly StringSequence InstanceLabelNames; @@ -58,4 +58,9 @@ public override string ToString() { return $"{MetricFamilyName}{InstanceLabelNames}"; } + + public override bool Equals(object? obj) + { + return obj is ManagedLifetimeMetricIdentity identity && Equals(identity); + } } diff --git a/Prometheus/StringSequence.cs b/Prometheus/StringSequence.cs index de79f5d9..39a5f3f9 100644 --- a/Prometheus/StringSequence.cs +++ b/Prometheus/StringSequence.cs @@ -156,6 +156,9 @@ private StringSequence(StringSequence? inheritFrom, StringSequence? thenFrom, st public static StringSequence From(params string[] values) { + if (values.Length == 0) + return Empty; + return new StringSequence(null, null, values); } From 511e4048215c5ea84d681f9b8c23d23241f1995b Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Sun, 26 Nov 2023 13:52:00 +0200 Subject: [PATCH 161/230] ConcurrentDictionary -> RWLS --- ...elEnrichingManagedLifetimeMetricFactory.cs | 129 ++++++++++++++++-- 1 file changed, 121 insertions(+), 8 deletions(-) diff --git a/Prometheus/LabelEnrichingManagedLifetimeMetricFactory.cs b/Prometheus/LabelEnrichingManagedLifetimeMetricFactory.cs index 51feb066..4340f345 100644 --- a/Prometheus/LabelEnrichingManagedLifetimeMetricFactory.cs +++ b/Prometheus/LabelEnrichingManagedLifetimeMetricFactory.cs @@ -35,11 +35,40 @@ public IManagedLifetimeMetricHandle CreateCounter(string name, string // 1-1 relationship between instance of inner handle and our labeling handle. // We expect lifetime of each to match the lifetime of the respective factory, so no need to cleanup anything. - return _counters.GetOrAdd(innerHandle, CreateCounterCore); + + _countersLock.EnterReadLock(); + + try + { + if (_counters.TryGetValue(innerHandle, out var existing)) + return existing; + } + finally + { + _countersLock.ExitReadLock(); + } + + _countersLock.EnterWriteLock(); + + try + { + if (_counters.TryGetValue(innerHandle, out var existing)) + return existing; + + var instance = CreateCounterCore(innerHandle); + _counters.Add(innerHandle, instance); + return instance; + } + finally + { + _countersLock.ExitWriteLock(); + } } private LabelEnrichingManagedLifetimeCounter CreateCounterCore(IManagedLifetimeMetricHandle inner) => new LabelEnrichingManagedLifetimeCounter(inner, _enrichWithLabelValues); - private readonly ConcurrentDictionary, LabelEnrichingManagedLifetimeCounter> _counters = new(); + + private readonly Dictionary, LabelEnrichingManagedLifetimeCounter> _counters = new(); + private readonly ReaderWriterLockSlim _countersLock = new(); public IManagedLifetimeMetricHandle CreateGauge(string name, string help, string[]? instanceLabelNames, GaugeConfiguration? configuration) { @@ -48,11 +77,39 @@ public IManagedLifetimeMetricHandle CreateGauge(string name, string help // 1-1 relationship between instance of inner handle and our labeling handle. // We expect lifetime of each to match the lifetime of the respective factory, so no need to cleanup anything. - return _gauges.GetOrAdd(innerHandle, CreateGaugeCore); + + _gaugesLock.EnterReadLock(); + + try + { + if (_gauges.TryGetValue(innerHandle, out var existing)) + return existing; + } + finally + { + _gaugesLock.ExitReadLock(); + } + + _gaugesLock.EnterWriteLock(); + + try + { + if (_gauges.TryGetValue(innerHandle, out var existing)) + return existing; + + var instance = CreateGaugeCore(innerHandle); + _gauges.Add(innerHandle, instance); + return instance; + } + finally + { + _gaugesLock.ExitWriteLock(); + } } private LabelEnrichingManagedLifetimeGauge CreateGaugeCore(IManagedLifetimeMetricHandle inner) => new LabelEnrichingManagedLifetimeGauge(inner, _enrichWithLabelValues); - private readonly ConcurrentDictionary, LabelEnrichingManagedLifetimeGauge> _gauges = new(); + private readonly Dictionary, LabelEnrichingManagedLifetimeGauge> _gauges = new(); + private readonly ReaderWriterLockSlim _gaugesLock = new(); public IManagedLifetimeMetricHandle CreateHistogram(string name, string help, string[]? instanceLabelNames, HistogramConfiguration? configuration) { @@ -61,11 +118,39 @@ public IManagedLifetimeMetricHandle CreateHistogram(string name, str // 1-1 relationship between instance of inner handle and our labeling handle. // We expect lifetime of each to match the lifetime of the respective factory, so no need to cleanup anything. - return _histograms.GetOrAdd(innerHandle, CreateHistogramCore); + + _histogramsLock.EnterReadLock(); + + try + { + if (_histograms.TryGetValue(innerHandle, out var existing)) + return existing; + } + finally + { + _histogramsLock.ExitReadLock(); + } + + _histogramsLock.EnterWriteLock(); + + try + { + if (_histograms.TryGetValue(innerHandle, out var existing)) + return existing; + + var instance = CreateHistogramCore(innerHandle); + _histograms.Add(innerHandle, instance); + return instance; + } + finally + { + _histogramsLock.ExitWriteLock(); + } } private LabelEnrichingManagedLifetimeHistogram CreateHistogramCore(IManagedLifetimeMetricHandle inner) => new LabelEnrichingManagedLifetimeHistogram(inner, _enrichWithLabelValues); - private readonly ConcurrentDictionary, LabelEnrichingManagedLifetimeHistogram> _histograms = new(); + private readonly Dictionary, LabelEnrichingManagedLifetimeHistogram> _histograms = new(); + private readonly ReaderWriterLockSlim _histogramsLock = new(); public IManagedLifetimeMetricHandle CreateSummary(string name, string help, string[]? instanceLabelNames, SummaryConfiguration? configuration) { @@ -74,11 +159,39 @@ public IManagedLifetimeMetricHandle CreateSummary(string name, string // 1-1 relationship between instance of inner handle and our labeling handle. // We expect lifetime of each to match the lifetime of the respective factory, so no need to cleanup anything. - return _summaries.GetOrAdd(innerHandle, CreateSummaryCore); + + _summariesLock.EnterReadLock(); + + try + { + if (_summaries.TryGetValue(innerHandle, out var existing)) + return existing; + } + finally + { + _summariesLock.ExitReadLock(); + } + + _summariesLock.EnterWriteLock(); + + try + { + if (_summaries.TryGetValue(innerHandle, out var existing)) + return existing; + + var instance = CreateSummaryCore(innerHandle); + _summaries.Add(innerHandle, instance); + return instance; + } + finally + { + _summariesLock.ExitWriteLock(); + } } private LabelEnrichingManagedLifetimeSummary CreateSummaryCore(IManagedLifetimeMetricHandle inner) => new LabelEnrichingManagedLifetimeSummary(inner, _enrichWithLabelValues); - private readonly ConcurrentDictionary, LabelEnrichingManagedLifetimeSummary> _summaries = new(); + private readonly Dictionary, LabelEnrichingManagedLifetimeSummary> _summaries = new(); + private readonly ReaderWriterLockSlim _summariesLock = new(); public IManagedLifetimeMetricFactory WithLabels(IDictionary labels) { From e9aae9d10f4d3db972b5946b5fbc994cf5e766fa Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Sun, 26 Nov 2023 18:29:57 +0200 Subject: [PATCH 162/230] Slightly reduce memory consumption of auto-leasing metrics by removing a lightweight wrapper object from the tree --- Prometheus/AutoLeasingCounter.cs | 62 -------------------- Prometheus/AutoLeasingGauge.cs | 68 ---------------------- Prometheus/AutoLeasingHistogram.cs | 58 ------------------- Prometheus/AutoLeasingSummary.cs | 46 --------------- Prometheus/ManagedLifetimeCounter.cs | 75 ++++++++++++++++++++++-- Prometheus/ManagedLifetimeGauge.cs | 80 ++++++++++++++++++++++++-- Prometheus/ManagedLifetimeHistogram.cs | 68 ++++++++++++++++++++-- Prometheus/ManagedLifetimeSummary.cs | 55 ++++++++++++++++-- 8 files changed, 262 insertions(+), 250 deletions(-) delete mode 100644 Prometheus/AutoLeasingCounter.cs delete mode 100644 Prometheus/AutoLeasingGauge.cs delete mode 100644 Prometheus/AutoLeasingHistogram.cs delete mode 100644 Prometheus/AutoLeasingSummary.cs diff --git a/Prometheus/AutoLeasingCounter.cs b/Prometheus/AutoLeasingCounter.cs deleted file mode 100644 index df31c265..00000000 --- a/Prometheus/AutoLeasingCounter.cs +++ /dev/null @@ -1,62 +0,0 @@ -namespace Prometheus; - -/// -/// A counter that automatically extends the lifetime of a lease-extended metric whenever it is used. -/// It only supports write operations because we cannot guarantee that the metric is still alive when reading. -/// -internal sealed class AutoLeasingCounter : ICollector -{ - public AutoLeasingCounter(IManagedLifetimeMetricHandle inner, ICollector root) - { - _inner = inner; - _root = root; - } - - private readonly IManagedLifetimeMetricHandle _inner; - private readonly ICollector _root; - - public string Name => _root.Name; - public string Help => _root.Help; - public string[] LabelNames => _root.LabelNames; - - public ICounter Unlabelled => new Instance(_inner, Array.Empty()); - - public ICounter WithLabels(params string[] labelValues) - { - return new Instance(_inner, labelValues); - } - - private sealed class Instance : ICounter - { - public Instance(IManagedLifetimeMetricHandle inner, string[] labelValues) - { - _inner = inner; - _labelValues = labelValues; - } - - private readonly IManagedLifetimeMetricHandle _inner; - private readonly string[] _labelValues; - - public double Value => throw new NotSupportedException("Read operations on a lifetime-extending-on-use expiring metric are not supported."); - - public void Inc(double increment) - { - Inc(increment, null); - } - - public void Inc(Exemplar? exemplar) - { - Inc(increment: 1, exemplar: exemplar); - } - - public void Inc(double increment, Exemplar? exemplar) - { - _inner.WithLease(x => x.Inc(increment, exemplar), _labelValues); - } - - public void IncTo(double targetValue) - { - _inner.WithLease(x => x.IncTo(targetValue), _labelValues); - } - } -} diff --git a/Prometheus/AutoLeasingGauge.cs b/Prometheus/AutoLeasingGauge.cs deleted file mode 100644 index b2101da8..00000000 --- a/Prometheus/AutoLeasingGauge.cs +++ /dev/null @@ -1,68 +0,0 @@ -namespace Prometheus -{ - /// - /// A gauge that automatically extends the lifetime of a lease-extended metric whenever it is used. - /// It only supports write operations because we cannot guarantee that the metric is still alive when reading. - /// - internal sealed class AutoLeasingGauge : ICollector - { - public AutoLeasingGauge(IManagedLifetimeMetricHandle inner, ICollector root) - { - _inner = inner; - _root = root; - } - - private readonly IManagedLifetimeMetricHandle _inner; - private readonly ICollector _root; - - public string Name => _root.Name; - public string Help => _root.Help; - public string[] LabelNames => _root.LabelNames; - - public IGauge Unlabelled => new Instance(_inner, Array.Empty()); - - public IGauge WithLabels(params string[] labelValues) - { - return new Instance(_inner, labelValues); - } - - private sealed class Instance : IGauge - { - public Instance(IManagedLifetimeMetricHandle inner, string[] labelValues) - { - _inner = inner; - _labelValues = labelValues; - } - - private readonly IManagedLifetimeMetricHandle _inner; - private readonly string[] _labelValues; - - public double Value => throw new NotSupportedException("Read operations on a lifetime-extending-on-use expiring metric are not supported."); - - public void Inc(double increment = 1) - { - _inner.WithLease(x => x.Inc(increment), _labelValues); - } - - public void Set(double val) - { - _inner.WithLease(x => x.Set(val), _labelValues); - } - - public void Dec(double decrement = 1) - { - _inner.WithLease(x => x.Dec(decrement), _labelValues); - } - - public void IncTo(double targetValue) - { - _inner.WithLease(x => x.IncTo(targetValue), _labelValues); - } - - public void DecTo(double targetValue) - { - _inner.WithLease(x => x.DecTo(targetValue), _labelValues); - } - } - } -} diff --git a/Prometheus/AutoLeasingHistogram.cs b/Prometheus/AutoLeasingHistogram.cs deleted file mode 100644 index c9ca10d4..00000000 --- a/Prometheus/AutoLeasingHistogram.cs +++ /dev/null @@ -1,58 +0,0 @@ -namespace Prometheus; - -/// -/// A histogram that automatically extends the lifetime of a lease-extended metric whenever it is used. -/// It only supports write operations because we cannot guarantee that the metric is still alive when reading. -/// -internal sealed class AutoLeasingHistogram : ICollector -{ - public AutoLeasingHistogram(IManagedLifetimeMetricHandle inner, ICollector root) - { - _inner = inner; - _root = root; - } - - private readonly IManagedLifetimeMetricHandle _inner; - private readonly ICollector _root; - - public string Name => _root.Name; - public string Help => _root.Help; - public string[] LabelNames => _root.LabelNames; - - public IHistogram Unlabelled => new Instance(_inner, Array.Empty()); - - public IHistogram WithLabels(params string[] labelValues) - { - return new Instance(_inner, labelValues); - } - - private sealed class Instance : IHistogram - { - public Instance(IManagedLifetimeMetricHandle inner, string[] labelValues) - { - _inner = inner; - _labelValues = labelValues; - } - - private readonly IManagedLifetimeMetricHandle _inner; - private readonly string[] _labelValues; - - public double Sum => throw new NotSupportedException("Read operations on a lifetime-extending-on-use expiring metric are not supported."); - public long Count => throw new NotSupportedException("Read operations on a lifetime-extending-on-use expiring metric are not supported."); - - public void Observe(double val, long count) - { - _inner.WithLease(x => x.Observe(val, count), _labelValues); - } - - public void Observe(double val, Exemplar? exemplar) - { - _inner.WithLease(x => x.Observe(val, exemplar), _labelValues); - } - - public void Observe(double val) - { - Observe(val, null); - } - } -} diff --git a/Prometheus/AutoLeasingSummary.cs b/Prometheus/AutoLeasingSummary.cs deleted file mode 100644 index 2ac5e056..00000000 --- a/Prometheus/AutoLeasingSummary.cs +++ /dev/null @@ -1,46 +0,0 @@ -namespace Prometheus -{ - /// - /// A Summary that automatically extends the lifetime of a lease-extended metric whenever it is used. - /// It only supports write operations because we cannot guarantee that the metric is still alive when reading. - /// - internal sealed class AutoLeasingSummary : ICollector - { - public AutoLeasingSummary(IManagedLifetimeMetricHandle inner, ICollector root) - { - _inner = inner; - _root = root; - } - - private readonly IManagedLifetimeMetricHandle _inner; - private readonly ICollector _root; - - public string Name => _root.Name; - public string Help => _root.Help; - public string[] LabelNames => _root.LabelNames; - - public ISummary Unlabelled => new Instance(_inner, Array.Empty()); - - public ISummary WithLabels(params string[] labelValues) - { - return new Instance(_inner, labelValues); - } - - private sealed class Instance : ISummary - { - public Instance(IManagedLifetimeMetricHandle inner, string[] labelValues) - { - _inner = inner; - _labelValues = labelValues; - } - - private readonly IManagedLifetimeMetricHandle _inner; - private readonly string[] _labelValues; - - public void Observe(double val) - { - _inner.WithLease(x => x.Observe(val), _labelValues); - } - } - } -} diff --git a/Prometheus/ManagedLifetimeCounter.cs b/Prometheus/ManagedLifetimeCounter.cs index 51f11f51..1ff2d3f4 100644 --- a/Prometheus/ManagedLifetimeCounter.cs +++ b/Prometheus/ManagedLifetimeCounter.cs @@ -1,11 +1,78 @@ -namespace Prometheus +namespace Prometheus; + +/// +/// This class implements two sets of functionality: +/// 1. A lifetime-managed metric handle that can be used to take leases on the metric. +/// 2. An automatically-lifetime-extending-on-use metric that creates leases automatically. +/// +/// While conceptually separate, we merge the two sets into one class to avoid allocating a bunch of small objects +/// every time you want to obtain a lifetime-extending-on-use metric (which tends to be on a relatively hot path). +/// +/// The lifetime-extending feature only supports write operations because we cannot guarantee that the metric is still alive when reading. +/// +internal sealed class ManagedLifetimeCounter : ManagedLifetimeMetricHandle, ICollector { - internal sealed class ManagedLifetimeCounter : ManagedLifetimeMetricHandle + static ManagedLifetimeCounter() + { + _assignUnlabelledFunc = AssignUnlabelled; + } + + public ManagedLifetimeCounter(Collector metric, TimeSpan expiresAfter) : base(metric, expiresAfter) + { + } + + public override ICollector WithExtendLifetimeOnUse() => this; + + #region ICollector implementation (for WithExtendLifetimeOnUse) + public string Name => _metric.Name; + public string Help => _metric.Help; + public string[] LabelNames => _metric.LabelNames; + + public ICounter Unlabelled => NonCapturingLazyInitializer.EnsureInitialized(ref _unlabelled, this, _assignUnlabelledFunc); + private AutoLeasingInstance? _unlabelled; + private static readonly Action _assignUnlabelledFunc; + private static void AssignUnlabelled(ManagedLifetimeCounter instance) => instance._unlabelled = new AutoLeasingInstance(instance, Array.Empty()); + + // These do not get cached, so are potentially expensive - user code should try avoiding re-allocating these when possible, + // though admittedly this may not be so easy as often these are on the hot path and the very reason that lifetime-managed + // metrics are used is that we do not have a meaningful way to reuse metrics or identify their lifetime. + public ICounter WithLabels(params string[] labelValues) + { + return new AutoLeasingInstance(this, labelValues); + } + #endregion + + private sealed class AutoLeasingInstance : ICounter { - public ManagedLifetimeCounter(Collector metric, TimeSpan expiresAfter) : base(metric, expiresAfter) + public AutoLeasingInstance(IManagedLifetimeMetricHandle inner, string[] labelValues) { + _inner = inner; + _labelValues = labelValues; } - public override ICollector WithExtendLifetimeOnUse() => new AutoLeasingCounter(this, _metric); + private readonly IManagedLifetimeMetricHandle _inner; + private readonly string[] _labelValues; + + public double Value => throw new NotSupportedException("Read operations on a lifetime-extending-on-use expiring metric are not supported."); + + public void Inc(double increment) + { + Inc(increment, null); + } + + public void Inc(Exemplar? exemplar) + { + Inc(increment: 1, exemplar: exemplar); + } + + public void Inc(double increment, Exemplar? exemplar) + { + _inner.WithLease(x => x.Inc(increment, exemplar), _labelValues); + } + + public void IncTo(double targetValue) + { + _inner.WithLease(x => x.IncTo(targetValue), _labelValues); + } } } diff --git a/Prometheus/ManagedLifetimeGauge.cs b/Prometheus/ManagedLifetimeGauge.cs index 70ef50d0..3b43e33e 100644 --- a/Prometheus/ManagedLifetimeGauge.cs +++ b/Prometheus/ManagedLifetimeGauge.cs @@ -1,11 +1,83 @@ -namespace Prometheus +namespace Prometheus; + +/// +/// This class implements two sets of functionality: +/// 1. A lifetime-managed metric handle that can be used to take leases on the metric. +/// 2. An automatically-lifetime-extending-on-use metric that creates leases automatically. +/// +/// While conceptually separate, we merge the two sets into one class to avoid allocating a bunch of small objects +/// every time you want to obtain a lifetime-extending-on-use metric (which tends to be on a relatively hot path). +/// +/// The lifetime-extending feature only supports write operations because we cannot guarantee that the metric is still alive when reading. +/// +internal sealed class ManagedLifetimeGauge : ManagedLifetimeMetricHandle, ICollector { - internal sealed class ManagedLifetimeGauge : ManagedLifetimeMetricHandle + static ManagedLifetimeGauge() + { + _assignUnlabelledFunc = AssignUnlabelled; + } + + public ManagedLifetimeGauge(Collector metric, TimeSpan expiresAfter) : base(metric, expiresAfter) { - public ManagedLifetimeGauge(Collector metric, TimeSpan expiresAfter) : base(metric, expiresAfter) + } + + public override ICollector WithExtendLifetimeOnUse() => this; + + #region ICollector implementation (for WithExtendLifetimeOnUse) + public string Name => _metric.Name; + public string Help => _metric.Help; + public string[] LabelNames => _metric.LabelNames; + + public IGauge Unlabelled => NonCapturingLazyInitializer.EnsureInitialized(ref _unlabelled, this, _assignUnlabelledFunc); + private AutoLeasingInstance? _unlabelled; + private static readonly Action _assignUnlabelledFunc; + private static void AssignUnlabelled(ManagedLifetimeGauge instance) => instance._unlabelled = new AutoLeasingInstance(instance, Array.Empty()); + + // These do not get cached, so are potentially expensive - user code should try avoiding re-allocating these when possible, + // though admittedly this may not be so easy as often these are on the hot path and the very reason that lifetime-managed + // metrics are used is that we do not have a meaningful way to reuse metrics or identify their lifetime. + public IGauge WithLabels(params string[] labelValues) + { + return new AutoLeasingInstance(this, labelValues); + } + #endregion + + private sealed class AutoLeasingInstance : IGauge + { + public AutoLeasingInstance(IManagedLifetimeMetricHandle inner, string[] labelValues) { + _inner = inner; + _labelValues = labelValues; } - public override ICollector WithExtendLifetimeOnUse() => new AutoLeasingGauge(this, _metric); + private readonly IManagedLifetimeMetricHandle _inner; + private readonly string[] _labelValues; + + public double Value => throw new NotSupportedException("Read operations on a lifetime-extending-on-use expiring metric are not supported."); + + public void Inc(double increment = 1) + { + _inner.WithLease(x => x.Inc(increment), _labelValues); + } + + public void Set(double val) + { + _inner.WithLease(x => x.Set(val), _labelValues); + } + + public void Dec(double decrement = 1) + { + _inner.WithLease(x => x.Dec(decrement), _labelValues); + } + + public void IncTo(double targetValue) + { + _inner.WithLease(x => x.IncTo(targetValue), _labelValues); + } + + public void DecTo(double targetValue) + { + _inner.WithLease(x => x.DecTo(targetValue), _labelValues); + } } } diff --git a/Prometheus/ManagedLifetimeHistogram.cs b/Prometheus/ManagedLifetimeHistogram.cs index d28e18fb..6b88e45d 100644 --- a/Prometheus/ManagedLifetimeHistogram.cs +++ b/Prometheus/ManagedLifetimeHistogram.cs @@ -1,11 +1,71 @@ -namespace Prometheus +namespace Prometheus; + +/// +/// This class implements two sets of functionality: +/// 1. A lifetime-managed metric handle that can be used to take leases on the metric. +/// 2. An automatically-lifetime-extending-on-use metric that creates leases automatically. +/// +/// While conceptually separate, we merge the two sets into one class to avoid allocating a bunch of small objects +/// every time you want to obtain a lifetime-extending-on-use metric (which tends to be on a relatively hot path). +/// +/// The lifetime-extending feature only supports write operations because we cannot guarantee that the metric is still alive when reading. +/// +internal sealed class ManagedLifetimeHistogram : ManagedLifetimeMetricHandle, ICollector { - internal sealed class ManagedLifetimeHistogram : ManagedLifetimeMetricHandle + static ManagedLifetimeHistogram() + { + _assignUnlabelledFunc = AssignUnlabelled; + } + + public ManagedLifetimeHistogram(Collector metric, TimeSpan expiresAfter) : base(metric, expiresAfter) + { + } + + public override ICollector WithExtendLifetimeOnUse() => this; + + #region ICollector implementation (for WithExtendLifetimeOnUse) + public string Name => _metric.Name; + public string Help => _metric.Help; + public string[] LabelNames => _metric.LabelNames; + + public IHistogram Unlabelled => NonCapturingLazyInitializer.EnsureInitialized(ref _unlabelled, this, _assignUnlabelledFunc); + private AutoLeasingInstance? _unlabelled; + private static readonly Action _assignUnlabelledFunc; + private static void AssignUnlabelled(ManagedLifetimeHistogram instance) => instance._unlabelled = new AutoLeasingInstance(instance, Array.Empty()); + + public IHistogram WithLabels(params string[] labelValues) { - public ManagedLifetimeHistogram(Collector metric, TimeSpan expiresAfter) : base(metric, expiresAfter) + return new AutoLeasingInstance(this, labelValues); + } + #endregion + + private sealed class AutoLeasingInstance : IHistogram + { + public AutoLeasingInstance(IManagedLifetimeMetricHandle inner, string[] labelValues) + { + _inner = inner; + _labelValues = labelValues; + } + + private readonly IManagedLifetimeMetricHandle _inner; + private readonly string[] _labelValues; + + public double Sum => throw new NotSupportedException("Read operations on a lifetime-extending-on-use expiring metric are not supported."); + public long Count => throw new NotSupportedException("Read operations on a lifetime-extending-on-use expiring metric are not supported."); + + public void Observe(double val, long count) { + _inner.WithLease(x => x.Observe(val, count), _labelValues); } - public override ICollector WithExtendLifetimeOnUse() => new AutoLeasingHistogram(this, _metric); + public void Observe(double val, Exemplar? exemplar) + { + _inner.WithLease(x => x.Observe(val, exemplar), _labelValues); + } + + public void Observe(double val) + { + Observe(val, null); + } } } diff --git a/Prometheus/ManagedLifetimeSummary.cs b/Prometheus/ManagedLifetimeSummary.cs index 6433aacf..0e6c1788 100644 --- a/Prometheus/ManagedLifetimeSummary.cs +++ b/Prometheus/ManagedLifetimeSummary.cs @@ -1,11 +1,58 @@ -namespace Prometheus +namespace Prometheus; + +/// +/// This class implements two sets of functionality: +/// 1. A lifetime-managed metric handle that can be used to take leases on the metric. +/// 2. An automatically-lifetime-extending-on-use metric that creates leases automatically. +/// +/// While conceptually separate, we merge the two sets into one class to avoid allocating a bunch of small objects +/// every time you want to obtain a lifetime-extending-on-use metric (which tends to be on a relatively hot path). +/// +/// The lifetime-extending feature only supports write operations because we cannot guarantee that the metric is still alive when reading. +/// +internal sealed class ManagedLifetimeSummary : ManagedLifetimeMetricHandle, ICollector { - internal sealed class ManagedLifetimeSummary : ManagedLifetimeMetricHandle + static ManagedLifetimeSummary() + { + _assignUnlabelledFunc = AssignUnlabelled; + } + + public ManagedLifetimeSummary(Collector metric, TimeSpan expiresAfter) : base(metric, expiresAfter) + { + } + + public override ICollector WithExtendLifetimeOnUse() => this; + + #region ICollector implementation (for WithExtendLifetimeOnUse) + public string Name => _metric.Name; + public string Help => _metric.Help; + public string[] LabelNames => _metric.LabelNames; + + public ISummary Unlabelled => NonCapturingLazyInitializer.EnsureInitialized(ref _unlabelled, this, _assignUnlabelledFunc); + private AutoLeasingInstance? _unlabelled; + private static readonly Action _assignUnlabelledFunc; + private static void AssignUnlabelled(ManagedLifetimeSummary instance) => instance._unlabelled = new AutoLeasingInstance(instance, Array.Empty()); + + public ISummary WithLabels(params string[] labelValues) + { + return new AutoLeasingInstance(this, labelValues); + } + #endregion + + private sealed class AutoLeasingInstance : ISummary { - public ManagedLifetimeSummary(Collector metric, TimeSpan expiresAfter) : base(metric, expiresAfter) + public AutoLeasingInstance(IManagedLifetimeMetricHandle inner, string[] labelValues) { + _inner = inner; + _labelValues = labelValues; } - public override ICollector WithExtendLifetimeOnUse() => new AutoLeasingSummary(this, _metric); + private readonly IManagedLifetimeMetricHandle _inner; + private readonly string[] _labelValues; + + public void Observe(double val) + { + _inner.WithLease(x => x.Observe(val), _labelValues); + } } } From 2a1eda1f9808c050838c75e180ef4fbbe97be8d7 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Sun, 26 Nov 2023 18:34:33 +0200 Subject: [PATCH 163/230] comments --- Prometheus/ManagedLifetimeHistogram.cs | 3 +++ Prometheus/ManagedLifetimeSummary.cs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/Prometheus/ManagedLifetimeHistogram.cs b/Prometheus/ManagedLifetimeHistogram.cs index 6b88e45d..a2aea74b 100644 --- a/Prometheus/ManagedLifetimeHistogram.cs +++ b/Prometheus/ManagedLifetimeHistogram.cs @@ -33,6 +33,9 @@ public ManagedLifetimeHistogram(Collector metric, TimeSpan expi private static readonly Action _assignUnlabelledFunc; private static void AssignUnlabelled(ManagedLifetimeHistogram instance) => instance._unlabelled = new AutoLeasingInstance(instance, Array.Empty()); + // These do not get cached, so are potentially expensive - user code should try avoiding re-allocating these when possible, + // though admittedly this may not be so easy as often these are on the hot path and the very reason that lifetime-managed + // metrics are used is that we do not have a meaningful way to reuse metrics or identify their lifetime. public IHistogram WithLabels(params string[] labelValues) { return new AutoLeasingInstance(this, labelValues); diff --git a/Prometheus/ManagedLifetimeSummary.cs b/Prometheus/ManagedLifetimeSummary.cs index 0e6c1788..39c1b1d8 100644 --- a/Prometheus/ManagedLifetimeSummary.cs +++ b/Prometheus/ManagedLifetimeSummary.cs @@ -33,6 +33,9 @@ public ManagedLifetimeSummary(Collector metric, TimeSpan expiresA private static readonly Action _assignUnlabelledFunc; private static void AssignUnlabelled(ManagedLifetimeSummary instance) => instance._unlabelled = new AutoLeasingInstance(instance, Array.Empty()); + // These do not get cached, so are potentially expensive - user code should try avoiding re-allocating these when possible, + // though admittedly this may not be so easy as often these are on the hot path and the very reason that lifetime-managed + // metrics are used is that we do not have a meaningful way to reuse metrics or identify their lifetime. public ISummary WithLabels(params string[] labelValues) { return new AutoLeasingInstance(this, labelValues); From 1b989f9c4ee05e4cd63183756e0bda2d781c22cf Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Sun, 26 Nov 2023 18:57:23 +0200 Subject: [PATCH 164/230] Reduce memory use in lifetime-managed metrics by reducing delegate allocations --- Prometheus/ManagedLifetimeMetricHandle.cs | 24 ++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/Prometheus/ManagedLifetimeMetricHandle.cs b/Prometheus/ManagedLifetimeMetricHandle.cs index 7b520c1f..9e71412f 100644 --- a/Prometheus/ManagedLifetimeMetricHandle.cs +++ b/Prometheus/ManagedLifetimeMetricHandle.cs @@ -8,6 +8,9 @@ internal abstract class ManagedLifetimeMetricHandle : { internal ManagedLifetimeMetricHandle(Collector metric, TimeSpan expiresAfter) { + _createLifetimeManagerFunc = CreateLifetimeManager; + _deleteMetricOuterFunc = DeleteMetricOuter; + _metric = metric; _expiresAfter = expiresAfter; } @@ -81,13 +84,15 @@ private sealed class LifetimeManager { public LifetimeManager(TChild child, TimeSpan expiresAfter, IDelayer delayer, Action remove) { + _releaseLeaseFunc = ReleaseLease; + _child = child; _expiresAfter = expiresAfter; _delayer = delayer; _remove = remove; // NB! There may be optimistic copies made by the ConcurrentDictionary - this may be such a copy! - _reusableLease = new ReusableLease(ReleaseLease); + _reusableLease = new ReusableLease(_releaseLeaseFunc); } private readonly TChild _child; @@ -112,7 +117,7 @@ public IDisposable TakeLease() { TakeLeaseCore(); - return new Lease(ReleaseLease); + return new Lease(_releaseLeaseFunc); } /// @@ -145,6 +150,8 @@ private void ReleaseLease() } } + private readonly Action _releaseLeaseFunc; + private void EnsureExpirationTimerStarted() { if (_timerStarted) @@ -251,6 +258,9 @@ public void Dispose() /// private readonly ConcurrentDictionary _lifetimeManagers = new(); + // This is not used for collection reads/writes but rather to block taking leases when we are ending a lifetime: + // read == taking a lease. + // write == lifetime being ended. private readonly ReaderWriterLockSlim _lifetimeManagersLock = new(); /// @@ -295,13 +305,11 @@ private LifetimeManager GetOrAddLifetimeManagerCore(TChild child) if (_lifetimeManagers.TryGetValue(child, out var existing)) return existing; - return _lifetimeManagers.GetOrAdd(child, CreateLifetimeManager); + return _lifetimeManagers.GetOrAdd(child, _createLifetimeManagerFunc); } - private LifetimeManager CreateLifetimeManager(TChild child) - { - return new LifetimeManager(child, _expiresAfter, Delayer, DeleteMetricOuter); - } + private LifetimeManager CreateLifetimeManager(TChild child) => new(child, _expiresAfter, Delayer, _deleteMetricOuterFunc); + private readonly Func _createLifetimeManagerFunc; /// /// Performs the locking necessary to ensure that a LifetimeManager that ends the lifetime does not get reused. @@ -321,4 +329,6 @@ private void DeleteMetricOuter(TChild child) _lifetimeManagersLock.ExitWriteLock(); } } + + private readonly Action _deleteMetricOuterFunc; } \ No newline at end of file From 87ad4479c03fb7bd55091687eb353ca656938c90 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Sun, 26 Nov 2023 23:47:09 +0200 Subject: [PATCH 165/230] Rewrite lifetime tracking in ManagedLifetimeMetricHandle to be leaner --- Prometheus/LowGranularityTimeSource.cs | 24 +- Prometheus/ManagedLifetimeMetricHandle.cs | 456 ++++++++++++---------- Tests.NetCore/MetricExpirationTests.cs | 30 +- 3 files changed, 288 insertions(+), 222 deletions(-) diff --git a/Prometheus/LowGranularityTimeSource.cs b/Prometheus/LowGranularityTimeSource.cs index 6a0b65de..92989936 100644 --- a/Prometheus/LowGranularityTimeSource.cs +++ b/Prometheus/LowGranularityTimeSource.cs @@ -1,4 +1,6 @@ -namespace Prometheus; +using System.Diagnostics; + +namespace Prometheus; /// /// We occasionally need timestamps to attach to metrics metadata. In high-performance code, calling the standard get-timestamp functions can be a nontrivial cost. @@ -9,19 +11,35 @@ internal static class LowGranularityTimeSource [ThreadStatic] private static double LastUnixSeconds; + [ThreadStatic] + private static long LastStopwatchTimestamp; + [ThreadStatic] private static int LastTickCount; public static double GetSecondsFromUnixEpoch() + { + UpdateIfRequired(); + + return LastUnixSeconds; + } + + public static long GetStopwatchTimestamp() + { + UpdateIfRequired(); + + return LastStopwatchTimestamp; + } + + private static void UpdateIfRequired() { var currentTickCount = Environment.TickCount; if (LastTickCount != currentTickCount) { LastUnixSeconds = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() / 1000.0; + LastStopwatchTimestamp = Stopwatch.GetTimestamp(); LastTickCount = currentTickCount; } - - return LastUnixSeconds; } } diff --git a/Prometheus/ManagedLifetimeMetricHandle.cs b/Prometheus/ManagedLifetimeMetricHandle.cs index 9e71412f..e282cece 100644 --- a/Prometheus/ManagedLifetimeMetricHandle.cs +++ b/Prometheus/ManagedLifetimeMetricHandle.cs @@ -1,15 +1,24 @@ -using System.Collections.Concurrent; +using System.Buffers; +using System.Diagnostics; namespace Prometheus; +/// +/// Represents a metric whose lifetime is managed by the caller, either via explicit leases or via extend-on-use behavior (implicit leases). +/// +/// +/// Each metric handle maintains a reaper task that occasionally removes metrics that have expired. The reaper is started +/// when the first lifetime-managed metric is created and terminates when the last lifetime-managed metric expires. +/// This does mean that the metric handle may keep objects alive until expiration, even if the handle itself is no longer used. +/// TODO: Can we do something to reduce that risk? +/// internal abstract class ManagedLifetimeMetricHandle : IManagedLifetimeMetricHandle where TChild : ChildBase, TMetricInterface where TMetricInterface : ICollectorChild { internal ManagedLifetimeMetricHandle(Collector metric, TimeSpan expiresAfter) { - _createLifetimeManagerFunc = CreateLifetimeManager; - _deleteMetricOuterFunc = DeleteMetricOuter; + _reaperFunc = Reaper; _metric = metric; _expiresAfter = expiresAfter; @@ -29,7 +38,7 @@ public IDisposable AcquireLease(out TMetricInterface metric, params string[] lab public void WithLease(Action action, params string[] labelValues) { var child = _metric.WithLabels(labelValues); - var lease = TakeLeaseFast(child); + var lease = TakeRefLease(child); try { @@ -66,269 +75,300 @@ public async Task WithLeaseAsync(Func internal IDelayer Delayer = RealDelayer.Instance; - /// - /// An instance of LifetimeManager takes care of the lifetime of a single child metric: - /// * It maintains the count of active leases. - /// * It schedules removal for a suitable moment after the last lease is released. - /// - /// Once the lifetime manager decides to remove the metric, it can no longer be used and a new lifetime manager must be allocated. - /// Taking new leases after removal will have no effect without recycling the lifetime manager (because it will be a lease on - /// a metric instance that has already been removed from its parent metric family - even if you update the value, it is no longer exported). - /// - /// - /// Expiration is managed on a loosely accurate method - when the first lease is taken, an expiration timer is started. - /// This timer will tick at a regular interval and, upon each tick, check whether the metric needs to expire. That's it. - /// The metric expiration is guaranteed to be no less than [expiresAfter] has elapsed, but may be more as the timer ticks on its own clock. - /// - private sealed class LifetimeManager + #region Lease tracking + // Contents modified via atomic operations, not guarded by locks. + private sealed class LifetimeInfo { - public LifetimeManager(TChild child, TimeSpan expiresAfter, IDelayer delayer, Action remove) - { - _releaseLeaseFunc = ReleaseLease; - - _child = child; - _expiresAfter = expiresAfter; - _delayer = delayer; - _remove = remove; + // Number of active leases. Nonzero value here indicates the lifetime extends forever. + public int LeaseCount; - // NB! There may be optimistic copies made by the ConcurrentDictionary - this may be such a copy! - _reusableLease = new ReusableLease(_releaseLeaseFunc); - } - - private readonly TChild _child; - private readonly TimeSpan _expiresAfter; - private readonly IDelayer _delayer; - private readonly Action _remove; + // When the last lifetime related activity was performed. Expiration timer starts counting from here. + // This is refreshed whenever a lease is released (a kept lease is a forever-keepalive, so we only care about releasing). + public long KeepaliveTimestamp; - private readonly object _lock = new(); - private int _leaseCount = 0; + // The lifetime has been ended, potentially while a lease was active. The next time a lease ends, + // it will have to re-register the lifetime instead of just extending the existing one. + public bool Ended; + } - // Taking or releasing a lease will always start a new epoch. The expiration timer simply checks whether the epoch changes between two ticks. - // If the epoch changes, it must mean there was some lease-related activity and it will do nothing. If the epoch remains the same and the lease - // count is 0, the metric has expired and will be removed. - private int _epoch = 0; + private readonly Dictionary _lifetimes = new(); - // We start the expiration timer the first time a lease is taken. - private bool _timerStarted; + // Guards the collection but not the contents. + private readonly ReaderWriterLockSlim _lifetimesLock = new(); - private readonly ReusableLease _reusableLease; + private bool HasAnyTrackedLifetimes() + { + _lifetimesLock.EnterReadLock(); - public IDisposable TakeLease() + try { - TakeLeaseCore(); - - return new Lease(_releaseLeaseFunc); + return _lifetimes.Count != 0; } - - /// - /// Returns a reusable lease-releaser object. Only for internal use - to avoid allocating on every lease. - /// - internal IDisposable TakeLeaseFast() + finally { - TakeLeaseCore(); - - return _reusableLease; + _lifetimesLock.ExitReadLock(); } + } - private void TakeLeaseCore() - { - lock (_lock) - { - EnsureExpirationTimerStarted(); - - _leaseCount++; - unchecked { _epoch++; } - } - } + /// + /// For testing only. Sets all keepalive timestamps to 0, which will cause all lifetimes to expire (if they have no leases). + /// + internal void ZeroAllKeepaliveTimestamps() + { + _lifetimesLock.EnterReadLock(); - private void ReleaseLease() + try { - lock (_lock) - { - _leaseCount--; - unchecked { _epoch++; } - } + foreach (var lifetime in _lifetimes.Values) + Volatile.Write(ref lifetime.KeepaliveTimestamp, 0L); } - - private readonly Action _releaseLeaseFunc; - - private void EnsureExpirationTimerStarted() + finally { - if (_timerStarted) - return; - - _timerStarted = true; - - _ = Task.Run(ExecuteExpirationTimer); + _lifetimesLock.ExitReadLock(); } + } - private async Task ExecuteExpirationTimer() - { - while (true) - { - int epochBeforeDelay; - - lock (_lock) - epochBeforeDelay = _epoch; + private IDisposable TakeLease(TChild child) + { + var lifetime = GetOrCreateLifetimeAndIncrementLeaseCount(child); + EnsureReaperActive(); - // We iterate on the expiration interval. This means that the real lifetime of a metric may be up to 2x the expiration interval. - // This is fine - we are intentionally loose here, to avoid the timer logic being scheduled too aggressively. Approximate is good enough. - await _delayer.Delay(_expiresAfter); + return new Lease(this, child, lifetime); + } - lock (_lock) - { - if (_leaseCount != 0) - continue; // Will not expire if there are active leases. + private RefLease TakeRefLease(TChild child) + { + var lifetime = GetOrCreateLifetimeAndIncrementLeaseCount(child); + EnsureReaperActive(); - if (_epoch != epochBeforeDelay) - continue; // Will not expire if some leasing activity happened during this interval. - } + return new RefLease(this, child, lifetime); + } - // Expired! - // - // It is possible that a new lease still gets taken before this call completes, because we are not yet holding the lifetime manager write lock that - // guards against new leases being taken. In that case, the new lease will be a dud - it will fail to extend the lifetime because the removal happens - // already now, even if the new lease is taken. This is intentional, to keep the code simple. - _remove(_child); - break; - } - } + private LifetimeInfo GetOrCreateLifetimeAndIncrementLeaseCount(TChild child) + { + _lifetimesLock.EnterReadLock(); - private sealed class Lease : IDisposable + try { - public Lease(Action releaseLease) - { - _releaseLease = releaseLease; - } - - ~Lease() + // Ideally, there already exists a registered lifetime for this metric instance. + if (_lifetimes.TryGetValue(child, out var lifetime)) { - // Anomalous but we'll do the best we can. - Dispose(); + // Immediately increment it, to reduce the risk of any concurrent activities ending the lifetime. + Interlocked.Increment(ref lifetime.LeaseCount); + return lifetime; } + } + finally + { + _lifetimesLock.ExitReadLock(); + } - private readonly Action _releaseLease; - - private bool _disposed; - private readonly object _lock = new(); - - public void Dispose() - { - lock (_lock) - { - if (_disposed) - return; - - _disposed = true; - } + // No lifetime registered yet - we need to take a write lock and register it. - _releaseLease(); - GC.SuppressFinalize(this); - } - } + _lifetimesLock.EnterWriteLock(); - public sealed class ReusableLease : IDisposable + try { - public ReusableLease(Action releaseLease) + // Did we get lucky and someone already registered it? + if (_lifetimes.TryGetValue(child, out var lifetime)) { - _releaseLease = releaseLease; + // Immediately increment it, to reduce the risk of any concurrent activities ending the lifetime. + Interlocked.Increment(ref lifetime.LeaseCount); + return lifetime; } - private readonly Action _releaseLease; - - public void Dispose() + // Did not get lucky. Make a new one. + lifetime = new LifetimeInfo { - _releaseLease(); - } - } - } - - /// - /// The lifetime manager of each child is stored here. We optimistically allocate them to avoid synchronization on the hot path. - /// We only synchronize when disposing of children whose lifetime has expired, to avoid racing between concurrent removal and re-publishing. - /// - /// Avoiding races during lifetime manager allocation: - /// * Creating a new instance of LifetimeManager is harmless in duplicate. - /// - An instance of LifetimeManager will only "start" once its methods are called, not in its ctor. - /// - ConcurrentDictionary will throw away an optimistically created duplicate. - /// * Creating a new instance takes a reader lock to allow allocation to be blocked by removal logic. - /// * Removal will take a writer lock to prevent concurrent allocataions (which also implies preventing concurrent new leases that might "renew" a lifetime). - /// - It can be that between "deletion needed" event and write lock being taken, the state of the lifetime manager changes because of - /// actions done by holders of the read lock (e.g. new lease added). For code simplicity, we accept this as a gap where we may lose data (such a lease fails to renew/start a lifetime). - /// - private readonly ConcurrentDictionary _lifetimeManagers = new(); - - // This is not used for collection reads/writes but rather to block taking leases when we are ending a lifetime: - // read == taking a lease. - // write == lifetime being ended. - private readonly ReaderWriterLockSlim _lifetimeManagersLock = new(); - - /// - /// Takes a new lease on a child, allocating a new lifetime manager if necessary. - /// Any number of leases may be held concurrently on the same child. - /// As soon as the last lease is released, the child is eligible for removal, though new leases may still be taken to extend the lifetime. - /// - private IDisposable TakeLease(TChild child) - { - // We synchronize here to ensure that we do not get a LifetimeManager that has already ended the lifetime. - _lifetimeManagersLock.EnterReadLock(); + LeaseCount = 1 + }; - try - { - return GetOrAddLifetimeManagerCore(child).TakeLease(); + _lifetimes.Add(child, lifetime); + return lifetime; } finally { - _lifetimeManagersLock.ExitReadLock(); + _lifetimesLock.ExitWriteLock(); } } - // Non-allocating variant, for internal use via WithLease(). - private IDisposable TakeLeaseFast(TChild child) + private void OnLeaseEnded(TChild child, LifetimeInfo lifetime) { - // We synchronize here to ensure that we do not get a LifetimeManager that has already ended the lifetime. - _lifetimeManagersLock.EnterReadLock(); + // Update keepalive timestamp before anything else, to avoid racing. + Volatile.Write(ref lifetime.KeepaliveTimestamp, LowGranularityTimeSource.GetStopwatchTimestamp()); - try + // If the lifetime has been ended while we still held a lease, it means there was a race that we lost. + // The metric instance may or may not be still alive. To ensure proper cleanup, we re-register a lifetime + // for the metric instance, which will ensure it gets cleaned up when it expires. + if (Volatile.Read(ref lifetime.Ended)) { - return GetOrAddLifetimeManagerCore(child).TakeLeaseFast(); - } - finally - { - _lifetimeManagersLock.ExitReadLock(); + // We just take a new lease and immediately dispose it. We are guaranteed not to loop here because the + // reaper removes lifetimes from the dictionary once ended, so we can never run into the same lifetime again. + TakeRefLease(child).Dispose(); } + + // Finally, decrement the lease count to relinquish any claim on extending the lifetime. + Interlocked.Decrement(ref lifetime.LeaseCount); } - private LifetimeManager GetOrAddLifetimeManagerCore(TChild child) + private sealed class Lease(ManagedLifetimeMetricHandle parent, TChild child, LifetimeInfo lifetime) : IDisposable { - // Let's assume optimistically that in the typical case, there already is a lifetime manager for it. - if (_lifetimeManagers.TryGetValue(child, out var existing)) - return existing; + public void Dispose() => parent.OnLeaseEnded(child, lifetime); + } - return _lifetimeManagers.GetOrAdd(child, _createLifetimeManagerFunc); + private readonly ref struct RefLease(ManagedLifetimeMetricHandle parent, TChild child, LifetimeInfo lifetime) + { + public void Dispose() => parent.OnLeaseEnded(child, lifetime); } + #endregion + + #region Reaper + // Whether the reaper is currently active. This is set to true when a metric instance is created and + // reset when the last metric instance expires (after which it may be set again). + // We use atomic operations without locking. + private int _reaperActiveBool = ReaperInactive; - private LifetimeManager CreateLifetimeManager(TChild child) => new(child, _expiresAfter, Delayer, _deleteMetricOuterFunc); - private readonly Func _createLifetimeManagerFunc; + private const int ReaperActive = 1; + private const int ReaperInactive = 0; /// - /// Performs the locking necessary to ensure that a LifetimeManager that ends the lifetime does not get reused. + /// Call this immediately after creating a metric instance that will eventually expire. /// - private void DeleteMetricOuter(TChild child) + private void EnsureReaperActive() { - _lifetimeManagersLock.EnterWriteLock(); - - try + if (Interlocked.CompareExchange(ref _reaperActiveBool, ReaperActive, ReaperInactive) == ReaperActive) { - // We assume here that LifetimeManagers are not so buggy to call this method twice (when another LifetimeManager has replaced the old one). - _ = _lifetimeManagers.TryRemove(child, out _); - child.Remove(); + // It was already active - nothing for us to do. + return; } - finally + + _ = Task.Run(_reaperFunc); + } + + // Reimplementation of Stopwatch.GetElapsedTime (only available on .NET 7 or newer). + private static TimeSpan GetElapsedTime(long start, long end) + => new((long)((end - start) * ((double)10_000_000 / Stopwatch.Frequency))); + + private async Task Reaper() + { + while (true) { - _lifetimeManagersLock.ExitWriteLock(); + var now = LowGranularityTimeSource.GetStopwatchTimestamp(); + + // Will contains the results of pass 1. + TChild[] expiredInstancesBuffer = null!; + int expiredInstanceCount = 0; + + // Pass 1: holding only a read lock, make a list of metric instances that have expired. + _lifetimesLock.EnterReadLock(); + + try + { + try + { + expiredInstancesBuffer = ArrayPool.Shared.Rent(_lifetimes.Count); + + foreach (var pair in _lifetimes) + { + if (Volatile.Read(ref pair.Value.LeaseCount) != 0) + continue; // Not expired. + + if (GetElapsedTime(Volatile.Read(ref pair.Value.KeepaliveTimestamp), now) < _expiresAfter) + continue; // Not expired. + + // No leases and keepalive has expired - it is an expired instance! + expiredInstancesBuffer[expiredInstanceCount++] = pair.Key; + } + } + finally + { + _lifetimesLock.ExitReadLock(); + } + + // Pass 2: if we have any work to do, take a write lock and remove the expired metric instances, + // assuming our judgement about their expiration remains valid. We process and lock one by one, + // to avoid holding locks for a long duration if many items expire at once - we are not in any rush. + for (var i = 0; i < expiredInstanceCount; i++) + { + var expiredInstance = expiredInstancesBuffer[i]; + + _lifetimesLock.EnterWriteLock(); + + try + { + if (!_lifetimes.TryGetValue(expiredInstance, out var lifetime)) + continue; // Already gone, nothing for us to do. + + // We need to check again whether the metric instance is still expired, because it may have been + // renewed by a new lease in the meantime. If it is still expired, we can remove it. + if (Volatile.Read(ref lifetime.LeaseCount) != 0) + continue; // Not expired. + + if (GetElapsedTime(Volatile.Read(ref lifetime.KeepaliveTimestamp), now) < _expiresAfter) + continue; // Not expired. + + // No leases and keepalive has expired - it is an expired instance! + + // We mark the old lifetime as ended - if it happened that it got associated with a new lease + // (which is possible because we do not prevent lease-taking while in this loop), the new lease + // upon being ended will re-register the lifetime instead of just extending the existing one. + // We can be certain that any concurrent lifetime-affecting logic is using the same LifetimeInfo + // instance because the lifetime dictionary remains locked until we are done (by which time this flag is set). + Volatile.Write(ref lifetime.Ended, true); + + _lifetimes.Remove(expiredInstance); + + // If we did encounter a race, removing the metric instance here means that some metric value updates + // may go missing (until the next lease creates a new instance). This is acceptable behavior, to keep the code simple. + expiredInstance.Remove(); + } + finally + { + _lifetimesLock.ExitWriteLock(); + } + } + + // Check if we need to shut down the reaper or keep going. + _lifetimesLock.EnterReadLock(); + + try + { + if (_lifetimes.Count == 0) + { + CleanupReaper(); + return; + } + } + finally + { + _lifetimesLock.ExitReadLock(); + } + + // Work done! Go sleep a bit and come back when something may have expired. + // We do not need to be too aggressive here, as expiration is not a hard schedule guarantee. + await Delayer.Delay(_expiresAfter); + } + finally + { + ArrayPool.Shared.Return(expiredInstancesBuffer); + } } } - private readonly Action _deleteMetricOuterFunc; + /// + /// Called when the reaper has noticed that all metric instances have expired and it has no more work to do. + /// + private void CleanupReaper() + { + Volatile.Write(ref _reaperActiveBool, ReaperInactive); + + // The reaper is now gone. However, as we do not use locking here it is possible that someone already + // added metric instances (which saw "oh reaper is still running") before we got here. Let's check - if + // there appear to be metric instances registered, we may need to start the reaper again. + if (HasAnyTrackedLifetimes()) + EnsureReaperActive(); + } + + private readonly Func _reaperFunc; + #endregion } \ No newline at end of file diff --git a/Tests.NetCore/MetricExpirationTests.cs b/Tests.NetCore/MetricExpirationTests.cs index 68cb3c56..ea4519f7 100644 --- a/Tests.NetCore/MetricExpirationTests.cs +++ b/Tests.NetCore/MetricExpirationTests.cs @@ -1,8 +1,7 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using System; +using System; using System.Collections.Generic; -using System.Threading; using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Prometheus.Tests { @@ -86,11 +85,11 @@ public void ManagedLifetimeMetric_ViaDifferentFactories_IsSameMetric() [TestMethod] public async Task ManagedLifetimeMetric_ExpiresOnlyAfterAllLeasesReleased() { - var handle = _expiringMetrics.CreateCounter(MetricName, "", Array.Empty()); + var handle = (ManagedLifetimeCounter)_expiringMetrics.CreateCounter(MetricName, "", Array.Empty()); // We break delays on demand to force any expiring-eligible metrics to expire. var delayer = new BreakableDelayer(); - ((ManagedLifetimeCounter)handle).Delayer = delayer; + handle.Delayer = delayer; // We detect expiration by the value having been reset when we try allocate the counter again. // We break 2 delays on every use, to ensure that the expiration logic has enough iterations to make up its mind. @@ -105,6 +104,7 @@ public async Task ManagedLifetimeMetric_ExpiresOnlyAfterAllLeasesReleased() await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and start expiring. + handle.ZeroAllKeepaliveTimestamps(); delayer.BreakAllDelays(); await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and finish expiring. delayer.BreakAllDelays(); @@ -116,6 +116,7 @@ public async Task ManagedLifetimeMetric_ExpiresOnlyAfterAllLeasesReleased() await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and start expiring. + handle.ZeroAllKeepaliveTimestamps(); delayer.BreakAllDelays(); await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and finish expiring. delayer.BreakAllDelays(); @@ -127,6 +128,7 @@ public async Task ManagedLifetimeMetric_ExpiresOnlyAfterAllLeasesReleased() await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and start expiring. + handle.ZeroAllKeepaliveTimestamps(); delayer.BreakAllDelays(); await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and finish expiring. delayer.BreakAllDelays(); @@ -160,15 +162,16 @@ public async Task ManagedLifetimeMetric_WithMultipleLabelingPaths_SharedSameLife var labelingFactory1 = _expiringMetrics.WithLabels(labels); var labelingFactory2 = _expiringMetrics.WithLabels(labels); - var factory1Handle = labelingFactory1.CreateCounter(MetricName, "", Array.Empty()); - var factory2Handle = labelingFactory2.CreateCounter(MetricName, "", Array.Empty()); - var rawHandle = _expiringMetrics.CreateCounter(MetricName, "", labelNames); + var factory1Handle = (LabelEnrichingManagedLifetimeCounter)labelingFactory1.CreateCounter(MetricName, "", Array.Empty()); + var factory2Handle = (LabelEnrichingManagedLifetimeCounter)labelingFactory2.CreateCounter(MetricName, "", Array.Empty()); + + var rawHandle = (ManagedLifetimeCounter)_expiringMetrics.CreateCounter(MetricName, "", labelNames); // We break delays on demand to force any expiring-eligible metrics to expire. var delayer = new BreakableDelayer(); - ((ManagedLifetimeCounter)((LabelEnrichingManagedLifetimeCounter)factory1Handle)._inner).Delayer = delayer; - ((ManagedLifetimeCounter)((LabelEnrichingManagedLifetimeCounter)factory2Handle)._inner).Delayer = delayer; - ((ManagedLifetimeCounter)rawHandle).Delayer = delayer; + ((ManagedLifetimeCounter)factory1Handle._inner).Delayer = delayer; + ((ManagedLifetimeCounter)factory2Handle._inner).Delayer = delayer; + rawHandle.Delayer = delayer; // We detect expiration by the value having been reset when we try allocate the counter again. // We break 2 delays on every use, to ensure that the expiration logic has enough iterations to make up its mind. @@ -183,6 +186,7 @@ public async Task ManagedLifetimeMetric_WithMultipleLabelingPaths_SharedSameLife await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and start expiring. + rawHandle.ZeroAllKeepaliveTimestamps(); delayer.BreakAllDelays(); await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and finish expiring. delayer.BreakAllDelays(); @@ -194,6 +198,7 @@ public async Task ManagedLifetimeMetric_WithMultipleLabelingPaths_SharedSameLife await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and start expiring. + rawHandle.ZeroAllKeepaliveTimestamps(); delayer.BreakAllDelays(); await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and finish expiring. delayer.BreakAllDelays(); @@ -208,6 +213,7 @@ public async Task ManagedLifetimeMetric_WithMultipleLabelingPaths_SharedSameLife await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and start expiring. + rawHandle.ZeroAllKeepaliveTimestamps(); delayer.BreakAllDelays(); await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and finish expiring. delayer.BreakAllDelays(); @@ -219,6 +225,7 @@ public async Task ManagedLifetimeMetric_WithMultipleLabelingPaths_SharedSameLife await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and start expiring. + rawHandle.ZeroAllKeepaliveTimestamps(); delayer.BreakAllDelays(); await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and finish expiring. delayer.BreakAllDelays(); @@ -230,6 +237,7 @@ public async Task ManagedLifetimeMetric_WithMultipleLabelingPaths_SharedSameLife await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and start expiring. + rawHandle.ZeroAllKeepaliveTimestamps(); delayer.BreakAllDelays(); await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and finish expiring. delayer.BreakAllDelays(); From 0b54049719f0e3b890b9af8937eddc65ff618798 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Sun, 26 Nov 2023 23:50:17 +0200 Subject: [PATCH 166/230] docs --- Prometheus/ManagedLifetimeMetricHandle.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Prometheus/ManagedLifetimeMetricHandle.cs b/Prometheus/ManagedLifetimeMetricHandle.cs index e282cece..8c383134 100644 --- a/Prometheus/ManagedLifetimeMetricHandle.cs +++ b/Prometheus/ManagedLifetimeMetricHandle.cs @@ -10,7 +10,6 @@ namespace Prometheus; /// Each metric handle maintains a reaper task that occasionally removes metrics that have expired. The reaper is started /// when the first lifetime-managed metric is created and terminates when the last lifetime-managed metric expires. /// This does mean that the metric handle may keep objects alive until expiration, even if the handle itself is no longer used. -/// TODO: Can we do something to reduce that risk? /// internal abstract class ManagedLifetimeMetricHandle : IManagedLifetimeMetricHandle where TChild : ChildBase, TMetricInterface From d852f75f19515f93fa1ab8cbdc335ff1241ec81c Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Mon, 27 Nov 2023 00:40:58 +0200 Subject: [PATCH 167/230] Clarify docs on when to use auto-leasing --- Prometheus/IManagedLifetimeMetricHandle.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Prometheus/IManagedLifetimeMetricHandle.cs b/Prometheus/IManagedLifetimeMetricHandle.cs index ebeccff7..4e8ad081 100644 --- a/Prometheus/IManagedLifetimeMetricHandle.cs +++ b/Prometheus/IManagedLifetimeMetricHandle.cs @@ -71,9 +71,8 @@ public interface IManagedLifetimeMetricHandle /// Returns a metric instance that automatically extends the lifetime of the timeseries whenever the value is changed. /// This is equivalent to taking a lease for every update to the value, and immediately releasing the lease. /// - /// This is useful if: - /// 1) the caller does not perform any long-running operations that would require keeping a lease for more than 1 update; - /// 2) or if the caller is lifetime-management-agnostic code that is not aware of the possibility to extend metric lifetime via leases. + /// This is useful if the caller is lifetime-management-agnostic code that is not aware of the possibility to extend metric lifetime via leases. + /// Do not use this if you can use explicit leases instead, as this is considerably less efficient. /// ICollector WithExtendLifetimeOnUse(); } From d7b598fc1da0a9751360f3ddab41d5f5313e3083 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Mon, 27 Nov 2023 10:39:26 +0200 Subject: [PATCH 168/230] Eliminate closure allocation in AutoLeasing pass-thorugh logic for reduced memory use --- .../MetricExpirationBenchmarks.cs | 1 + Prometheus/IManagedLifetimeMetricHandle.cs | 13 ++++ .../LabelEnrichingManagedLifetimeCounter.cs | 5 ++ .../LabelEnrichingManagedLifetimeGauge.cs | 5 ++ .../LabelEnrichingManagedLifetimeHistogram.cs | 5 ++ .../LabelEnrichingManagedLifetimeSummary.cs | 5 ++ Prometheus/ManagedLifetimeCounter.cs | 34 +++++++--- Prometheus/ManagedLifetimeGauge.cs | 64 +++++++++++++++++-- Prometheus/ManagedLifetimeHistogram.cs | 30 ++++++++- Prometheus/ManagedLifetimeMetricHandle.cs | 19 +++--- Prometheus/ManagedLifetimeSummary.cs | 16 ++++- 11 files changed, 172 insertions(+), 25 deletions(-) diff --git a/Benchmark.NetCore/MetricExpirationBenchmarks.cs b/Benchmark.NetCore/MetricExpirationBenchmarks.cs index 717084cb..423a84b5 100644 --- a/Benchmark.NetCore/MetricExpirationBenchmarks.cs +++ b/Benchmark.NetCore/MetricExpirationBenchmarks.cs @@ -7,6 +7,7 @@ namespace Benchmark.NetCore; /// Here we try to ensure that creating/using expiring metrics does not impose too heavy of a performance burden or create easily identifiable memory leaks. /// [MemoryDiagnoser] +//[EventPipeProfiler(BenchmarkDotNet.Diagnosers.EventPipeProfile.GcVerbose)] public class MetricExpirationBenchmarks { /// diff --git a/Prometheus/IManagedLifetimeMetricHandle.cs b/Prometheus/IManagedLifetimeMetricHandle.cs index 4e8ad081..5e2e68e4 100644 --- a/Prometheus/IManagedLifetimeMetricHandle.cs +++ b/Prometheus/IManagedLifetimeMetricHandle.cs @@ -31,6 +31,19 @@ public interface IManagedLifetimeMetricHandle /// void WithLease(Action action, params string[] labelValues); + /// + /// While executing an action, holds a lifetime-extending lease on the metric, scoped to a specific combination of label values. + /// Passes a given argument to the callback. + /// + /// The typical pattern is that the metric value is only modified when the caller is holding a lease on the metric. + /// Automatic removal of the metric will not occur until all leases on the metric are disposed and the expiration duration elapses. + /// + /// + /// Acquiring a new lease after the metric has been removed will re-publish the metric without preserving the old value. + /// Re-publishing may return a new instance of the metric (data collected via expired instances will not be published). + /// + void WithLease(Action action, TArg arg, params string[] labelValues); + /// /// While executing an action, holds a lifetime-extending lease on the metric, scoped to a specific combination of label values. /// diff --git a/Prometheus/LabelEnrichingManagedLifetimeCounter.cs b/Prometheus/LabelEnrichingManagedLifetimeCounter.cs index 149da1ac..e99060a8 100644 --- a/Prometheus/LabelEnrichingManagedLifetimeCounter.cs +++ b/Prometheus/LabelEnrichingManagedLifetimeCounter.cs @@ -27,6 +27,11 @@ public void WithLease(Action action, params string[] labelValues) _inner.WithLease(action, WithEnrichedLabelValues(labelValues)); } + public void WithLease(Action action, TArg arg, params string[] labelValues) + { + _inner.WithLease(action, arg, WithEnrichedLabelValues(labelValues)); + } + public TResult WithLease(Func func, params string[] labelValues) { return _inner.WithLease(func, WithEnrichedLabelValues(labelValues)); diff --git a/Prometheus/LabelEnrichingManagedLifetimeGauge.cs b/Prometheus/LabelEnrichingManagedLifetimeGauge.cs index f51e31cf..d4201edd 100644 --- a/Prometheus/LabelEnrichingManagedLifetimeGauge.cs +++ b/Prometheus/LabelEnrichingManagedLifetimeGauge.cs @@ -26,6 +26,11 @@ public void WithLease(Action action, params string[] labelValues) _inner.WithLease(action, WithEnrichedLabelValues(labelValues)); } + public void WithLease(Action action, TArg arg, params string[] labelValues) + { + _inner.WithLease(action, arg, WithEnrichedLabelValues(labelValues)); + } + public TResult WithLease(Func func, params string[] labelValues) { return _inner.WithLease(func, WithEnrichedLabelValues(labelValues)); diff --git a/Prometheus/LabelEnrichingManagedLifetimeHistogram.cs b/Prometheus/LabelEnrichingManagedLifetimeHistogram.cs index ecda1fe8..61d3119c 100644 --- a/Prometheus/LabelEnrichingManagedLifetimeHistogram.cs +++ b/Prometheus/LabelEnrichingManagedLifetimeHistogram.cs @@ -26,6 +26,11 @@ public void WithLease(Action action, params string[] labelValues) _inner.WithLease(action, WithEnrichedLabelValues(labelValues)); } + public void WithLease(Action action, TArg arg, params string[] labelValues) + { + _inner.WithLease(action, arg, WithEnrichedLabelValues(labelValues)); + } + public TResult WithLease(Func func, params string[] labelValues) { return _inner.WithLease(func, WithEnrichedLabelValues(labelValues)); diff --git a/Prometheus/LabelEnrichingManagedLifetimeSummary.cs b/Prometheus/LabelEnrichingManagedLifetimeSummary.cs index 39e7b6bc..a8af1413 100644 --- a/Prometheus/LabelEnrichingManagedLifetimeSummary.cs +++ b/Prometheus/LabelEnrichingManagedLifetimeSummary.cs @@ -26,6 +26,11 @@ public void WithLease(Action action, params string[] labelValues) _inner.WithLease(action, WithEnrichedLabelValues(labelValues)); } + public void WithLease(Action action, TArg arg, params string[] labelValues) + { + _inner.WithLease(action, arg, WithEnrichedLabelValues(labelValues)); + } + public TResult WithLease(Func func, params string[] labelValues) { return _inner.WithLease(func, WithEnrichedLabelValues(labelValues)); diff --git a/Prometheus/ManagedLifetimeCounter.cs b/Prometheus/ManagedLifetimeCounter.cs index 1ff2d3f4..327b8fe0 100644 --- a/Prometheus/ManagedLifetimeCounter.cs +++ b/Prometheus/ManagedLifetimeCounter.cs @@ -44,6 +44,12 @@ public ICounter WithLabels(params string[] labelValues) private sealed class AutoLeasingInstance : ICounter { + static AutoLeasingInstance() + { + _incCoreFunc = IncCore; + _incToCoreFunc = IncToCore; + } + public AutoLeasingInstance(IManagedLifetimeMetricHandle inner, string[] labelValues) { _inner = inner; @@ -55,24 +61,36 @@ public AutoLeasingInstance(IManagedLifetimeMetricHandle inner, string[ public double Value => throw new NotSupportedException("Read operations on a lifetime-extending-on-use expiring metric are not supported."); - public void Inc(double increment) + public void Inc(double increment) => Inc(increment, null); + public void Inc(Exemplar? exemplar) => Inc(increment: 1, exemplar: exemplar); + + public void Inc(double increment, Exemplar? exemplar) { - Inc(increment, null); + var args = new IncArgs(increment, exemplar); + _inner.WithLease(_incCoreFunc, args, _labelValues); } - public void Inc(Exemplar? exemplar) + private readonly struct IncArgs(double increment, Exemplar? exemplar) { - Inc(increment: 1, exemplar: exemplar); + public readonly double Increment = increment; + public readonly Exemplar? Exemplar = exemplar; } - public void Inc(double increment, Exemplar? exemplar) + private static void IncCore(IncArgs args, ICounter counter) => counter.Inc(args.Increment, args.Exemplar); + private static readonly Action _incCoreFunc; + + public void IncTo(double targetValue) { - _inner.WithLease(x => x.Inc(increment, exemplar), _labelValues); + var args = new IncToArgs(targetValue); + _inner.WithLease(_incToCoreFunc, args, _labelValues); } - public void IncTo(double targetValue) + private readonly struct IncToArgs(double targetValue) { - _inner.WithLease(x => x.IncTo(targetValue), _labelValues); + public readonly double TargetValue = targetValue; } + + private static void IncToCore(IncToArgs args, ICounter counter) => counter.IncTo(args.TargetValue); + private static readonly Action _incToCoreFunc; } } diff --git a/Prometheus/ManagedLifetimeGauge.cs b/Prometheus/ManagedLifetimeGauge.cs index 3b43e33e..a4642db8 100644 --- a/Prometheus/ManagedLifetimeGauge.cs +++ b/Prometheus/ManagedLifetimeGauge.cs @@ -44,6 +44,15 @@ public IGauge WithLabels(params string[] labelValues) private sealed class AutoLeasingInstance : IGauge { + static AutoLeasingInstance() + { + _incCoreFunc = IncCore; + _incToCoreFunc = IncToCore; + _setCoreFunc = SetCore; + _decCoreFunc = DecCore; + _decToCoreFunc = DecToCore; + } + public AutoLeasingInstance(IManagedLifetimeMetricHandle inner, string[] labelValues) { _inner = inner; @@ -57,27 +66,72 @@ public AutoLeasingInstance(IManagedLifetimeMetricHandle inner, string[] public void Inc(double increment = 1) { - _inner.WithLease(x => x.Inc(increment), _labelValues); + var args = new IncArgs(increment); + _inner.WithLease(_incCoreFunc, args, _labelValues); + } + + private readonly struct IncArgs(double increment) + { + public readonly double Increment = increment; } + private static void IncCore(IncArgs args, IGauge gauge) => gauge.Inc(args.Increment); + private static readonly Action _incCoreFunc; + public void Set(double val) { - _inner.WithLease(x => x.Set(val), _labelValues); + var args = new SetArgs(val); + _inner.WithLease(_setCoreFunc, args, _labelValues); + } + + private readonly struct SetArgs(double val) + { + public readonly double Val = val; } + private static void SetCore(SetArgs args, IGauge gauge) => gauge.Set(args.Val); + private static readonly Action _setCoreFunc; + public void Dec(double decrement = 1) { - _inner.WithLease(x => x.Dec(decrement), _labelValues); + var args = new DecArgs(decrement); + _inner.WithLease(_decCoreFunc, args, _labelValues); } + private readonly struct DecArgs(double decrement) + { + public readonly double Decrement = decrement; + } + + private static void DecCore(DecArgs args, IGauge gauge) => gauge.Dec(args.Decrement); + private static readonly Action _decCoreFunc; + public void IncTo(double targetValue) { - _inner.WithLease(x => x.IncTo(targetValue), _labelValues); + var args = new IncToArgs(targetValue); + _inner.WithLease(_incToCoreFunc, args, _labelValues); + } + + private readonly struct IncToArgs(double targetValue) + { + public readonly double TargetValue = targetValue; } + private static void IncToCore(IncToArgs args, IGauge gauge) => gauge.IncTo(args.TargetValue); + private static readonly Action _incToCoreFunc; + public void DecTo(double targetValue) { - _inner.WithLease(x => x.DecTo(targetValue), _labelValues); + var args = new DecToArgs(targetValue); + _inner.WithLease(_decToCoreFunc, args, _labelValues); } + + private readonly struct DecToArgs(double targetValue) + { + public readonly double TargetValue = targetValue; + } + + private static void DecToCore(DecToArgs args, IGauge gauge) => gauge.DecTo(args.TargetValue); + private static readonly Action _decToCoreFunc; } } diff --git a/Prometheus/ManagedLifetimeHistogram.cs b/Prometheus/ManagedLifetimeHistogram.cs index a2aea74b..678697f2 100644 --- a/Prometheus/ManagedLifetimeHistogram.cs +++ b/Prometheus/ManagedLifetimeHistogram.cs @@ -44,6 +44,12 @@ public IHistogram WithLabels(params string[] labelValues) private sealed class AutoLeasingInstance : IHistogram { + static AutoLeasingInstance() + { + _observeValCountCoreFunc = ObserveValCountCore; + _observeValExemplarCoreFunc = ObserveValExemplarCore; + } + public AutoLeasingInstance(IManagedLifetimeMetricHandle inner, string[] labelValues) { _inner = inner; @@ -58,14 +64,34 @@ public AutoLeasingInstance(IManagedLifetimeMetricHandle inner, strin public void Observe(double val, long count) { - _inner.WithLease(x => x.Observe(val, count), _labelValues); + var args = new ObserveValCountArgs(val, count); + _inner.WithLease(_observeValCountCoreFunc, args, _labelValues); + } + + private readonly struct ObserveValCountArgs(double val, long count) + { + public readonly double Val = val; + public readonly long Count = count; } + private static void ObserveValCountCore(ObserveValCountArgs args, IHistogram histogram) => histogram.Observe(args.Val, args.Count); + private static readonly Action _observeValCountCoreFunc; + public void Observe(double val, Exemplar? exemplar) { - _inner.WithLease(x => x.Observe(val, exemplar), _labelValues); + var args = new ObserveValExemplarArgs(val, exemplar); + _inner.WithLease(_observeValExemplarCoreFunc, args, _labelValues); } + private readonly struct ObserveValExemplarArgs(double val, Exemplar? exemplar) + { + public readonly double Val = val; + public readonly Exemplar? Exemplar = exemplar; + } + + private static void ObserveValExemplarCore(ObserveValExemplarArgs args, IHistogram histogram) => histogram.Observe(args.Val, args.Exemplar); + private static readonly Action _observeValExemplarCoreFunc; + public void Observe(double val) { Observe(val, null); diff --git a/Prometheus/ManagedLifetimeMetricHandle.cs b/Prometheus/ManagedLifetimeMetricHandle.cs index 8c383134..a9bd6dec 100644 --- a/Prometheus/ManagedLifetimeMetricHandle.cs +++ b/Prometheus/ManagedLifetimeMetricHandle.cs @@ -37,16 +37,17 @@ public IDisposable AcquireLease(out TMetricInterface metric, params string[] lab public void WithLease(Action action, params string[] labelValues) { var child = _metric.WithLabels(labelValues); - var lease = TakeRefLease(child); + using var lease = TakeRefLease(child); - try - { - action(child); - } - finally - { - lease.Dispose(); - } + action(child); + } + + public void WithLease(Action action, TArg arg, params string[] labelValues) + { + var child = _metric.WithLabels(labelValues); + using var lease = TakeRefLease(child); + + action(arg, child); } public async Task WithLeaseAsync(Func action, params string[] labelValues) diff --git a/Prometheus/ManagedLifetimeSummary.cs b/Prometheus/ManagedLifetimeSummary.cs index 39c1b1d8..5260a5a0 100644 --- a/Prometheus/ManagedLifetimeSummary.cs +++ b/Prometheus/ManagedLifetimeSummary.cs @@ -44,6 +44,11 @@ public ISummary WithLabels(params string[] labelValues) private sealed class AutoLeasingInstance : ISummary { + static AutoLeasingInstance() + { + _observeCoreFunc = ObserveCore; + } + public AutoLeasingInstance(IManagedLifetimeMetricHandle inner, string[] labelValues) { _inner = inner; @@ -55,7 +60,16 @@ public AutoLeasingInstance(IManagedLifetimeMetricHandle inner, string[ public void Observe(double val) { - _inner.WithLease(x => x.Observe(val), _labelValues); + var args = new ObserveArgs(val); + _inner.WithLease(_observeCoreFunc, args, _labelValues); } + + private readonly struct ObserveArgs(double val) + { + public readonly double Val = val; + } + + private static void ObserveCore(ObserveArgs args, ISummary summary) => summary.Observe(args.Val); + private static readonly Action _observeCoreFunc; } } From 62350d371bdd8806a7bf2e9721991983cb767493 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Mon, 27 Nov 2023 11:16:11 +0200 Subject: [PATCH 169/230] Add TakeRefLease() for manually managed stack-only leases --- .../MetricExpirationBenchmarks.cs | 31 ++++++++++++++ Prometheus/ChildLifetimeInfo.cs | 27 +++++++++++++ Prometheus/IManagedLifetimeMetricHandle.cs | 13 ++++++ Prometheus/INotifyLeaseEnded.cs | 6 +++ .../LabelEnrichingManagedLifetimeCounter.cs | 5 +++ .../LabelEnrichingManagedLifetimeGauge.cs | 5 +++ .../LabelEnrichingManagedLifetimeHistogram.cs | 5 +++ .../LabelEnrichingManagedLifetimeSummary.cs | 5 +++ Prometheus/ManagedLifetimeMetricHandle.cs | 40 ++++++++----------- Prometheus/RefLease.cs | 21 ++++++++++ 10 files changed, 135 insertions(+), 23 deletions(-) create mode 100644 Prometheus/ChildLifetimeInfo.cs create mode 100644 Prometheus/INotifyLeaseEnded.cs create mode 100644 Prometheus/RefLease.cs diff --git a/Benchmark.NetCore/MetricExpirationBenchmarks.cs b/Benchmark.NetCore/MetricExpirationBenchmarks.cs index 423a84b5..abc0ade1 100644 --- a/Benchmark.NetCore/MetricExpirationBenchmarks.cs +++ b/Benchmark.NetCore/MetricExpirationBenchmarks.cs @@ -137,6 +137,37 @@ public void CreateAndUse_ManualLease_WithDuplicates() } } + [Benchmark] + public void CreateAndUse_ManualRefLease() + { + for (var i = 0; i < _metricCount; i++) + { + var counter = _factory.CreateCounter(_metricNames[i], _help, _labels); + + for (var repeat = 0; repeat < RepeatCount; repeat++) + { + using var lease = counter.AcquireRefLease(out var instance, _labels); + instance.Inc(); + } + } + } + + [Benchmark] + public void CreateAndUse_ManualRefLease_WithDuplicates() + { + for (var dupe = 0; dupe < _duplicateCount; dupe++) + for (var i = 0; i < _metricCount; i++) + { + var counter = _factory.CreateCounter(_metricNames[i], _help, _labels); + + for (var repeat = 0; repeat < RepeatCount; repeat++) + { + using var lease = counter.AcquireRefLease(out var instance, _labels); + instance.Inc(); + } + } + } + private static void IncrementCounter(ICounter counter) { counter.Inc(); diff --git a/Prometheus/ChildLifetimeInfo.cs b/Prometheus/ChildLifetimeInfo.cs new file mode 100644 index 00000000..759da018 --- /dev/null +++ b/Prometheus/ChildLifetimeInfo.cs @@ -0,0 +1,27 @@ +namespace Prometheus; + +/// +/// Describes a lifetime of a lifetime-managed metric instance. +/// +/// +/// Contents modified via atomic operations, not guarded by locks. +/// +internal sealed class ChildLifetimeInfo +{ + /// + /// Number of active leases. Nonzero value here indicates the lifetime extends forever. + /// + public int LeaseCount; + + /// + /// When the last lifetime related activity was performed. Expiration timer starts counting from here. + /// This is refreshed whenever a lease is released (a kept lease is a forever-keepalive, so we only care about releasing). + /// + public long KeepaliveTimestamp; + + /// + /// The lifetime has been ended, potentially while a lease was active. The next time a lease ends, + /// it will have to re-register the lifetime instead of just extending the existing one. + /// + public bool Ended; +} \ No newline at end of file diff --git a/Prometheus/IManagedLifetimeMetricHandle.cs b/Prometheus/IManagedLifetimeMetricHandle.cs index 5e2e68e4..20418a8c 100644 --- a/Prometheus/IManagedLifetimeMetricHandle.cs +++ b/Prometheus/IManagedLifetimeMetricHandle.cs @@ -19,6 +19,19 @@ public interface IManagedLifetimeMetricHandle /// IDisposable AcquireLease(out TMetricInterface metric, params string[] labelValues); + /// + /// Takes a lifetime-extending lease on the metric, scoped to a specific combination of label values. + /// The lease is returned as a stack-only struct, which is faster than the IDisposable version. + /// + /// The typical pattern is that the metric value is only modified when the caller is holding a lease on the metric. + /// Automatic removal of the metric will not occur until all leases on the metric are disposed and the expiration duration elapses. + /// + /// + /// Acquiring a new lease after the metric has been removed will re-publish the metric without preserving the old value. + /// Re-publishing may return a new instance of the metric (data collected via expired instances will not be published). + /// + RefLease AcquireRefLease(out TMetricInterface metric, params string[] labelValues); + /// /// While executing an action, holds a lifetime-extending lease on the metric, scoped to a specific combination of label values. /// diff --git a/Prometheus/INotifyLeaseEnded.cs b/Prometheus/INotifyLeaseEnded.cs new file mode 100644 index 00000000..d402a518 --- /dev/null +++ b/Prometheus/INotifyLeaseEnded.cs @@ -0,0 +1,6 @@ +namespace Prometheus; + +internal interface INotifyLeaseEnded +{ + void OnLeaseEnded(object child, ChildLifetimeInfo lifetime); +} diff --git a/Prometheus/LabelEnrichingManagedLifetimeCounter.cs b/Prometheus/LabelEnrichingManagedLifetimeCounter.cs index e99060a8..219026ab 100644 --- a/Prometheus/LabelEnrichingManagedLifetimeCounter.cs +++ b/Prometheus/LabelEnrichingManagedLifetimeCounter.cs @@ -17,6 +17,11 @@ public IDisposable AcquireLease(out ICounter metric, params string[] labelValues return _inner.AcquireLease(out metric, WithEnrichedLabelValues(labelValues)); } + public RefLease AcquireRefLease(out ICounter metric, params string[] labelValues) + { + return _inner.AcquireRefLease(out metric, WithEnrichedLabelValues(labelValues)); + } + public ICollector WithExtendLifetimeOnUse() { return new LabelEnrichingAutoLeasingMetric(_inner.WithExtendLifetimeOnUse(), _enrichWithLabelValues); diff --git a/Prometheus/LabelEnrichingManagedLifetimeGauge.cs b/Prometheus/LabelEnrichingManagedLifetimeGauge.cs index d4201edd..edd55da5 100644 --- a/Prometheus/LabelEnrichingManagedLifetimeGauge.cs +++ b/Prometheus/LabelEnrichingManagedLifetimeGauge.cs @@ -16,6 +16,11 @@ public IDisposable AcquireLease(out IGauge metric, params string[] labelValues) return _inner.AcquireLease(out metric, WithEnrichedLabelValues(labelValues)); } + public RefLease AcquireRefLease(out IGauge metric, params string[] labelValues) + { + return _inner.AcquireRefLease(out metric, WithEnrichedLabelValues(labelValues)); + } + public ICollector WithExtendLifetimeOnUse() { return new LabelEnrichingAutoLeasingMetric(_inner.WithExtendLifetimeOnUse(), _enrichWithLabelValues); diff --git a/Prometheus/LabelEnrichingManagedLifetimeHistogram.cs b/Prometheus/LabelEnrichingManagedLifetimeHistogram.cs index 61d3119c..5e69c790 100644 --- a/Prometheus/LabelEnrichingManagedLifetimeHistogram.cs +++ b/Prometheus/LabelEnrichingManagedLifetimeHistogram.cs @@ -16,6 +16,11 @@ public IDisposable AcquireLease(out IHistogram metric, params string[] labelValu return _inner.AcquireLease(out metric, WithEnrichedLabelValues(labelValues)); } + public RefLease AcquireRefLease(out IHistogram metric, params string[] labelValues) + { + return _inner.AcquireRefLease(out metric, WithEnrichedLabelValues(labelValues)); + } + public ICollector WithExtendLifetimeOnUse() { return new LabelEnrichingAutoLeasingMetric(_inner.WithExtendLifetimeOnUse(), _enrichWithLabelValues); diff --git a/Prometheus/LabelEnrichingManagedLifetimeSummary.cs b/Prometheus/LabelEnrichingManagedLifetimeSummary.cs index a8af1413..b5d5d094 100644 --- a/Prometheus/LabelEnrichingManagedLifetimeSummary.cs +++ b/Prometheus/LabelEnrichingManagedLifetimeSummary.cs @@ -16,6 +16,11 @@ public IDisposable AcquireLease(out ISummary metric, params string[] labelValues return _inner.AcquireLease(out metric, WithEnrichedLabelValues(labelValues)); } + public RefLease AcquireRefLease(out ISummary metric, params string[] labelValues) + { + return _inner.AcquireRefLease(out metric, WithEnrichedLabelValues(labelValues)); + } + public ICollector WithExtendLifetimeOnUse() { return new LabelEnrichingAutoLeasingMetric(_inner.WithExtendLifetimeOnUse(), _enrichWithLabelValues); diff --git a/Prometheus/ManagedLifetimeMetricHandle.cs b/Prometheus/ManagedLifetimeMetricHandle.cs index a9bd6dec..4266cb74 100644 --- a/Prometheus/ManagedLifetimeMetricHandle.cs +++ b/Prometheus/ManagedLifetimeMetricHandle.cs @@ -11,7 +11,8 @@ namespace Prometheus; /// when the first lifetime-managed metric is created and terminates when the last lifetime-managed metric expires. /// This does mean that the metric handle may keep objects alive until expiration, even if the handle itself is no longer used. /// -internal abstract class ManagedLifetimeMetricHandle : IManagedLifetimeMetricHandle +internal abstract class ManagedLifetimeMetricHandle + : IManagedLifetimeMetricHandle, INotifyLeaseEnded where TChild : ChildBase, TMetricInterface where TMetricInterface : ICollectorChild { @@ -34,6 +35,14 @@ public IDisposable AcquireLease(out TMetricInterface metric, params string[] lab return TakeLease(child); } + public RefLease AcquireRefLease(out TMetricInterface metric, params string[] labelValues) + { + var child = _metric.WithLabels(labelValues); + metric = child; + + return TakeRefLease(child); + } + public void WithLease(Action action, params string[] labelValues) { var child = _metric.WithLabels(labelValues); @@ -76,22 +85,7 @@ public async Task WithLeaseAsync(Func _lifetimes = new(); + private readonly Dictionary _lifetimes = new(); // Guards the collection but not the contents. private readonly ReaderWriterLockSlim _lifetimesLock = new(); @@ -144,7 +138,7 @@ private RefLease TakeRefLease(TChild child) return new RefLease(this, child, lifetime); } - private LifetimeInfo GetOrCreateLifetimeAndIncrementLeaseCount(TChild child) + private ChildLifetimeInfo GetOrCreateLifetimeAndIncrementLeaseCount(TChild child) { _lifetimesLock.EnterReadLock(); @@ -178,7 +172,7 @@ private LifetimeInfo GetOrCreateLifetimeAndIncrementLeaseCount(TChild child) } // Did not get lucky. Make a new one. - lifetime = new LifetimeInfo + lifetime = new ChildLifetimeInfo { LeaseCount = 1 }; @@ -192,7 +186,7 @@ private LifetimeInfo GetOrCreateLifetimeAndIncrementLeaseCount(TChild child) } } - private void OnLeaseEnded(TChild child, LifetimeInfo lifetime) + internal void OnLeaseEnded(TChild child, ChildLifetimeInfo lifetime) { // Update keepalive timestamp before anything else, to avoid racing. Volatile.Write(ref lifetime.KeepaliveTimestamp, LowGranularityTimeSource.GetStopwatchTimestamp()); @@ -211,12 +205,12 @@ private void OnLeaseEnded(TChild child, LifetimeInfo lifetime) Interlocked.Decrement(ref lifetime.LeaseCount); } - private sealed class Lease(ManagedLifetimeMetricHandle parent, TChild child, LifetimeInfo lifetime) : IDisposable + void INotifyLeaseEnded.OnLeaseEnded(object child, ChildLifetimeInfo lifetime) { - public void Dispose() => parent.OnLeaseEnded(child, lifetime); + OnLeaseEnded((TChild)child, lifetime); } - private readonly ref struct RefLease(ManagedLifetimeMetricHandle parent, TChild child, LifetimeInfo lifetime) + private sealed class Lease(ManagedLifetimeMetricHandle parent, TChild child, ChildLifetimeInfo lifetime) : IDisposable { public void Dispose() => parent.OnLeaseEnded(child, lifetime); } diff --git a/Prometheus/RefLease.cs b/Prometheus/RefLease.cs new file mode 100644 index 00000000..17feabf1 --- /dev/null +++ b/Prometheus/RefLease.cs @@ -0,0 +1,21 @@ +namespace Prometheus; + +/// +/// A stack-only struct for holding a lease on a lifetime-managed metric. +/// Helps avoid allocation when you need to take a lease in a synchronous context where stack-only structs are allowed. +/// +public readonly ref struct RefLease +{ + internal RefLease(INotifyLeaseEnded notifyLeaseEnded, object child, ChildLifetimeInfo lifetime) + { + _notifyLeaseEnded = notifyLeaseEnded; + _child = child; + _lifetime = lifetime; + } + + private readonly INotifyLeaseEnded _notifyLeaseEnded; + private readonly object _child; + private readonly ChildLifetimeInfo _lifetime; + + public void Dispose() => _notifyLeaseEnded.OnLeaseEnded(_child, _lifetime); +} \ No newline at end of file From 9a52ed71d3b960ca92607691ac5fb6811f3905d2 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Tue, 28 Nov 2023 14:46:27 +0200 Subject: [PATCH 170/230] Fix bug: GetAllLabelValues() was processing buffer beyond used item count --- Prometheus/Collector.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Prometheus/Collector.cs b/Prometheus/Collector.cs index d57d5235..c23f9046 100644 --- a/Prometheus/Collector.cs +++ b/Prometheus/Collector.cs @@ -276,8 +276,10 @@ public IEnumerable GetAllLabelValues() _childrenLock.ExitReadLock(); } - foreach (var labels in buffer) + for (var i = 0; i < childCount; i++) { + var labels = buffer[i]; + if (labels.Length == 0) continue; // We do not return the "unlabelled" label set. From 593b1083271540ff8407e9b2a0e70d4d0b7aabf6 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Tue, 28 Nov 2023 15:01:17 +0200 Subject: [PATCH 171/230] Add debug info if lifetime tests fail Only reproduces in build pipeline, so let's look closer --- Prometheus/ChildBase.cs | 6 ++++ Prometheus/ChildLifetimeInfo.cs | 15 ++++++++- Prometheus/Collector.cs | 6 ++++ Prometheus/LabelSequence.cs | 6 ++++ Prometheus/ManagedLifetimeMetricHandle.cs | 31 ++++++++++++++---- Prometheus/PlatformCompatibilityHelpers.cs | 10 ++++++ Tests.NetCore/MetricExpirationTests.cs | 38 +++------------------- 7 files changed, 71 insertions(+), 41 deletions(-) create mode 100644 Prometheus/PlatformCompatibilityHelpers.cs diff --git a/Prometheus/ChildBase.cs b/Prometheus/ChildBase.cs index a34a36b3..4cf98db8 100644 --- a/Prometheus/ChildBase.cs +++ b/Prometheus/ChildBase.cs @@ -185,4 +185,10 @@ static ChildBase() ExemplarsRecorded = Metrics.CreateCounter("prometheus_net_exemplars_recorded_total", "Number of exemplars that were accepted into in-memory storage in the prometheus-net SDK."); }); } + + public override string ToString() + { + // Just for debugging. + return $"{Parent.Name}{{{FlattenedLabels}}}"; + } } \ No newline at end of file diff --git a/Prometheus/ChildLifetimeInfo.cs b/Prometheus/ChildLifetimeInfo.cs index 759da018..fa0e4e0e 100644 --- a/Prometheus/ChildLifetimeInfo.cs +++ b/Prometheus/ChildLifetimeInfo.cs @@ -1,4 +1,6 @@ -namespace Prometheus; +using System.Diagnostics; + +namespace Prometheus; /// /// Describes a lifetime of a lifetime-managed metric instance. @@ -24,4 +26,15 @@ internal sealed class ChildLifetimeInfo /// it will have to re-register the lifetime instead of just extending the existing one. /// public bool Ended; + + public override string ToString() + { + var leaseCount = Volatile.Read(ref LeaseCount); + var keepaliveTimestamp = Volatile.Read(ref KeepaliveTimestamp); + var ended = Volatile.Read(ref Ended); + + var age = PlatformCompatibilityHelpers.StopwatchGetElapsedTime(keepaliveTimestamp, Stopwatch.GetTimestamp()); + + return $"LeaseCount: {leaseCount}, KeepaliveTimestamp: {keepaliveTimestamp}, Ended: {ended}, Age: {age.TotalSeconds:F3} seconds"; + } } \ No newline at end of file diff --git a/Prometheus/Collector.cs b/Prometheus/Collector.cs index c23f9046..d81b9486 100644 --- a/Prometheus/Collector.cs +++ b/Prometheus/Collector.cs @@ -155,6 +155,12 @@ internal static void ValidateLabelName(string labelName) if (ReservedLabelRegex.IsMatch(labelName)) throw new ArgumentException($"Label name '{labelName}' is not valid - labels starting with double underscore are reserved!"); } + + public override string ToString() + { + // Just for debugging. + return $"{Name}{{{FlattenedLabelNames}}}"; + } } /// diff --git a/Prometheus/LabelSequence.cs b/Prometheus/LabelSequence.cs index 89a2a315..d3fc5538 100644 --- a/Prometheus/LabelSequence.cs +++ b/Prometheus/LabelSequence.cs @@ -236,4 +236,10 @@ public IDictionary ToDictionary() return result; } + + public override string ToString() + { + // Just for debugging. + return $"({Length})" + string.Join("; ", ToDictionary().Select(pair => $"{pair.Key} = {pair.Value}")); + } } diff --git a/Prometheus/ManagedLifetimeMetricHandle.cs b/Prometheus/ManagedLifetimeMetricHandle.cs index 4266cb74..738e018c 100644 --- a/Prometheus/ManagedLifetimeMetricHandle.cs +++ b/Prometheus/ManagedLifetimeMetricHandle.cs @@ -1,4 +1,5 @@ using System.Buffers; +using System.ComponentModel; using System.Diagnostics; namespace Prometheus; @@ -122,6 +123,28 @@ internal void ZeroAllKeepaliveTimestamps() } } + /// + /// For anomaly analysis during testing only. + /// + internal void DebugDumpLifetimes() + { + _lifetimesLock.EnterReadLock(); + + try + { + Console.WriteLine($"Dumping {_lifetimes.Count} lifetimes of {_metric}. Reaper status: {Volatile.Read(ref _reaperActiveBool)}."); + + foreach (var pair in _lifetimes) + { + Console.WriteLine($"{pair.Key} -> {pair.Value}"); + } + } + finally + { + _lifetimesLock.ExitReadLock(); + } + } + private IDisposable TakeLease(TChild child) { var lifetime = GetOrCreateLifetimeAndIncrementLeaseCount(child); @@ -239,10 +262,6 @@ private void EnsureReaperActive() _ = Task.Run(_reaperFunc); } - // Reimplementation of Stopwatch.GetElapsedTime (only available on .NET 7 or newer). - private static TimeSpan GetElapsedTime(long start, long end) - => new((long)((end - start) * ((double)10_000_000 / Stopwatch.Frequency))); - private async Task Reaper() { while (true) @@ -267,7 +286,7 @@ private async Task Reaper() if (Volatile.Read(ref pair.Value.LeaseCount) != 0) continue; // Not expired. - if (GetElapsedTime(Volatile.Read(ref pair.Value.KeepaliveTimestamp), now) < _expiresAfter) + if (PlatformCompatibilityHelpers.StopwatchGetElapsedTime(Volatile.Read(ref pair.Value.KeepaliveTimestamp), now) < _expiresAfter) continue; // Not expired. // No leases and keepalive has expired - it is an expired instance! @@ -298,7 +317,7 @@ private async Task Reaper() if (Volatile.Read(ref lifetime.LeaseCount) != 0) continue; // Not expired. - if (GetElapsedTime(Volatile.Read(ref lifetime.KeepaliveTimestamp), now) < _expiresAfter) + if (PlatformCompatibilityHelpers.StopwatchGetElapsedTime(Volatile.Read(ref lifetime.KeepaliveTimestamp), now) < _expiresAfter) continue; // Not expired. // No leases and keepalive has expired - it is an expired instance! diff --git a/Prometheus/PlatformCompatibilityHelpers.cs b/Prometheus/PlatformCompatibilityHelpers.cs new file mode 100644 index 00000000..fe35b88b --- /dev/null +++ b/Prometheus/PlatformCompatibilityHelpers.cs @@ -0,0 +1,10 @@ +using System.Diagnostics; + +namespace Prometheus; + +internal class PlatformCompatibilityHelpers +{ + // Reimplementation of Stopwatch.GetElapsedTime (only available on .NET 7 or newer). + public static TimeSpan StopwatchGetElapsedTime(long start, long end) + => new((long)((end - start) * ((double)10_000_000 / Stopwatch.Frequency))); +} diff --git a/Tests.NetCore/MetricExpirationTests.cs b/Tests.NetCore/MetricExpirationTests.cs index ea4519f7..c4176a0b 100644 --- a/Tests.NetCore/MetricExpirationTests.cs +++ b/Tests.NetCore/MetricExpirationTests.cs @@ -92,7 +92,6 @@ public async Task ManagedLifetimeMetric_ExpiresOnlyAfterAllLeasesReleased() handle.Delayer = delayer; // We detect expiration by the value having been reset when we try allocate the counter again. - // We break 2 delays on every use, to ensure that the expiration logic has enough iterations to make up its mind. using (handle.AcquireLease(out var instance1)) { @@ -102,37 +101,27 @@ public async Task ManagedLifetimeMetric_ExpiresOnlyAfterAllLeasesReleased() { instance2.Inc(); - await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and start expiring. - handle.ZeroAllKeepaliveTimestamps(); delayer.BreakAllDelays(); await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and finish expiring. - delayer.BreakAllDelays(); - await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and finish expiring. // 2 leases remain - should not have expired yet. Check with a fresh copy from the root registry. Assert.AreEqual(2, _metrics.CreateCounter(MetricName, "").Value); } - await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and start expiring. - handle.ZeroAllKeepaliveTimestamps(); delayer.BreakAllDelays(); await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and finish expiring. - delayer.BreakAllDelays(); - await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and finish expiring. // 1 lease remains - should not have expired yet. Check with a fresh copy from the root registry. Assert.AreEqual(2, _metrics.CreateCounter(MetricName, "").Value); } - await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and start expiring. - handle.ZeroAllKeepaliveTimestamps(); delayer.BreakAllDelays(); await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and finish expiring. - delayer.BreakAllDelays(); - await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and finish expiring. + + handle.DebugDumpLifetimes(); // 0 leases remains - should have expired. Check with a fresh copy from the root registry. Assert.AreEqual(0, _metrics.CreateCounter(MetricName, "").Value); @@ -174,7 +163,6 @@ public async Task ManagedLifetimeMetric_WithMultipleLabelingPaths_SharedSameLife rawHandle.Delayer = delayer; // We detect expiration by the value having been reset when we try allocate the counter again. - // We break 2 delays on every use, to ensure that the expiration logic has enough iterations to make up its mind. using (factory1Handle.AcquireLease(out var instance1)) { @@ -184,25 +172,17 @@ public async Task ManagedLifetimeMetric_WithMultipleLabelingPaths_SharedSameLife { instance2.Inc(); - await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and start expiring. - rawHandle.ZeroAllKeepaliveTimestamps(); delayer.BreakAllDelays(); await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and finish expiring. - delayer.BreakAllDelays(); - await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and finish expiring. // 2 leases remain - should not have expired yet. Check with a fresh copy from the root registry. Assert.AreEqual(2, _metrics.CreateCounter(MetricName, "", labelNames).WithLabels(labelValues).Value); } - await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and start expiring. - rawHandle.ZeroAllKeepaliveTimestamps(); delayer.BreakAllDelays(); await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and finish expiring. - delayer.BreakAllDelays(); - await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and finish expiring. // 1 lease remains - should not have expired yet. Check with a fresh copy from the root registry. Assert.AreEqual(2, _metrics.CreateCounter(MetricName, "", labelNames).WithLabels(labelValues).Value); @@ -211,37 +191,27 @@ public async Task ManagedLifetimeMetric_WithMultipleLabelingPaths_SharedSameLife { instance3.Inc(); - await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and start expiring. - rawHandle.ZeroAllKeepaliveTimestamps(); delayer.BreakAllDelays(); await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and finish expiring. - delayer.BreakAllDelays(); - await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and finish expiring. // 2 leases remain - should not have expired yet. Check with a fresh copy from the root registry. Assert.AreEqual(3, _metrics.CreateCounter(MetricName, "", labelNames).WithLabels(labelValues).Value); } - await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and start expiring. - rawHandle.ZeroAllKeepaliveTimestamps(); delayer.BreakAllDelays(); await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and finish expiring. - delayer.BreakAllDelays(); - await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and finish expiring. // 1 lease remains - should not have expired yet. Check with a fresh copy from the root registry. Assert.AreEqual(3, _metrics.CreateCounter(MetricName, "", labelNames).WithLabels(labelValues).Value); } - await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and start expiring. - rawHandle.ZeroAllKeepaliveTimestamps(); delayer.BreakAllDelays(); await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and finish expiring. - delayer.BreakAllDelays(); - await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and finish expiring. + + rawHandle.DebugDumpLifetimes(); // 0 leases remains - should have expired. Check with a fresh copy from the root registry. Assert.AreEqual(0, _metrics.CreateCounter(MetricName, "", labelNames).WithLabels(labelValues).Value); From 83ea9ebe8019f41765478ba2ef945dd7a37e4942 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Tue, 28 Nov 2023 15:16:55 +0200 Subject: [PATCH 172/230] SetAllKeepaliveTimestampsToDistantPast --- Prometheus/ManagedLifetimeMetricHandle.cs | 13 ++++++++----- Prometheus/PlatformCompatibilityHelpers.cs | 3 +++ Tests.NetCore/MetricExpirationTests.cs | 16 ++++++++-------- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/Prometheus/ManagedLifetimeMetricHandle.cs b/Prometheus/ManagedLifetimeMetricHandle.cs index 738e018c..6b49cb24 100644 --- a/Prometheus/ManagedLifetimeMetricHandle.cs +++ b/Prometheus/ManagedLifetimeMetricHandle.cs @@ -1,6 +1,4 @@ using System.Buffers; -using System.ComponentModel; -using System.Diagnostics; namespace Prometheus; @@ -106,16 +104,21 @@ private bool HasAnyTrackedLifetimes() } /// - /// For testing only. Sets all keepalive timestamps to 0, which will cause all lifetimes to expire (if they have no leases). + /// For testing only. Sets all keepalive timestamps to a time in the disstant past, + /// which will cause all lifetimes to expire (if they have no leases). /// - internal void ZeroAllKeepaliveTimestamps() + internal void SetAllKeepaliveTimestampsToDistantPast() { + // We cannot just zero this because zero is the machine start timestamp, so zero is not necessarily + // far in the past (especially if the machine is a build agent that just started up). 1 year negative should work, though. + var distantPast = -PlatformCompatibilityHelpers.ElapsedToTimeStopwatchTicks(TimeSpan.FromDays(365)); + _lifetimesLock.EnterReadLock(); try { foreach (var lifetime in _lifetimes.Values) - Volatile.Write(ref lifetime.KeepaliveTimestamp, 0L); + Volatile.Write(ref lifetime.KeepaliveTimestamp, distantPast); } finally { diff --git a/Prometheus/PlatformCompatibilityHelpers.cs b/Prometheus/PlatformCompatibilityHelpers.cs index fe35b88b..dd7a3220 100644 --- a/Prometheus/PlatformCompatibilityHelpers.cs +++ b/Prometheus/PlatformCompatibilityHelpers.cs @@ -7,4 +7,7 @@ internal class PlatformCompatibilityHelpers // Reimplementation of Stopwatch.GetElapsedTime (only available on .NET 7 or newer). public static TimeSpan StopwatchGetElapsedTime(long start, long end) => new((long)((end - start) * ((double)10_000_000 / Stopwatch.Frequency))); + + public static long ElapsedToTimeStopwatchTicks(TimeSpan elapsedTime) + => (long)(elapsedTime.TotalSeconds * (Stopwatch.Frequency / (double)10_000_000)); } diff --git a/Tests.NetCore/MetricExpirationTests.cs b/Tests.NetCore/MetricExpirationTests.cs index c4176a0b..86cec7b2 100644 --- a/Tests.NetCore/MetricExpirationTests.cs +++ b/Tests.NetCore/MetricExpirationTests.cs @@ -101,7 +101,7 @@ public async Task ManagedLifetimeMetric_ExpiresOnlyAfterAllLeasesReleased() { instance2.Inc(); - handle.ZeroAllKeepaliveTimestamps(); + handle.SetAllKeepaliveTimestampsToDistantPast(); delayer.BreakAllDelays(); await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and finish expiring. @@ -109,7 +109,7 @@ public async Task ManagedLifetimeMetric_ExpiresOnlyAfterAllLeasesReleased() Assert.AreEqual(2, _metrics.CreateCounter(MetricName, "").Value); } - handle.ZeroAllKeepaliveTimestamps(); + handle.SetAllKeepaliveTimestampsToDistantPast(); delayer.BreakAllDelays(); await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and finish expiring. @@ -117,7 +117,7 @@ public async Task ManagedLifetimeMetric_ExpiresOnlyAfterAllLeasesReleased() Assert.AreEqual(2, _metrics.CreateCounter(MetricName, "").Value); } - handle.ZeroAllKeepaliveTimestamps(); + handle.SetAllKeepaliveTimestampsToDistantPast(); delayer.BreakAllDelays(); await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and finish expiring. @@ -172,7 +172,7 @@ public async Task ManagedLifetimeMetric_WithMultipleLabelingPaths_SharedSameLife { instance2.Inc(); - rawHandle.ZeroAllKeepaliveTimestamps(); + rawHandle.SetAllKeepaliveTimestampsToDistantPast(); delayer.BreakAllDelays(); await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and finish expiring. @@ -180,7 +180,7 @@ public async Task ManagedLifetimeMetric_WithMultipleLabelingPaths_SharedSameLife Assert.AreEqual(2, _metrics.CreateCounter(MetricName, "", labelNames).WithLabels(labelValues).Value); } - rawHandle.ZeroAllKeepaliveTimestamps(); + rawHandle.SetAllKeepaliveTimestampsToDistantPast(); delayer.BreakAllDelays(); await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and finish expiring. @@ -191,7 +191,7 @@ public async Task ManagedLifetimeMetric_WithMultipleLabelingPaths_SharedSameLife { instance3.Inc(); - rawHandle.ZeroAllKeepaliveTimestamps(); + rawHandle.SetAllKeepaliveTimestampsToDistantPast(); delayer.BreakAllDelays(); await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and finish expiring. @@ -199,7 +199,7 @@ public async Task ManagedLifetimeMetric_WithMultipleLabelingPaths_SharedSameLife Assert.AreEqual(3, _metrics.CreateCounter(MetricName, "", labelNames).WithLabels(labelValues).Value); } - rawHandle.ZeroAllKeepaliveTimestamps(); + rawHandle.SetAllKeepaliveTimestampsToDistantPast(); delayer.BreakAllDelays(); await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and finish expiring. @@ -207,7 +207,7 @@ public async Task ManagedLifetimeMetric_WithMultipleLabelingPaths_SharedSameLife Assert.AreEqual(3, _metrics.CreateCounter(MetricName, "", labelNames).WithLabels(labelValues).Value); } - rawHandle.ZeroAllKeepaliveTimestamps(); + rawHandle.SetAllKeepaliveTimestampsToDistantPast(); delayer.BreakAllDelays(); await Task.Delay(WaitForAsyncActionSleepTime); // Give it a moment to wake up and finish expiring. From ffaf31d2fa49345e9b825b41b5d493893844da3b Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Tue, 28 Nov 2023 15:17:23 +0200 Subject: [PATCH 173/230] Version number bump --- Resources/SolutionAssemblyInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Resources/SolutionAssemblyInfo.cs b/Resources/SolutionAssemblyInfo.cs index 77bbc325..2ec83a8b 100644 --- a/Resources/SolutionAssemblyInfo.cs +++ b/Resources/SolutionAssemblyInfo.cs @@ -2,7 +2,7 @@ using System.Runtime.CompilerServices; // This is the real version number, used in NuGet packages and for display purposes. -[assembly: AssemblyFileVersion("8.1.1")] +[assembly: AssemblyFileVersion("8.2.0")] // Only use major version here, with others kept at zero, for correct assembly binding logic. [assembly: AssemblyVersion("8.0.0")] From 5ce1261acbc9a399e5cb49ff6dea1d68e10f6663 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Tue, 28 Nov 2023 15:39:08 +0200 Subject: [PATCH 174/230] fix bad math --- Prometheus/PlatformCompatibilityHelpers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Prometheus/PlatformCompatibilityHelpers.cs b/Prometheus/PlatformCompatibilityHelpers.cs index dd7a3220..6069e4e7 100644 --- a/Prometheus/PlatformCompatibilityHelpers.cs +++ b/Prometheus/PlatformCompatibilityHelpers.cs @@ -9,5 +9,5 @@ public static TimeSpan StopwatchGetElapsedTime(long start, long end) => new((long)((end - start) * ((double)10_000_000 / Stopwatch.Frequency))); public static long ElapsedToTimeStopwatchTicks(TimeSpan elapsedTime) - => (long)(elapsedTime.TotalSeconds * (Stopwatch.Frequency / (double)10_000_000)); + => (long)(elapsedTime.Ticks * (Stopwatch.Frequency / (double)10_000_000)); } From 20ce5d725591f86bee2d32f434ca0e621e37a8cb Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Wed, 29 Nov 2023 15:06:41 +0200 Subject: [PATCH 175/230] Bugfix: MeterAdapter cannot clear collections on instrument shutdown because concurrent observations may still be in progress --- Prometheus/MeterAdapter.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Prometheus/MeterAdapter.cs b/Prometheus/MeterAdapter.cs index 1b48418e..a81e36e6 100644 --- a/Prometheus/MeterAdapter.cs +++ b/Prometheus/MeterAdapter.cs @@ -220,9 +220,9 @@ private void OnMeasurementsCompleted(Instrument instrument, object? state) // 2) We already have a perfectly satisfactory expiration based lifetime control model, no need to complicate with a second logic alongside. // 3) There is no 1:1 mapping between instrument and metric due to allowing flexible label name combinations, which may cause undesirable complexity. - // We know we will not need this data anymore, though, so we can throw it out. - _instrumentPrometheusNames.TryRemove(instrument, out _); - _instrumentPrometheusHelp.TryRemove(instrument, out _); + // We also cannot clear our mapping collections yet because it is possible that some measurement observations are still in progress! + // In other words, this may be called before the last OnMeasurementRecorded() call for the instrument has completed (perhaps even started?). + // The entire adapter data set will be collected when the Prometheus registry itself is garbage collected. } private string[] TagsToLabelNames(List> tags) From 774da2ed5dc3a7d949932cba1155221f9b4358be Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Wed, 29 Nov 2023 15:06:54 +0200 Subject: [PATCH 176/230] Fine-tuning of MeasurementBenchmarks --- Benchmark.NetCore/MeasurementBenchmarks.cs | 33 ++++++++++++---------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/Benchmark.NetCore/MeasurementBenchmarks.cs b/Benchmark.NetCore/MeasurementBenchmarks.cs index 7c3e86db..f13945f6 100644 --- a/Benchmark.NetCore/MeasurementBenchmarks.cs +++ b/Benchmark.NetCore/MeasurementBenchmarks.cs @@ -9,15 +9,7 @@ namespace Benchmark.NetCore; [MemoryDiagnoser] public class MeasurementBenchmarks { - public enum MetricType - { - Counter, - Gauge, - Histogram, - Summary - } - - [Params(1_000_000)] + [Params(100_000)] public int MeasurementCount { get; set; } [Params(ExemplarMode.Auto, ExemplarMode.None, ExemplarMode.Provided)] @@ -65,7 +57,10 @@ public enum ExemplarMode /// The max value we observe for histograms, to give us coverage of all the histogram buckets /// but not waste 90% of the benchmark on incrementing the +Inf bucket. /// - private const int HistogramMaxValue = 32 * 1024; + private const int WideHistogramMaxValue = 32 * 1024; + + // Same but for the regular histogram. + private readonly int _regularHistogramMaxValue; public MeasurementBenchmarks() { @@ -84,15 +79,21 @@ public MeasurementBenchmarks() new(0.99, 0.005) } }); + + // 1 ms to 32K ms, 16 buckets. Same as used in HTTP metrics by default. + var regularHistogramBuckets = Prometheus.Histogram.ExponentialBuckets(0.001, 2, 16); + + // Last one is +inf, so take the second-to-last. + _regularHistogramMaxValue = (int)regularHistogramBuckets[regularHistogramBuckets.Length - 2]; + var histogramTemplate = _factory.CreateHistogram("histogram", "test histogram", new[] { "label" }, new HistogramConfiguration { - // 1 ms to 32K ms, 16 buckets. Same as used in HTTP metrics by default. - Buckets = Prometheus.Histogram.ExponentialBuckets(0.001, 2, 16) + Buckets = regularHistogramBuckets }); var wideHistogramTemplate = _factory.CreateHistogram("wide_histogram", "test histogram", new[] { "label" }, new HistogramConfiguration { - Buckets = Prometheus.Histogram.LinearBuckets(1, HistogramMaxValue / 128, 128) + Buckets = Prometheus.Histogram.LinearBuckets(1, WideHistogramMaxValue / 128, 128) }); // We cache the children, as is typical usage. @@ -147,7 +148,8 @@ public void Histogram() for (var i = 0; i < MeasurementCount; i++) { - _histogram.Observe(i, exemplarProvider()); + var value = i % _regularHistogramMaxValue; + _histogram.Observe(value, exemplarProvider()); } } @@ -158,7 +160,8 @@ public void WideHistogram() for (var i = 0; i < MeasurementCount; i++) { - _wideHistogram.Observe(i, exemplarProvider()); + var value = i % WideHistogramMaxValue; + _wideHistogram.Observe(value, exemplarProvider()); } } From 1cd2a44bbed42ca49199d3260043fb4a7a911b98 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Wed, 29 Nov 2023 15:07:43 +0200 Subject: [PATCH 177/230] Incorporate MetricAdapterBenchmarks and MetricAdapterTests from #443 --- Benchmark.NetCore/MeterAdapterBenchmarks.cs | 94 +++++++++++ Tests.NetCore/MeterAdapterTests.cs | 172 ++++++++++++++++++++ 2 files changed, 266 insertions(+) create mode 100644 Benchmark.NetCore/MeterAdapterBenchmarks.cs create mode 100644 Tests.NetCore/MeterAdapterTests.cs diff --git a/Benchmark.NetCore/MeterAdapterBenchmarks.cs b/Benchmark.NetCore/MeterAdapterBenchmarks.cs new file mode 100644 index 00000000..0d6274ae --- /dev/null +++ b/Benchmark.NetCore/MeterAdapterBenchmarks.cs @@ -0,0 +1,94 @@ +using BenchmarkDotNet.Attributes; +using Prometheus; +using SDM = System.Diagnostics.Metrics; + +namespace Benchmark.NetCore; + +/// +/// Equivalent of MeasurementBenchmarks, except we publish the data via .NET Meters API and convert via MeterAdapter. +/// +[MemoryDiagnoser] +public class MeterAdapterBenchmarks +{ + [Params(100_000)] + public int MeasurementCount { get; set; } + + private readonly SDM.Meter _meter = new("prometheus-net benchmark"); + private readonly SDM.Counter _intCounter; + private readonly SDM.Counter _floatCounter; + private readonly SDM.Histogram _intHistogram; + private readonly SDM.Histogram _floatHistogram; + + private readonly CollectorRegistry _registry; + + private readonly IDisposable _meterAdapter; + + private readonly KeyValuePair _label = new("label", "label value"); + + public MeterAdapterBenchmarks() + { + _intCounter = _meter.CreateCounter("int_counter"); + _floatCounter = _meter.CreateCounter("float_counter"); + _intHistogram = _meter.CreateHistogram("int_histogram"); + _floatHistogram = _meter.CreateHistogram("float_histogram"); + + _registry = Metrics.NewCustomRegistry(); + + _meterAdapter = MeterAdapter.StartListening(new MeterAdapterOptions + { + InstrumentFilterPredicate = instrument => instrument.Meter == _meter, + Registry = _registry, + // We resolve a custom set of buckets here, for maximum stress and to avoid easily reused default buckets. + // 1 ms to 32K ms, 16 buckets. Same as used in HTTP metrics by default. + ResolveHistogramBuckets = _ => Histogram.ExponentialBuckets(0.001, 2, 16) + }); + + // We take a single measurement, to warm things up and avoid any first-call impact. + _intCounter.Add(1, _label); + _floatCounter.Add(1, _label); + _intHistogram.Record(1, _label); + _floatHistogram.Record(1, _label); + } + + [GlobalCleanup] + public void Cleanup() + { + _meterAdapter.Dispose(); + } + + [Benchmark] + public void CounterInt() + { + for (var i = 0; i < MeasurementCount; i++) + { + _intCounter.Add(1, _label); + } + } + + [Benchmark] + public void CounterFloat() + { + for (var i = 0; i < MeasurementCount; i++) + { + _floatCounter.Add(1, _label); + } + } + + [Benchmark] + public void HistogramInt() + { + for (var i = 0; i < MeasurementCount; i++) + { + _intHistogram.Record(i, _label); + } + } + + [Benchmark] + public void HistogramFloat() + { + for (var i = 0; i < MeasurementCount; i++) + { + _floatHistogram.Record(i, _label); + } + } +} diff --git a/Tests.NetCore/MeterAdapterTests.cs b/Tests.NetCore/MeterAdapterTests.cs new file mode 100644 index 00000000..cc4bc7cc --- /dev/null +++ b/Tests.NetCore/MeterAdapterTests.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SDM = System.Diagnostics.Metrics; + +namespace Prometheus.Tests; + +[TestClass] +public class MeterAdapterTests: IDisposable +{ + private CollectorRegistry _registry; + private MetricFactory _metrics; + private readonly SDM.Meter _meter = new("test"); + private readonly SDM.Counter _intCounter; + private readonly SDM.Counter _floatCounter; + private readonly SDM.Histogram _histogram; + private IDisposable _adapter; + + public MeterAdapterTests() + { + _registry = Metrics.NewCustomRegistry(); + _metrics = Metrics.WithCustomRegistry(_registry); + + _intCounter = _meter.CreateCounter("int_counter"); + _floatCounter = _meter.CreateCounter("float_counter"); + _histogram = _meter.CreateHistogram("histogram"); + + _registry = Metrics.NewCustomRegistry(); + _metrics = Metrics.WithCustomRegistry(_registry); + + _adapter = MeterAdapter.StartListening(new MeterAdapterOptions { + InstrumentFilterPredicate = instrument => { + return instrument.Meter == _meter; + }, + Registry = _registry, + MetricFactory = _metrics, + ResolveHistogramBuckets = instrument => new double[] { 1, 2, 3, 4 }, + }); + } + + private FakeSerializer SerializeMetrics(CollectorRegistry registry) + { + var serializer = new FakeSerializer(); + registry.CollectAndSerializeAsync(serializer, default).Wait(); + return serializer; + } + + private double GetValue(string meterName, params (string name, string value)[] labels) => + GetValue(_registry, meterName, labels); + private double GetValue(CollectorRegistry registry, string meterName, params (string name, string value)[] labels) + { + var serializer = SerializeMetrics(registry); + if (serializer.Data.Count == 0) + throw new Exception("No metrics found"); + var labelsString = string.Join(",", labels.Select(l => $"{l.name}=\"{l.value}\"")); + foreach (var d in serializer.Data) + { + Console.WriteLine($"{d.name} {d.labels} {d.canonicalLabel} {d.value}"); + + if (d.name == meterName && d.labels == labelsString) + { + return d.value; + } + } + if (serializer.Data.Any(d => d.name == meterName)) + throw new Exception($"Metric {meterName}{{{labelsString}}} not found, only these labels were found: {string.Join(" / ", serializer.Data.Where(d => d.name == meterName).Select(d => d.labels))}"); + throw new Exception($"Metric {meterName} not found, only these metrics were found: {string.Join(" / ", serializer.Data.Select(d => d.name).Distinct())}"); + } + + [TestMethod] + public void CounterInt() + { + _intCounter.Add(1); + Assert.AreEqual(1, GetValue("test_int_counter")); + _intCounter.Add(2); + Assert.AreEqual(3, GetValue("test_int_counter")); + } + + [TestMethod] + public void CounterFloat() + { + _floatCounter.Add(1); + Assert.AreEqual(1, GetValue("test_float_counter")); + _floatCounter.Add(0.002); + Assert.AreEqual(1.002, GetValue("test_float_counter")); + } + + [TestMethod] + public void CounterLabels() + { + _intCounter.Add(1, new ("l1", "value"), new ("l2", 111)); + Assert.AreEqual(1, GetValue("test_int_counter", ("l1", "value"), ("l2", "111"))); + _intCounter.Add(1000); + _intCounter.Add(1000, new ("l1", "value"), new ("l2", 0)); + _intCounter.Add(1000, new KeyValuePair("l1", "value")); + _intCounter.Add(1, new ("l2", 111), new ("l1", "value")); + Assert.AreEqual(2, GetValue("test_int_counter", ("l1", "value"), ("l2", "111"))); + Assert.AreEqual(1000, GetValue("test_int_counter", ("l1", "value"), ("l2", "0"))); + Assert.AreEqual(1000, GetValue("test_int_counter", ("l1", "value"))); + Assert.AreEqual(1000, GetValue("test_int_counter")); + } + + [TestMethod] + public void LabelRenaming() + { + _intCounter.Add(1, new ("my-label", 1), new ("Another.Label", 1)); + Assert.AreEqual(1, GetValue("test_int_counter", ("another_label", "1"), ("my_label", "1"))); + } + + + [TestMethod] + public void MultipleInstances() + { + _intCounter.Add(1000); + + var registry2 = Metrics.NewCustomRegistry(); + var metrics2 = Metrics.WithCustomRegistry(registry2); + + var adapter2 = MeterAdapter.StartListening(new MeterAdapterOptions { + InstrumentFilterPredicate = instrument => { + return instrument.Meter == _meter; + }, + Registry = registry2, + MetricFactory = metrics2, + ResolveHistogramBuckets = instrument => new double[] { 1, 2, 3, 4 }, + }); + + _intCounter.Add(1); + Assert.AreEqual(1001, GetValue("test_int_counter")); + Assert.AreEqual(1, GetValue(registry2, "test_int_counter")); + + adapter2.Dispose(); + + _intCounter.Add(1); + Assert.AreEqual(1002, GetValue("test_int_counter")); + Assert.AreEqual(1, GetValue(registry2, "test_int_counter")); + } + + + public void Dispose() + { + _adapter.Dispose(); + } + + class FakeSerializer : IMetricsSerializer + { + public List<(string name, string labels, string canonicalLabel, double value, ObservedExemplar exemplar)> Data = new(); + public Task FlushAsync(CancellationToken cancel) => Task.CompletedTask; + public ValueTask WriteEnd(CancellationToken cancel) => default; + + public ValueTask WriteFamilyDeclarationAsync(string name, byte[] nameBytes, byte[] helpBytes, MetricType type, byte[] typeBytes, CancellationToken cancel) => default; + + public ValueTask WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, CanonicalLabel canonicalLabel, CancellationToken cancel, double value, ObservedExemplar exemplar, byte[] suffix = null) + { + Data.Add(( + name: Encoding.UTF8.GetString(name), + labels: Encoding.UTF8.GetString(flattenedLabels), + canonicalLabel: canonicalLabel.ToString(), + value: value, + exemplar: exemplar + )); + return default; + } + + public ValueTask WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, CanonicalLabel canonicalLabel, CancellationToken cancel, long value, ObservedExemplar exemplar, byte[] suffix = null) => + WriteMetricPointAsync(name, flattenedLabels, canonicalLabel, cancel, (double)value, exemplar, suffix); + } +} From cac99cd0d76634af5f1211f2002d8ecf2c0c5d7a Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Wed, 29 Nov 2023 23:16:20 +0200 Subject: [PATCH 178/230] Speed up MeterAdapter by reducing closures and caching delegates --- Prometheus/MeterAdapter.cs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/Prometheus/MeterAdapter.cs b/Prometheus/MeterAdapter.cs index a81e36e6..9ff495a4 100644 --- a/Prometheus/MeterAdapter.cs +++ b/Prometheus/MeterAdapter.cs @@ -144,14 +144,14 @@ private void OnMeasurementRecorded(Instrument instrument, T measurement, Read var handle = _factory.CreateGauge(_instrumentPrometheusNames[instrument], _instrumentPrometheusHelp[instrument], labelNames); // A measurement is the increment. - handle.WithLease(x => x.Inc(value), labelValues); + handle.WithLease(_incrementGaugeFunc, value, labelValues); } else if (instrument is ObservableCounter) { var handle = _factory.CreateGauge(_instrumentPrometheusNames[instrument], _instrumentPrometheusHelp[instrument], labelNames); // A measurement is the current value. We transform it into a Set() to allow the counter to reset itself (unusual but who are we to say no). - handle.WithLease(x => x.Set(value), labelValues); + handle.WithLease(_setGaugeFunc, value, labelValues); } #if NET7_0_OR_GREATER else if (instrument is UpDownCounter) @@ -159,7 +159,7 @@ private void OnMeasurementRecorded(Instrument instrument, T measurement, Read var handle = _factory.CreateGauge(_instrumentPrometheusNames[instrument], _instrumentPrometheusHelp[instrument], labelNames); // A measurement is the increment. - handle.WithLease(x => x.Inc(value), labelValues); + handle.WithLease(_incrementGaugeFunc, value, labelValues); } #endif else if (instrument is ObservableGauge @@ -171,7 +171,7 @@ or ObservableUpDownCounter var handle = _factory.CreateGauge(_instrumentPrometheusNames[instrument], _instrumentPrometheusHelp[instrument], labelNames); // A measurement is the current value. - handle.WithLease(x => x.Set(value), labelValues); + handle.WithLease(_setGaugeFunc, value, labelValues); } else if (instrument is Histogram) { @@ -182,7 +182,7 @@ or ObservableUpDownCounter }); // A measurement is the observed value. - handle.WithLease(x => x.Observe(value), labelValues); + handle.WithLease(_observeHistogramFunc, value, labelValues); } else { @@ -195,6 +195,15 @@ or ObservableUpDownCounter } } + private static void IncrementGauge(double value, IGauge gauge) => gauge.Inc(value); + private static readonly Action _incrementGaugeFunc = IncrementGauge; + + private static void SetGauge(double value, IGauge gauge) => gauge.Set(value); + private static readonly Action _setGaugeFunc = SetGauge; + + private static void ObserveHistogram(double value, IHistogram histogram) => histogram.Observe(value); + private static readonly Action _observeHistogramFunc = ObserveHistogram; + private static void FilterLabelsToAvoidConflicts(string[] nameCandidates, string[] valueCandidates, string[] namesToSkip, out string[] names, out string[] values) { var acceptedNames = new List(nameCandidates.Length); From e98b9ea822a9ef0e5f2deb3b528b0af9b76c61f2 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Wed, 29 Nov 2023 23:26:26 +0200 Subject: [PATCH 179/230] Reuse StringBuilder when constructing Prometheus help strings --- Benchmark.NetCore/MeterAdapterBenchmarks.cs | 8 +++--- Prometheus/MeterAdapter.cs | 28 ++++++++++++++++----- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/Benchmark.NetCore/MeterAdapterBenchmarks.cs b/Benchmark.NetCore/MeterAdapterBenchmarks.cs index 0d6274ae..f28d9933 100644 --- a/Benchmark.NetCore/MeterAdapterBenchmarks.cs +++ b/Benchmark.NetCore/MeterAdapterBenchmarks.cs @@ -27,10 +27,10 @@ public class MeterAdapterBenchmarks public MeterAdapterBenchmarks() { - _intCounter = _meter.CreateCounter("int_counter"); - _floatCounter = _meter.CreateCounter("float_counter"); - _intHistogram = _meter.CreateHistogram("int_histogram"); - _floatHistogram = _meter.CreateHistogram("float_histogram"); + _intCounter = _meter.CreateCounter("int_counter", description: "This is an integer counter."); + _floatCounter = _meter.CreateCounter("float_counter", description: "This is a floating-point counter."); + _intHistogram = _meter.CreateHistogram("int_histogram", description: "This is an integer histogram."); + _floatHistogram = _meter.CreateHistogram("float_histogram", description: "This is a floating-point histogram."); _registry = Metrics.NewCustomRegistry(); diff --git a/Prometheus/MeterAdapter.cs b/Prometheus/MeterAdapter.cs index 9ff495a4..8bc657bb 100644 --- a/Prometheus/MeterAdapter.cs +++ b/Prometheus/MeterAdapter.cs @@ -240,7 +240,7 @@ private string[] TagsToLabelNames(List> tags) for (var i = 0; i < tags.Count; i++) { - var prometheusLabelName = _tagPrometheusNames.GetOrAdd(tags[i].Key, TranslateTagNameToPrometheusName); + var prometheusLabelName = _tagPrometheusNames.GetOrAdd(tags[i].Key, _translateTagNameToPrometheusNameFunc); labelNames[i] = prometheusLabelName; } @@ -283,19 +283,35 @@ private static string TranslateTagNameToPrometheusName(string tagName) return PrometheusNameHelpers.TranslateNameToPrometheusName(tagName); } + private static readonly Func _translateTagNameToPrometheusNameFunc = TranslateTagNameToPrometheusName; + + [ThreadStatic] + private static StringBuilder? _prometheusHelpBuilder; + + // If the string builder grows over this, we throw it away and use a new one next time to avoid keeping a large buffer around. + private const int PrometheusHelpBuilderReusableCapacity = 1 * 1024; + private static string TranslateInstrumentDescriptionToPrometheusHelp(Instrument instrument) { - var sb = new StringBuilder(); + _prometheusHelpBuilder ??= new(PrometheusHelpBuilderReusableCapacity); if (!string.IsNullOrWhiteSpace(instrument.Unit)) - sb.Append($"({instrument.Unit}) "); + _prometheusHelpBuilder.Append($"({instrument.Unit}) "); - sb.Append(instrument.Description); + _prometheusHelpBuilder.Append(instrument.Description); // Append the base type name, so we see what type of metric it is. - sb.Append($" ({instrument.GetType().Name})"); + _prometheusHelpBuilder.Append($" ({instrument.GetType().Name})"); + + var result = _prometheusHelpBuilder.ToString(); + + // If it grew too big, throw it away. + if (_prometheusHelpBuilder.Capacity > PrometheusHelpBuilderReusableCapacity) + _prometheusHelpBuilder = null; + else + _prometheusHelpBuilder.Clear(); - return sb.ToString(); + return result; } } #endif From 9de73f07d3c0531906c23afe66e5679e528a42d0 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 30 Nov 2023 09:50:33 +0200 Subject: [PATCH 180/230] StringSequence fine tuning and using Memory --- Prometheus/StringSequence.cs | 126 +++++++++++++++++++---------------- 1 file changed, 69 insertions(+), 57 deletions(-) diff --git a/Prometheus/StringSequence.cs b/Prometheus/StringSequence.cs index 39a5f3f9..cecd3370 100644 --- a/Prometheus/StringSequence.cs +++ b/Prometheus/StringSequence.cs @@ -1,4 +1,4 @@ -using System.Collections; +using System.Runtime.CompilerServices; namespace Prometheus; @@ -18,7 +18,7 @@ namespace Prometheus; public Enumerator GetEnumerator() { - return new Enumerator(_values, _inheritedValues); + return new Enumerator(_values.Span, _inheritedValues ?? []); } public ref struct Enumerator @@ -27,36 +27,47 @@ public ref struct Enumerator private int _completedInheritedArrays; private int _completedItemsInCurrentArray; - private readonly string[]? _values; - private readonly string[][]? _inheritedValues; + private readonly ReadOnlySpan _values; + private readonly ReadOnlyMemory[] _inheritedValues; - public string Current { get; private set; } + private ReadOnlySpan _currentArray; + private string _current; - public Enumerator(string[]? values, string[][]? inheritedValues) + public string Current + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _current; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Enumerator(ReadOnlySpan values, ReadOnlyMemory[] inheritedValues) { _values = values; _inheritedValues = inheritedValues; - Current = string.Empty; + _current = string.Empty; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool MoveNext() { // Do we have an item to get from the primary values array? - if (_values != null && _values.Length > _completedItemsInValues) + if (_values.Length > _completedItemsInValues) { - Current = _values[_completedItemsInValues]; + _current = _values[_completedItemsInValues]; _completedItemsInValues++; return true; } // Do we have an item to get from an inherited array? - else if (_inheritedValues != null && _inheritedValues.Length > _completedInheritedArrays) + else if (_inheritedValues.Length > _completedInheritedArrays) { - var array = _inheritedValues[_completedInheritedArrays]; - Current = array[_completedItemsInCurrentArray++]; + if (_completedItemsInCurrentArray == 0) + _currentArray = _inheritedValues[_completedInheritedArrays].Span; + + _current = _currentArray[_completedItemsInCurrentArray++]; // Did we complete this array? - if (array.Length == _completedItemsInCurrentArray) + if (_currentArray.Length == _completedItemsInCurrentArray) { _completedItemsInCurrentArray = 0; _completedInheritedArrays++; @@ -74,6 +85,8 @@ public bool MoveNext() public int Length { get; } + public bool IsEmpty => Length == 0; + public bool Equals(StringSequence other) { if (_hashCode != other._hashCode) return false; @@ -106,39 +119,33 @@ public override bool Equals(object? obj) // There are various ways we can make a StringSequence, comining one or two parents and maybe adding some extra to the start. // This ctor tries to account for all these options. - private StringSequence(StringSequence? inheritFrom, StringSequence? thenFrom, string[]? andFinallyPrepend) + private StringSequence(StringSequence inheritFrom, StringSequence thenFrom, ReadOnlyMemory andFinallyPrepend) { - // Simplify construction if we are given empty inputs. - if (inheritFrom.HasValue && inheritFrom.Value.Length == 0) - inheritFrom = null; - - if (thenFrom.HasValue && thenFrom.Value.Length == 0) - thenFrom = null; - - if (andFinallyPrepend != null && andFinallyPrepend.Length == 0) - andFinallyPrepend = null; - - // Simplify construction if we have nothing at all. - if (!inheritFrom.HasValue && !thenFrom.HasValue && andFinallyPrepend == null) - return; - // Simplify construction if we just need to match one of the cloneable inputs. - if (inheritFrom.HasValue && !thenFrom.HasValue && andFinallyPrepend == null) + if (!inheritFrom.IsEmpty && thenFrom.IsEmpty && andFinallyPrepend.Length == 0) { - this = inheritFrom.Value; + this = inheritFrom; return; } - else if (thenFrom.HasValue && !inheritFrom.HasValue && andFinallyPrepend == null) + else if (!thenFrom.IsEmpty && inheritFrom.IsEmpty && andFinallyPrepend.Length == 0) { - this = thenFrom.Value; + this = thenFrom; return; } - // Anything inherited is already validated. - if (andFinallyPrepend != null) + // Simplify construction if we have nothing at all. + if (inheritFrom.IsEmpty && thenFrom.IsEmpty && andFinallyPrepend.Length == 0) + return; + + // Anything inherited is already validated. Perform a sanity check on anything new. + if (andFinallyPrepend.Length != 0) { - foreach (var ownValue in andFinallyPrepend) + var span = andFinallyPrepend.Span; + + for (var i = 0; i < span.Length; i++) { + var ownValue = span[i]; + if (ownValue == null) throw new NotSupportedException("Null values are not supported for metric label names and values."); } @@ -147,9 +154,7 @@ private StringSequence(StringSequence? inheritFrom, StringSequence? thenFrom, st _values = andFinallyPrepend; _inheritedValues = InheritFrom(inheritFrom, thenFrom); - Length = (_values?.Length ?? 0) - + (inheritFrom.HasValue ? inheritFrom.Value.Length : 0) - + (thenFrom.HasValue ? thenFrom.Value.Length : 0); + Length = _values.Length + inheritFrom.Length + thenFrom.Length; _hashCode = CalculateHashCode(); } @@ -159,13 +164,21 @@ public static StringSequence From(params string[] values) if (values.Length == 0) return Empty; - return new StringSequence(null, null, values); + return new StringSequence(Empty, Empty, values); + } + + public static StringSequence From(ReadOnlyMemory values) + { + if (values.Length == 0) + return Empty; + + return new StringSequence(Empty, Empty, values); } // Creates a new sequence, inheriting all current values and optionally adding more. New values are prepended to the sequence, inherited values come last. public StringSequence InheritAndPrepend(params string[] prependValues) { - return new StringSequence(this, null, prependValues); + return new StringSequence(this, Empty, prependValues); } // Creates a new sequence, inheriting all current values and optionally adding more. New values are prepended to the sequence, inherited values come last. @@ -180,18 +193,17 @@ public StringSequence Concat(StringSequence concatenatedValues) return new StringSequence(concatenatedValues, this, null); } - // Values added by this instance. - // It may be null because structs have a default ctor and let's be paranoid. - private readonly string[]? _values; + // Values added by this instance. It may be empty. + private readonly ReadOnlyMemory _values; // Inherited values from one or more parent instances. - // It may be null because structs have a default ctor and let's be paranoid. - private readonly string[][]? _inheritedValues; + // It may be null because structs have a default ctor that zero-initializes them, so watch out. + private readonly ReadOnlyMemory[]? _inheritedValues; private readonly int _hashCode; // We can inherit from one or two parent sequences. Order is "first at the end, second prefixed to it" as is typical (ancestors at the end). - private static string[][]? InheritFrom(StringSequence? first, StringSequence? second) + private static ReadOnlyMemory[]? InheritFrom(StringSequence first, StringSequence second) { // Expected output: second._values, second._inheritedValues, first._values, first._inheritedValues @@ -200,16 +212,16 @@ public StringSequence Concat(StringSequence concatenatedValues) int secondOwnArrayCount = 0; int secondInheritedArrayCount = 0; - if (first.HasValue) + if (!first.IsEmpty) { - firstOwnArrayCount = first.Value._values?.Length > 0 ? 1 : 0; - firstInheritedArrayCount = first.Value._inheritedValues?.Length ?? 0; + firstOwnArrayCount = first._values.Length > 0 ? 1 : 0; + firstInheritedArrayCount = first._inheritedValues?.Length ?? 0; } - if (second.HasValue) + if (!second.IsEmpty) { - secondOwnArrayCount = second.Value._values?.Length > 0 ? 1 : 0; - secondInheritedArrayCount = second.Value._inheritedValues?.Length ?? 0; + secondOwnArrayCount = second._values.Length > 0 ? 1 : 0; + secondInheritedArrayCount = second._inheritedValues?.Length ?? 0; } var totalSegmentCount = firstOwnArrayCount + firstInheritedArrayCount + secondOwnArrayCount + secondInheritedArrayCount; @@ -217,29 +229,29 @@ public StringSequence Concat(StringSequence concatenatedValues) if (totalSegmentCount == 0) return null; - var result = new string[totalSegmentCount][]; + var result = new ReadOnlyMemory[totalSegmentCount]; var targetIndex = 0; if (secondOwnArrayCount != 0) { - result[targetIndex++] = second!.Value._values!; + result[targetIndex++] = second._values; } if (secondInheritedArrayCount != 0) { - Array.Copy(second!.Value._inheritedValues!, 0, result, targetIndex, secondInheritedArrayCount); + Array.Copy(second._inheritedValues!, 0, result, targetIndex, secondInheritedArrayCount); targetIndex += secondInheritedArrayCount; } if (firstOwnArrayCount != 0) { - result[targetIndex++] = first!.Value._values!; + result[targetIndex++] = first._values; } if (firstInheritedArrayCount != 0) { - Array.Copy(first!.Value._inheritedValues!, 0, result, targetIndex, firstInheritedArrayCount); + Array.Copy(first._inheritedValues!, 0, result, targetIndex, firstInheritedArrayCount); targetIndex += firstInheritedArrayCount; } From a18680b2ad0bced775f74fec439319761b0c9ab2 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 30 Nov 2023 09:50:55 +0200 Subject: [PATCH 181/230] NetFx tests dependencies --- Tests.NetFramework/app.config | 53 ++++++++++++-------- Tests.NetFramework/packages.config | 3 ++ Tests.NetFramework/tests.netframework.csproj | 10 ++++ 3 files changed, 45 insertions(+), 21 deletions(-) diff --git a/Tests.NetFramework/app.config b/Tests.NetFramework/app.config index da5fff44..0ae1d6b2 100644 --- a/Tests.NetFramework/app.config +++ b/Tests.NetFramework/app.config @@ -1,23 +1,34 @@  - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tests.NetFramework/packages.config b/Tests.NetFramework/packages.config index 500902a2..5644b6c2 100644 --- a/Tests.NetFramework/packages.config +++ b/Tests.NetFramework/packages.config @@ -4,6 +4,9 @@ + + + diff --git a/Tests.NetFramework/tests.netframework.csproj b/Tests.NetFramework/tests.netframework.csproj index 516278b1..98ce9ba4 100644 --- a/Tests.NetFramework/tests.netframework.csproj +++ b/Tests.NetFramework/tests.netframework.csproj @@ -65,9 +65,19 @@ ..\packages\NSubstitute.4.4.0\lib\net46\NSubstitute.dll + + ..\packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll + + + ..\packages\System.Memory.4.5.5\lib\net461\System.Memory.dll + + + + ..\packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll + ..\packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll From a6a737a07e865a41d72f455fd441cf660f81f1f8 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 30 Nov 2023 09:51:19 +0200 Subject: [PATCH 182/230] MeterAdapter improvements based on ideas from #443 --- Prometheus/MeterAdapter.cs | 282 +++++++++++++++++++++++++++++-------- 1 file changed, 225 insertions(+), 57 deletions(-) diff --git a/Prometheus/MeterAdapter.cs b/Prometheus/MeterAdapter.cs index 8bc657bb..0dc74da5 100644 --- a/Prometheus/MeterAdapter.cs +++ b/Prometheus/MeterAdapter.cs @@ -1,7 +1,9 @@ #if NET6_0_OR_GREATER +using System.Buffers; using System.Collections.Concurrent; using System.Diagnostics; using System.Diagnostics.Metrics; +using System.Runtime.InteropServices; using System.Text; namespace Prometheus; @@ -32,6 +34,9 @@ public static IDisposable StartListening(MeterAdapterOptions options) private MeterAdapter(MeterAdapterOptions options) { + _createGaugeFunc = CreateGauge; + _createHistogramFunc = CreateHistogram; + _options = options; _registry = options.Registry; @@ -79,11 +84,11 @@ private MeterAdapter(MeterAdapterOptions options) private readonly MeterListener _listener = new MeterListener(); private volatile bool _disposed; - private readonly object _lock = new(); + private readonly object _disposedLock = new(); public void Dispose() { - lock (_lock) + lock (_disposedLock) { if (_disposed) return; @@ -124,42 +129,35 @@ private void OnMeasurementRecorded(Instrument instrument, T measurement, Read try { - // NB! Order of labels matters in the prometheus-net API. However, in .NET Meters the data is unordered. - // Therefore, we need to sort the labels to ensure that we always create metrics with the same order. - var sortedTags = tags.ToArray().OrderBy(x => x.Key, StringComparer.Ordinal).ToList(); - var labelNameCandidates = TagsToLabelNames(sortedTags); - var labelValueCandidates = TagsToLabelValues(sortedTags); - - // NOTE: As we accept random input from external code here, there is no guarantee that the labels in this code do not conflict with existing static labels. - // We must therefore take explicit action here to prevent conflict (as prometheus-net will detect and fault on such conflicts). We do this by inspecting - // the internals of the factory to identify conflicts with any static labels, and remove those lables from the Meters API data point (static overrides dynamic). - FilterLabelsToAvoidConflicts(labelNameCandidates, labelValueCandidates, _inheritedStaticLabelNames, out var labelNames, out var labelValues); - var value = Convert.ToDouble(measurement); - // We do not represent any of the "counter" style .NET meter types as counters because they may be re-created on the .NET Meters side at any time, decrementing the value! + // We do not represent any of the "counter" style .NET meter types as counters because + // they may be re-created on the .NET Meters side at any time, decrementing the value! if (instrument is Counter) { - var handle = _factory.CreateGauge(_instrumentPrometheusNames[instrument], _instrumentPrometheusHelp[instrument], labelNames); + var context = GetOrCreateGaugeContext(instrument, tags); + var labelValues = TagsToLabelValues(context, tags); // A measurement is the increment. - handle.WithLease(_incrementGaugeFunc, value, labelValues); + context.MetricInstanceHandle.WithLease(_incrementGaugeFunc, value, labelValues); } else if (instrument is ObservableCounter) { - var handle = _factory.CreateGauge(_instrumentPrometheusNames[instrument], _instrumentPrometheusHelp[instrument], labelNames); + var context = GetOrCreateGaugeContext(instrument, tags); + var labelValues = TagsToLabelValues(context, tags); // A measurement is the current value. We transform it into a Set() to allow the counter to reset itself (unusual but who are we to say no). - handle.WithLease(_setGaugeFunc, value, labelValues); + context.MetricInstanceHandle.WithLease(_setGaugeFunc, value, labelValues); } #if NET7_0_OR_GREATER else if (instrument is UpDownCounter) { - var handle = _factory.CreateGauge(_instrumentPrometheusNames[instrument], _instrumentPrometheusHelp[instrument], labelNames); + var context = GetOrCreateGaugeContext(instrument, tags); + var labelValues = TagsToLabelValues(context, tags); // A measurement is the increment. - handle.WithLease(_incrementGaugeFunc, value, labelValues); + context.MetricInstanceHandle.WithLease(_incrementGaugeFunc, value, labelValues); } #endif else if (instrument is ObservableGauge @@ -168,21 +166,19 @@ or ObservableUpDownCounter #endif ) { - var handle = _factory.CreateGauge(_instrumentPrometheusNames[instrument], _instrumentPrometheusHelp[instrument], labelNames); - + var context = GetOrCreateGaugeContext(instrument, tags); + var labelValues = TagsToLabelValues(context, tags); + // A measurement is the current value. - handle.WithLease(_setGaugeFunc, value, labelValues); + context.MetricInstanceHandle.WithLease(_setGaugeFunc, value, labelValues); } else if (instrument is Histogram) { - var handle = _factory.CreateHistogram(_instrumentPrometheusNames[instrument], _instrumentPrometheusHelp[instrument], labelNames, new HistogramConfiguration - { - // We outsource the bucket definition to the callback in options, as it might need to be different for different instruments. - Buckets = _options.ResolveHistogramBuckets(instrument) - }); + var context = GetOrCreateHistogramContext(instrument, tags); + var labelValues = TagsToLabelValues(context, tags); // A measurement is the observed value. - handle.WithLease(_observeHistogramFunc, value, labelValues); + context.MetricInstanceHandle.WithLease(_observeHistogramFunc, value, labelValues); } else { @@ -204,22 +200,205 @@ or ObservableUpDownCounter private static void ObserveHistogram(double value, IHistogram histogram) => histogram.Observe(value); private static readonly Action _observeHistogramFunc = ObserveHistogram; - private static void FilterLabelsToAvoidConflicts(string[] nameCandidates, string[] valueCandidates, string[] namesToSkip, out string[] names, out string[] values) + // Cache key: Instrument + user-ordered list of label names. + // NB! The same Instrument may be cached multiple times, with the same label names in a different order! + private readonly struct CacheKey(Instrument instrument, StringSequence meterLabelNames) + { + public Instrument Instrument { get; } = instrument; + + // Order is whatever was provided by the caller of the .NET Meters API. + public StringSequence MeterLabelNames { get; } = meterLabelNames; + + public override readonly bool Equals(object? obj) => obj is CacheKey other && Equals(other); + + public override readonly int GetHashCode() => _hashCode; + private readonly int _hashCode = HashCode.Combine(instrument, meterLabelNames); + + public readonly bool Equals(CacheKey other) => Instrument == other.Instrument && MeterLabelNames.Equals(other.MeterLabelNames); + } + + // Cache value: Prometheus metric handle + Prometheus-ordered indexes into original Meters tags list. + // Not all Meter tags may be preserved, as some may have conflicted with static labels and been filtered out. + private sealed class MetricContext( + IManagedLifetimeMetricHandle metricInstanceHandle, + int[] prometheusLabelValueIndexes) + where TMetricInterface : ICollectorChild + { + public IManagedLifetimeMetricHandle MetricInstanceHandle { get; } = metricInstanceHandle; + + // Index into the .NET Meters API labels list, indicating which original label to take the value from. + public int[] PrometheusLabelValueIndexes { get; } = prometheusLabelValueIndexes; + } + + private readonly Dictionary> _gaugeCache = new(); + private readonly ReaderWriterLockSlim _gaugeCacheLock = new(); + + private readonly Dictionary> _histogramCache = new(); + private readonly ReaderWriterLockSlim _histogramCacheLock = new(); + + private MetricContext GetOrCreateGaugeContext(Instrument instrument, ReadOnlySpan> tags) + => GetOrCreateMetricContext(instrument, tags, _createGaugeFunc, _gaugeCacheLock, _gaugeCache); + + private MetricContext GetOrCreateHistogramContext(Instrument instrument, ReadOnlySpan> tags) + => GetOrCreateMetricContext(instrument, tags, _createHistogramFunc, _histogramCacheLock, _histogramCache); + + private IManagedLifetimeMetricHandle CreateGauge(Instrument instrument, string name, string help, string[] labelNames) + => _factory.CreateGauge(name, help, labelNames); + private Func> _createGaugeFunc; + + private IManagedLifetimeMetricHandle CreateHistogram(Instrument instrument, string name, string help, string[] labelNames) + => _factory.CreateHistogram(name, help, labelNames, new HistogramConfiguration + { + // We outsource the bucket definition to the callback in options, as it might need to be different for different instruments. + Buckets = _options.ResolveHistogramBuckets(instrument) + }); + private Func> _createHistogramFunc; + + private MetricContext GetOrCreateMetricContext( + Instrument instrument, + ReadOnlySpan> tags, + Func> metricFactory, + ReaderWriterLockSlim cacheLock, + Dictionary> cache) + where TMetricInstance : ICollectorChild { - var acceptedNames = new List(nameCandidates.Length); - var acceptedValues = new List(valueCandidates.Length); + // Use a pooled array for the cache key if we are performing a lookup. + // This avoids allocating a new array if the context is already cached. + var meterLabelNamesBuffer = ArrayPool.Shared.Rent(tags.Length); + var meterLabelNamesCount = tags.Length; - for (int i = 0; i < nameCandidates.Length; i++) + try { - if (namesToSkip.Contains(nameCandidates[i])) - continue; + for (var i = 0; i < tags.Length; i++) + meterLabelNamesBuffer[i] = tags[i].Key; + + var meterLabelNames = StringSequence.From(meterLabelNamesBuffer.AsMemory(0, meterLabelNamesCount)); + var cacheKey = new CacheKey(instrument, meterLabelNames); - acceptedNames.Add(nameCandidates[i]); - acceptedValues.Add(valueCandidates[i]); + cacheLock.EnterReadLock(); + + try + { + // In the common case, we will find the context in the cache and can return it here without any memory allocation. + if (cache.TryGetValue(cacheKey, out var context)) + return context; + } + finally + { + cacheLock.ExitReadLock(); + } + } + finally + { + ArrayPool.Shared.Return(meterLabelNamesBuffer); } - names = acceptedNames.ToArray(); - values = acceptedValues.ToArray(); + // If we got here, we did not find the context in the cache. Make a new one. + return CreateMetricContext(instrument, tags, metricFactory, cacheLock, cache); + } + + private MetricContext CreateMetricContext( + Instrument instrument, + ReadOnlySpan> tags, + Func> metricFactory, + ReaderWriterLockSlim cacheLock, + Dictionary> cache) + where TMetricInstance : ICollectorChild + { + var meterLabelNamesBuffer = new string[tags.Length]; + + for (var i = 0; i < tags.Length; i++) + meterLabelNamesBuffer[i] = tags[i].Key; + + var meterLabelNames = StringSequence.From(meterLabelNamesBuffer); + var cacheKey = new CacheKey(instrument, meterLabelNames); + + // Create the context before taking any locks, to avoid holding the cache too long. + DeterminePrometheusLabels(tags, out var prometheusLabelNames, out var prometheusLabelValueIndexes); + var metricHandle = metricFactory(instrument, _instrumentPrometheusNames[instrument], _instrumentPrometheusHelp[instrument], prometheusLabelNames); + + cacheLock.EnterWriteLock(); + + try + { + // It is theoretically possible that another thread got to it already, in which case we exit early. + if (cache.TryGetValue(cacheKey, out var context)) + return context; + + context = new MetricContext(metricHandle, prometheusLabelValueIndexes); + cache.Add(cacheKey, context); + return context; + } + finally + { + cacheLock.ExitWriteLock(); + } + } + + private void DeterminePrometheusLabels( + ReadOnlySpan> tags, + out string[] prometheusLabelNames, + out int[] prometheusLabelValueIndexes) + { + var originalsCount = tags.Length; + + // Prometheus name of the label. + var namesBuffer = ArrayPool.Shared.Rent(originalsCount); + // Index into the original label list. + var indexesBuffer = ArrayPool.Shared.Rent(originalsCount); + // Whether the label should be skipped entirely (because it conflicts with a static label). + var skipFlagsBuffer = ArrayPool.Shared.Rent(originalsCount); + + try + { + for (var i = 0; i < tags.Length; i++) + { + var prometheusName = _tagPrometheusNames.GetOrAdd(tags[i].Key, _translateTagNameToPrometheusNameFunc); + + namesBuffer[i] = prometheusName; + indexesBuffer[i] = i; + } + + // The order of labels matters in the prometheus-net API. However, in .NET Meters the tags are unordered. + // Therefore, we need to sort the labels to ensure that we always create metrics with the same order. + Array.Sort(keys: namesBuffer, items: indexesBuffer, index: 0, length: originalsCount, StringComparer.Ordinal); + + // NOTE: As we accept random input from external code here, there is no guarantee that the labels in this code + // do not conflict with existing static labels. We must therefore take explicit action here to prevent conflict + // (as prometheus-net will detect and fault on such conflicts). We do this by inspecting the internals of the + // factory to identify conflicts with any static labels, and remove those lables from the Meters API data point + // (static overrides dynamic) if there is a match (by just skipping them in our output index set). + var preservedLabelCount = 0; + + for (var i = 0; i < tags.Length; i++) + { + skipFlagsBuffer[i] = _inheritedStaticLabelNames.Contains(namesBuffer[i], StringComparer.Ordinal); + + if (skipFlagsBuffer[i] == false) + preservedLabelCount++; + } + + prometheusLabelNames = new string[preservedLabelCount]; + prometheusLabelValueIndexes = new int[preservedLabelCount]; + + var nextIndex = 0; + + for (var i = 0; i < tags.Length; i++) + { + if (skipFlagsBuffer[i]) + continue; + + prometheusLabelNames[nextIndex] = namesBuffer[i]; + prometheusLabelValueIndexes[nextIndex] = indexesBuffer[i]; + nextIndex++; + } + } + finally + { + ArrayPool.Shared.Return(skipFlagsBuffer); + ArrayPool.Shared.Return(indexesBuffer); + ArrayPool.Shared.Return(namesBuffer); + } } private void OnMeasurementsCompleted(Instrument instrument, object? state) @@ -234,32 +413,21 @@ private void OnMeasurementsCompleted(Instrument instrument, object? state) // The entire adapter data set will be collected when the Prometheus registry itself is garbage collected. } - private string[] TagsToLabelNames(List> tags) - { - var labelNames = new string[tags.Count]; - - for (var i = 0; i < tags.Count; i++) - { - var prometheusLabelName = _tagPrometheusNames.GetOrAdd(tags[i].Key, _translateTagNameToPrometheusNameFunc); - labelNames[i] = prometheusLabelName; - } - - return labelNames; - } - - private string[] TagsToLabelValues(List> tags) + private static string[] TagsToLabelValues(MetricContext context, ReadOnlySpan> tags) + where TMetricInstance : ICollectorChild { - var labelValues = new string[tags.Count]; + var labelValues = new string[context.PrometheusLabelValueIndexes.Length]; - for (var i = 0; i < tags.Count; i++) + for (var i = 0; i < labelValues.Length; i++) { - labelValues[i] = tags[i].Value?.ToString() ?? ""; + var index = context.PrometheusLabelValueIndexes[i]; + labelValues[i] = tags[index].Value?.ToString() ?? ""; } return labelValues; } - // We use these dictionaries to register Prometheus metrics on-demand for different tag sets. + // We use these dictionaries to register Prometheus metrics on-demand for different instruments. private static readonly ConcurrentDictionary _instrumentPrometheusNames = new(); private static readonly ConcurrentDictionary _instrumentPrometheusHelp = new(); From f84c9ecb3cf15eb35ffd62b8d839c41dfc309cd6 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 30 Nov 2023 11:19:11 +0200 Subject: [PATCH 183/230] Fix missing accidental lack of delegate caching --- Prometheus/CollectorFamily.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Prometheus/CollectorFamily.cs b/Prometheus/CollectorFamily.cs index 651b0ec3..5ce2c61c 100644 --- a/Prometheus/CollectorFamily.cs +++ b/Prometheus/CollectorFamily.cs @@ -25,7 +25,7 @@ internal async ValueTask CollectAndSerializeAsync(IMetricsSerializer serializer, var operation = _serializeFamilyOperationPool.Get(); operation.Serializer = serializer; - await ForEachCollectorAsync(CollectAndSerialize, operation, cancel); + await ForEachCollectorAsync(_collectAndSerializeFunc, operation, cancel); _serializeFamilyOperationPool.Return(operation); } From d246784e1a9649b71b3a3059e98a540448e0a41d Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 30 Nov 2023 11:43:40 +0200 Subject: [PATCH 184/230] Skip allocating a Lazy<> for each metric instance --- Prometheus/Collector.cs | 35 +++++++++++++---------------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/Prometheus/Collector.cs b/Prometheus/Collector.cs index d81b9486..18c8e89e 100644 --- a/Prometheus/Collector.cs +++ b/Prometheus/Collector.cs @@ -23,7 +23,7 @@ public abstract class Collector internal byte[] NameBytes => NonCapturingLazyInitializer.EnsureInitialized(ref _nameBytes, this, _assignNameBytesFunc)!; private byte[]? _nameBytes; - private static readonly Action _assignNameBytesFunc; + private static readonly Action _assignNameBytesFunc = AssignNameBytes; private static void AssignNameBytes(Collector instance) => instance._nameBytes = PrometheusConstants.ExportEncoding.GetBytes(instance.Name); /// @@ -33,7 +33,7 @@ public abstract class Collector internal byte[] HelpBytes => NonCapturingLazyInitializer.EnsureInitialized(ref _helpBytes, this, _assignHelpBytesFunc)!; private byte[]? _helpBytes; - private static readonly Action _assignHelpBytesFunc; + private static readonly Action _assignHelpBytesFunc = AssignHelpBytes; private static void AssignHelpBytes(Collector instance) => instance._helpBytes = string.IsNullOrWhiteSpace(instance.Help) ? [] : PrometheusConstants.ExportEncoding.GetBytes(instance.Help); @@ -44,7 +44,7 @@ private static void AssignHelpBytes(Collector instance) => /// public string[] LabelNames => NonCapturingLazyInitializer.EnsureInitialized(ref _labelNames, this, _assignLabelNamesFunc)!; private string[]? _labelNames; - private static readonly Action _assignLabelNamesFunc; + private static readonly Action _assignLabelNamesFunc = AssignLabelNames; private static void AssignLabelNames(Collector instance) => instance._labelNames = instance.InstanceLabelNames.ToArray(); internal StringSequence InstanceLabelNames; @@ -76,13 +76,6 @@ private static void AssignHelpBytes(Collector instance) => private static readonly Regex LabelNameRegex = new Regex(ValidLabelNameExpression, RegexOptions.Compiled); private static readonly Regex ReservedLabelRegex = new Regex(ReservedLabelNameExpression, RegexOptions.Compiled); - static Collector() - { - _assignNameBytesFunc = AssignNameBytes; - _assignHelpBytesFunc = AssignHelpBytes; - _assignLabelNamesFunc = AssignLabelNames; - } - internal Collector(string name, string help, StringSequence instanceLabelNames, LabelSequence staticLabels) { if (!MetricNameRegex.IsMatch(name)) @@ -175,12 +168,15 @@ public abstract class Collector : Collector, ICollector // Lazy-initialized since not every collector will use a child with no labels. // Lazy instance will be replaced if the unlabelled timeseries is removed. - private Lazy _unlabelledLazy; + private TChild? _lazyUnlabelled; /// /// Gets the child instance that has no labels. /// - protected internal TChild Unlabelled => _unlabelledLazy.Value; + protected internal TChild Unlabelled => LazyInitializer.EnsureInitialized(ref _lazyUnlabelled, _createdUnlabelledFunc)!; + + private TChild CreateUnlabelled() => GetOrAddLabelled(LabelSequence.Empty); + private readonly Func _createdUnlabelledFunc; // We need it for the ICollector interface but using this is rarely relevant in client code, so keep it obscured. TChild ICollector.Unlabelled => Unlabelled; @@ -227,15 +223,10 @@ internal override void RemoveLabelled(LabelSequence labels) { // If we remove the unlabeled instance (technically legitimate, if the caller really desires to do so) then // we need to also ensure that the special-casing used for it gets properly wired up the next time. - _unlabelledLazy = GetUnlabelledLazyInitializer(); + Volatile.Write(ref _lazyUnlabelled, null); } } - private Lazy GetUnlabelledLazyInitializer() - { - return new Lazy(() => GetOrAddLabelled(LabelSequence.Empty)); - } - internal override int ChildCount { get @@ -357,11 +348,11 @@ private TChild CreateLabelledChild(LabelSequence instanceLabels) internal Collector(string name, string help, StringSequence instanceLabelNames, LabelSequence staticLabels, bool suppressInitialValue, ExemplarBehavior exemplarBehavior) : base(name, help, instanceLabelNames, staticLabels) { + _createdUnlabelledFunc = CreateUnlabelled; + _suppressInitialValue = suppressInitialValue; _exemplarBehavior = exemplarBehavior; - _unlabelledLazy = GetUnlabelledLazyInitializer(); - _createdLabelledChildFunc = CreateLabelledChild; } @@ -423,8 +414,8 @@ private void EnsureUnlabelledMetricCreatedIfNoLabels() // If there are no label names then clearly this metric is supposed to be used unlabelled, so create it. // Otherwise, we allow unlabelled metrics to be used if the user explicitly does it but omit them by default. - if (!_unlabelledLazy.IsValueCreated && InstanceLabelNames.Length == 0) - GetOrAddLabelled(LabelSequence.Empty); + if (InstanceLabelNames.Length == 0) + LazyInitializer.EnsureInitialized(ref _lazyUnlabelled, _createdUnlabelledFunc); } private readonly ExemplarBehavior _exemplarBehavior; From 46584727eee27222f1391a9bceb955cdb4046ee8 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 30 Nov 2023 13:21:11 +0200 Subject: [PATCH 185/230] Speed up metric registration by reducing indirection in the metric setup logic --- Prometheus/CollectorFamily.cs | 10 ++++-- Prometheus/CollectorIdentity.cs | 2 +- Prometheus/CollectorRegistry.cs | 56 ++++++------------------------ Prometheus/MetricFactory.cs | 61 +++++++++++++++------------------ 4 files changed, 47 insertions(+), 82 deletions(-) diff --git a/Prometheus/CollectorFamily.cs b/Prometheus/CollectorFamily.cs index 5ce2c61c..12ea17c7 100644 --- a/Prometheus/CollectorFamily.cs +++ b/Prometheus/CollectorFamily.cs @@ -73,7 +73,13 @@ private async ValueTask CollectAndSerialize(Collector collector, SerializeFamily private readonly Func _collectAndSerializeFunc; - internal Collector GetOrAdd(CollectorIdentity identity, in CollectorRegistry.CollectorInitializer initializer) + internal Collector GetOrAdd( + in CollectorIdentity identity, + string name, + string help, + TConfiguration configuration, + ExemplarBehavior exemplarBehavior, + CollectorRegistry.CollectorInitializer initializer) where TCollector : Collector where TConfiguration : MetricConfiguration { @@ -99,7 +105,7 @@ internal Collector GetOrAdd(CollectorIdentity identi if (_collectors.TryGetValue(identity, out var collector)) return collector; - var newCollector = initializer.CreateInstance(); + var newCollector = initializer(name, help, identity.InstanceLabelNames, identity.StaticLabels, configuration, exemplarBehavior); _collectors.Add(identity, newCollector); return newCollector; } diff --git a/Prometheus/CollectorIdentity.cs b/Prometheus/CollectorIdentity.cs index c6cd75c1..0f90cebf 100644 --- a/Prometheus/CollectorIdentity.cs +++ b/Prometheus/CollectorIdentity.cs @@ -5,7 +5,7 @@ /// * Any difference in static labels (keys or values) means it is a different collector. /// * Any difference in the names of instance labels means it is a different collector. /// -internal struct CollectorIdentity : IEquatable +internal readonly struct CollectorIdentity : IEquatable { public readonly StringSequence InstanceLabelNames; public readonly LabelSequence StaticLabels; diff --git a/Prometheus/CollectorRegistry.cs b/Prometheus/CollectorRegistry.cs index d714f95d..067a394e 100644 --- a/Prometheus/CollectorRegistry.cs +++ b/Prometheus/CollectorRegistry.cs @@ -153,62 +153,26 @@ public Task CollectAndExportAsTextAsync(Stream to, ExpositionFormat format, Canc return CollectAndSerializeAsync(new TextSerializer(to, format), cancel); } - // We pass this thing to GetOrAdd to avoid allocating a collector or a closure. - // This reduces memory usage in situations where the collector is already registered. - internal readonly ref struct CollectorInitializer + internal delegate TCollector CollectorInitializer(string name, string help, in StringSequence instanceLabelNames, in LabelSequence staticLabels, TConfiguration configuration, ExemplarBehavior exemplarBehavior) where TCollector : Collector - where TConfiguration : MetricConfiguration - { - private readonly CreateInstanceDelegate _createInstance; - private readonly string _name; - private readonly string _help; - private readonly StringSequence _instanceLabelNames; - private readonly LabelSequence _staticLabels; - private readonly TConfiguration _configuration; - // This is already resolved to inherit from any parent or defaults. - private readonly ExemplarBehavior _exemplarBehavior; - - public string Name => _name; - public StringSequence InstanceLabelNames => _instanceLabelNames; - public LabelSequence StaticLabels => _staticLabels; - - public CollectorInitializer(CreateInstanceDelegate createInstance, string name, string help, StringSequence instanceLabelNames, LabelSequence staticLabels, TConfiguration configuration, ExemplarBehavior exemplarBehavior) - { - _createInstance = createInstance; - _name = name; - _help = help; - _instanceLabelNames = instanceLabelNames; - _staticLabels = staticLabels; - _configuration = configuration; - _exemplarBehavior = exemplarBehavior; - } - - public TCollector CreateInstance() => _createInstance(_name, _help, _instanceLabelNames, _staticLabels, _configuration, _exemplarBehavior); - - public delegate TCollector CreateInstanceDelegate(string name, string help, StringSequence instanceLabelNames, LabelSequence staticLabels, TConfiguration configuration, ExemplarBehavior exemplarBehavior); - } + where TConfiguration : MetricConfiguration; /// /// Adds a collector to the registry, returning an existing instance if one with a matching name was already registered. /// - internal TCollector GetOrAdd(in CollectorInitializer initializer) + internal TCollector GetOrAdd(string name, string help, in StringSequence instanceLabelNames, in LabelSequence staticLabels, TConfiguration configuration, ExemplarBehavior exemplarBehavior, in CollectorInitializer initializer) where TCollector : Collector where TConfiguration : MetricConfiguration { - // Should we optimize for the case where the family/collector is already registered? It is unlikely to be very common. However, we still should because: - // 1) In scenarios where collector re-registration is common, it could be an expensive persistent cost in terms of allocations. - // 2) In scenarios where collector re-registration is not common, a tiny compute overhead is unlikely to be significant in the big picture, as it only happens once. + var family = GetOrAddCollectorFamily(name); - var family = GetOrAddCollectorFamily(initializer); + var collectorIdentity = new CollectorIdentity(instanceLabelNames, staticLabels); - var collectorIdentity = new CollectorIdentity(initializer.InstanceLabelNames, initializer.StaticLabels); - - return (TCollector)family.GetOrAdd(collectorIdentity, initializer); + return (TCollector)family.GetOrAdd(collectorIdentity, name, help, configuration, exemplarBehavior, initializer); } - private CollectorFamily GetOrAddCollectorFamily(in CollectorInitializer initializer) + private CollectorFamily GetOrAddCollectorFamily(string finalName) where TCollector : Collector - where TConfiguration : MetricConfiguration { static CollectorFamily ValidateFamily(CollectorFamily candidate) { @@ -226,7 +190,7 @@ static CollectorFamily ValidateFamily(CollectorFamily candidate) try { - if (_families.TryGetValue(initializer.Name, out var existing)) + if (_families.TryGetValue(finalName, out var existing)) return ValidateFamily(existing); } finally @@ -239,11 +203,11 @@ static CollectorFamily ValidateFamily(CollectorFamily candidate) try { - if (_families.TryGetValue(initializer.Name, out var existing)) + if (_families.TryGetValue(finalName, out var existing)) return ValidateFamily(existing); var newFamily = new CollectorFamily(typeof(TCollector)); - _families.Add(initializer.Name, newFamily); + _families.Add(finalName, newFamily); return newFamily; } finally diff --git a/Prometheus/MetricFactory.cs b/Prometheus/MetricFactory.cs index b42987e7..db330d63 100644 --- a/Prometheus/MetricFactory.cs +++ b/Prometheus/MetricFactory.cs @@ -1,4 +1,6 @@ -namespace Prometheus; +using static Prometheus.CollectorRegistry; + +namespace Prometheus; /// /// Adds metrics to a registry. @@ -19,7 +21,7 @@ internal MetricFactory(CollectorRegistry registry) : this(registry, LabelSequenc { } - internal MetricFactory(CollectorRegistry registry, LabelSequence withLabels) + internal MetricFactory(CollectorRegistry registry, in LabelSequence withLabels) { _registry = registry ?? throw new ArgumentNullException(nameof(registry)); _factoryLabels = withLabels; @@ -85,55 +87,48 @@ public Summary CreateSummary(string name, string help, string[] labelNames, Summ internal Counter CreateCounter(string name, string help, StringSequence instanceLabelNames, CounterConfiguration? configuration) { - static Counter CreateInstance(string finalName, string finalHelp, StringSequence finalInstanceLabelNames, LabelSequence finalStaticLabels, CounterConfiguration finalConfiguration, ExemplarBehavior finalExemplarBehavior) - { - return new Counter(finalName, finalHelp, finalInstanceLabelNames, finalStaticLabels, finalConfiguration.SuppressInitialValue, finalExemplarBehavior); - } - var exemplarBehavior = configuration?.ExemplarBehavior ?? ExemplarBehavior ?? ExemplarBehavior.Default; - var initializer = new CollectorRegistry.CollectorInitializer(CreateInstance, name, help, instanceLabelNames, _staticLabelsLazy.Value, configuration ?? CounterConfiguration.Default, exemplarBehavior); - return _registry.GetOrAdd(initializer); + + return _registry.GetOrAdd(name, help, instanceLabelNames, _staticLabelsLazy.Value, configuration ?? CounterConfiguration.Default, exemplarBehavior, _createCounterInstanceFunc); } internal Gauge CreateGauge(string name, string help, StringSequence instanceLabelNames, GaugeConfiguration? configuration) { - static Gauge CreateInstance(string finalName, string finalHelp, StringSequence finalInstanceLabelNames, LabelSequence finalStaticLabels, GaugeConfiguration finalConfiguration, ExemplarBehavior finalExemplarBehavior) - { - return new Gauge(finalName, finalHelp, finalInstanceLabelNames, finalStaticLabels, finalConfiguration.SuppressInitialValue, finalExemplarBehavior); - } - - // Note: exemplars are not supported for gauges. We just pass it along here to avoid forked APIs downsream. + // Note: exemplars are not supported for gauges. We just pass it along here to avoid forked APIs downstream. var exemplarBehavior = ExemplarBehavior ?? ExemplarBehavior.Default; - var initializer = new CollectorRegistry.CollectorInitializer(CreateInstance, name, help, instanceLabelNames, _staticLabelsLazy.Value, configuration ?? GaugeConfiguration.Default, exemplarBehavior); - return _registry.GetOrAdd(initializer); + + return _registry.GetOrAdd(name, help, instanceLabelNames, _staticLabelsLazy.Value, configuration ?? GaugeConfiguration.Default, exemplarBehavior, _createGaugeInstanceFunc); } internal Histogram CreateHistogram(string name, string help, StringSequence instanceLabelNames, HistogramConfiguration? configuration) { - static Histogram CreateInstance(string finalName, string finalHelp, StringSequence finalInstanceLabelNames, LabelSequence finalStaticLabels, HistogramConfiguration finalConfiguration, ExemplarBehavior finalExemplarBehavior) - { - return new Histogram(finalName, finalHelp, finalInstanceLabelNames, finalStaticLabels, finalConfiguration.SuppressInitialValue, finalConfiguration.Buckets, finalExemplarBehavior); - } - var exemplarBehavior = configuration?.ExemplarBehavior ?? ExemplarBehavior ?? ExemplarBehavior.Default; - var initializer = new CollectorRegistry.CollectorInitializer(CreateInstance, name, help, instanceLabelNames, _staticLabelsLazy.Value, configuration ?? HistogramConfiguration.Default, exemplarBehavior); - return _registry.GetOrAdd(initializer); + + return _registry.GetOrAdd(name, help, instanceLabelNames, _staticLabelsLazy.Value, configuration ?? HistogramConfiguration.Default, exemplarBehavior, _createHistogramInstanceFunc); } internal Summary CreateSummary(string name, string help, StringSequence instanceLabelNames, SummaryConfiguration? configuration) { - static Summary CreateInstance(string finalName, string finalHelp, StringSequence finalInstanceLabelNames, LabelSequence finalStaticLabels, SummaryConfiguration finalConfiguration, ExemplarBehavior finalExemplarBehavior) - { - return new Summary(finalName, finalHelp, finalInstanceLabelNames, finalStaticLabels, finalExemplarBehavior, finalConfiguration.SuppressInitialValue, - finalConfiguration.Objectives, finalConfiguration.MaxAge, finalConfiguration.AgeBuckets, finalConfiguration.BufferSize); - } - - // Note: exemplars are not supported for summaries. We just pass it along here to avoid forked APIs downsream. + // Note: exemplars are not supported for summaries. We just pass it along here to avoid forked APIs downstream. var exemplarBehavior = ExemplarBehavior ?? ExemplarBehavior.Default; - var initializer = new CollectorRegistry.CollectorInitializer(CreateInstance, name, help, instanceLabelNames, _staticLabelsLazy.Value, configuration ?? SummaryConfiguration.Default, exemplarBehavior); - return _registry.GetOrAdd(initializer); + + return _registry.GetOrAdd(name, help, instanceLabelNames, _staticLabelsLazy.Value, configuration ?? SummaryConfiguration.Default, exemplarBehavior, _createSummaryInstanceFunc); } + private static Counter CreateCounterInstance(string Name, string Help, in StringSequence InstanceLabelNames, in LabelSequence StaticLabels, CounterConfiguration Configuration, ExemplarBehavior ExemplarBehavior) => new(Name, Help, InstanceLabelNames, StaticLabels, Configuration.SuppressInitialValue, ExemplarBehavior); + + private static Gauge CreateGaugeInstance(string Name, string Help, in StringSequence InstanceLabelNames, in LabelSequence StaticLabels, GaugeConfiguration Configuration, ExemplarBehavior ExemplarBehavior) => new(Name, Help, InstanceLabelNames, StaticLabels, Configuration.SuppressInitialValue, ExemplarBehavior); + + private static Histogram CreateHistogramInstance(string Name, string Help, in StringSequence InstanceLabelNames, in LabelSequence StaticLabels, HistogramConfiguration Configuration, ExemplarBehavior ExemplarBehavior) => new(Name, Help, InstanceLabelNames, StaticLabels, Configuration.SuppressInitialValue, Configuration.Buckets, ExemplarBehavior); + + private static Summary CreateSummaryInstance(string name, string help, in StringSequence instanceLabelNames, in LabelSequence staticLabels, SummaryConfiguration configuration, ExemplarBehavior exemplarBehavior) => new(name, help, instanceLabelNames, staticLabels, exemplarBehavior, configuration.SuppressInitialValue, + configuration.Objectives, configuration.MaxAge, configuration.AgeBuckets, configuration.BufferSize); + + private static readonly CollectorInitializer _createCounterInstanceFunc = CreateCounterInstance; + private static readonly CollectorInitializer _createGaugeInstanceFunc = CreateGaugeInstance; + private static readonly CollectorInitializer _createHistogramInstanceFunc = CreateHistogramInstance; + private static readonly CollectorInitializer _createSummaryInstanceFunc = CreateSummaryInstance; + /// /// Counters only increase in value and reset to zero when the process restarts. /// From 749650b09196fa1fa7ad0c46785c943593d6397f Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 30 Nov 2023 14:25:47 +0200 Subject: [PATCH 186/230] Reduce duplication of data in Summary instances, reusing fields in parent instead --- Prometheus/Summary.cs | 524 +++++++++++++++++++++--------------------- 1 file changed, 261 insertions(+), 263 deletions(-) diff --git a/Prometheus/Summary.cs b/Prometheus/Summary.cs index de607986..d576c46e 100644 --- a/Prometheus/Summary.cs +++ b/Prometheus/Summary.cs @@ -2,337 +2,335 @@ using System.Runtime.CompilerServices; using Prometheus.SummaryImpl; -namespace Prometheus +namespace Prometheus; + +public sealed class Summary : Collector, ISummary { - public sealed class Summary : Collector, ISummary + // Label that defines the quantile in a summary. + private const string QuantileLabel = "quantile"; + + /// + /// Client library guidelines say that the summary should default to not measuring quantiles. + /// https://prometheus.io/docs/instrumenting/writing_clientlibs/#summary + /// + internal static readonly QuantileEpsilonPair[] DefObjectivesArray = new QuantileEpsilonPair[0]; + + // Default duration for which observations stay relevant + public static readonly TimeSpan DefMaxAge = TimeSpan.FromMinutes(10); + + // Default number of buckets used to calculate the age of observations + public static readonly int DefAgeBuckets = 5; + + // Standard buffer size for collecting Summary observations + public static readonly int DefBufCap = 500; + + // Objectives defines the quantile rank estimates with their respective + // absolute error. If Objectives[q] = e, then the value reported + // for q will be the φ-quantile value for some φ between q-e and q+e. + // The default value is DefObjectives. + private readonly IReadOnlyList _objectives; + + // MaxAge defines the duration for which an observation stays relevant + // for the summary. Must be positive. The default value is DefMaxAge. + private readonly TimeSpan _maxAge; + + // AgeBuckets is the number of buckets used to exclude observations that + // are older than MaxAge from the summary. A higher number has a + // resource penalty, so only increase it if the higher resolution is + // really required. For very high observation rates, you might want to + // reduce the number of age buckets. With only one age bucket, you will + // effectively see a complete reset of the summary each time MaxAge has + // passed. The default value is DefAgeBuckets. + private readonly int _ageBuckets; + + // BufCap defines the default sample stream buffer size. The default + // value of DefBufCap should suffice for most uses. If there is a need + // to increase the value, a multiple of 500 is recommended (because that + // is the internal buffer size of the underlying package + // "github.com/bmizerany/perks/quantile"). + private readonly int _bufCap; + + private readonly double[] _sortedObjectives; + + // These labels go together with the objectives, so we do not need to allocate them for every child. + private readonly CanonicalLabel[] _quantileLabels; + + private static readonly byte[] QuantileLabelName = PrometheusConstants.ExportEncoding.GetBytes("quantile"); + + internal Summary( + string name, + string help, + StringSequence instanceLabelNames, + LabelSequence staticLabels, + ExemplarBehavior exemplarBehavior, + bool suppressInitialValue = false, + IReadOnlyList? objectives = null, + TimeSpan? maxAge = null, + int? ageBuckets = null, + int? bufCap = null) + : base(name, help, instanceLabelNames, staticLabels, suppressInitialValue, exemplarBehavior) { - // Label that defines the quantile in a summary. - private const string QuantileLabel = "quantile"; + _objectives = objectives ?? DefObjectivesArray; + _maxAge = maxAge ?? DefMaxAge; + _ageBuckets = ageBuckets ?? DefAgeBuckets; + _bufCap = bufCap ?? DefBufCap; - /// - /// Client library guidelines say that the summary should default to not measuring quantiles. - /// https://prometheus.io/docs/instrumenting/writing_clientlibs/#summary - /// - internal static readonly QuantileEpsilonPair[] DefObjectivesArray = new QuantileEpsilonPair[0]; - - // Default duration for which observations stay relevant - public static readonly TimeSpan DefMaxAge = TimeSpan.FromMinutes(10); - - // Default number of buckets used to calculate the age of observations - public static readonly int DefAgeBuckets = 5; - - // Standard buffer size for collecting Summary observations - public static readonly int DefBufCap = 500; - - private readonly IReadOnlyList _objectives; - private readonly TimeSpan _maxAge; - private readonly int _ageBuckets; - private readonly int _bufCap; - - internal Summary( - string name, - string help, - StringSequence instanceLabelNames, - LabelSequence staticLabels, - ExemplarBehavior exemplarBehavior, - bool suppressInitialValue = false, - IReadOnlyList? objectives = null, - TimeSpan? maxAge = null, - int? ageBuckets = null, - int? bufCap = null) - : base(name, help, instanceLabelNames, staticLabels, suppressInitialValue, exemplarBehavior) - { - _objectives = objectives ?? DefObjectivesArray; - _maxAge = maxAge ?? DefMaxAge; - _ageBuckets = ageBuckets ?? DefAgeBuckets; - _bufCap = bufCap ?? DefBufCap; + if (_objectives.Count == 0) + _objectives = DefObjectivesArray; - if (_objectives.Count == 0) - _objectives = DefObjectivesArray; + if (_maxAge < TimeSpan.Zero) + throw new ArgumentException($"Illegal max age {_maxAge}"); - if (_maxAge < TimeSpan.Zero) - throw new ArgumentException($"Illegal max age {_maxAge}"); + if (_ageBuckets == 0) + _ageBuckets = DefAgeBuckets; - if (_ageBuckets == 0) - _ageBuckets = DefAgeBuckets; + if (_bufCap == 0) + _bufCap = DefBufCap; - if (_bufCap == 0) - _bufCap = DefBufCap; + if (instanceLabelNames.Contains(QuantileLabel)) + throw new ArgumentException($"{QuantileLabel} is a reserved label name"); - if (instanceLabelNames.Contains(QuantileLabel)) - throw new ArgumentException($"{QuantileLabel} is a reserved label name"); - } + _sortedObjectives = new double[_objectives.Count]; + _quantileLabels = new CanonicalLabel[_objectives.Count]; - private protected override Child NewChild(LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish, ExemplarBehavior exemplarBehavior) + for (var i = 0; i < _objectives.Count; i++) { - return new Child(this, instanceLabels, flattenedLabels, publish, exemplarBehavior); + _sortedObjectives[i] = _objectives[i].Quantile; + _quantileLabels[i] = TextSerializer.EncodeValueAsCanonicalLabel(QuantileLabelName, _objectives[i].Quantile); } - internal override MetricType Type => MetricType.Summary; + Array.Sort(_sortedObjectives); + } - public sealed class Child : ChildBase, ISummary - { - internal Child(Summary parent, LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish, ExemplarBehavior exemplarBehavior) - : base(parent, instanceLabels, flattenedLabels, publish, exemplarBehavior) - { - _objectives = parent._objectives; - _maxAge = parent._maxAge; - _ageBuckets = parent._ageBuckets; - _bufCap = parent._bufCap; - - _sortedObjectives = new double[_objectives.Count]; - _hotBuf = new SampleBuffer(_bufCap); - _coldBuf = new SampleBuffer(_bufCap); - _streamDuration = new TimeSpan(_maxAge.Ticks / _ageBuckets); - _headStreamExpTime = DateTime.UtcNow.Add(_streamDuration); - _hotBufExpTime = _headStreamExpTime; - - _streams = new QuantileStream[_ageBuckets]; - for (var i = 0; i < _ageBuckets; i++) - { - _streams[i] = QuantileStream.NewTargeted(_objectives); - } + private protected override Child NewChild(LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish, ExemplarBehavior exemplarBehavior) + { + return new Child(this, instanceLabels, flattenedLabels, publish, exemplarBehavior); + } - _headStream = _streams[0]; + internal override MetricType Type => MetricType.Summary; - _quantileLabels = new CanonicalLabel[_objectives.Count]; - for (var i = 0; i < _objectives.Count; i++) - { - _sortedObjectives[i] = _objectives[i].Quantile; - _quantileLabels[i] = TextSerializer.EncodeValueAsCanonicalLabel( - QuantileLabelName, _objectives[i].Quantile); - } + public sealed class Child : ChildBase, ISummary + { + internal Child(Summary parent, LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish, ExemplarBehavior exemplarBehavior) + : base(parent, instanceLabels, flattenedLabels, publish, exemplarBehavior) + { + _parent = parent; - Array.Sort(_sortedObjectives); + _hotBuf = new SampleBuffer(_parent._bufCap); + _coldBuf = new SampleBuffer(_parent._bufCap); + _streamDuration = new TimeSpan(_parent._maxAge.Ticks / _parent._ageBuckets); + _headStreamExpTime = DateTime.UtcNow.Add(_streamDuration); + _hotBufExpTime = _headStreamExpTime; + + _streams = new QuantileStream[_parent._ageBuckets]; + for (var i = 0; i < _parent._ageBuckets; i++) + { + _streams[i] = QuantileStream.NewTargeted(_parent._objectives); } - private readonly CanonicalLabel[] _quantileLabels; - private static readonly byte[] SumSuffix = PrometheusConstants.ExportEncoding.GetBytes("sum"); - private static readonly byte[] CountSuffix = PrometheusConstants.ExportEncoding.GetBytes("count"); - private static readonly byte[] QuantileLabelName = PrometheusConstants.ExportEncoding.GetBytes("quantile"); + _headStream = _streams[0]; + } + + private readonly Summary _parent; + + private static readonly byte[] SumSuffix = PrometheusConstants.ExportEncoding.GetBytes("sum"); + private static readonly byte[] CountSuffix = PrometheusConstants.ExportEncoding.GetBytes("count"); #if NET - [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] + [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] #endif - private protected override async ValueTask CollectAndSerializeImplAsync(IMetricsSerializer serializer, - CancellationToken cancel) - { - // We output sum. - // We output count. - // We output quantiles. + private protected override async ValueTask CollectAndSerializeImplAsync(IMetricsSerializer serializer, + CancellationToken cancel) + { + // We output sum. + // We output count. + // We output quantiles. - var now = DateTime.UtcNow; + var now = DateTime.UtcNow; - long count; - double sum; + long count; + double sum; - var values = ArrayPool<(double quantile, double value)>.Shared.Rent(_objectives.Count); - var valuesIndex = 0; + var values = ArrayPool<(double quantile, double value)>.Shared.Rent(_parent._objectives.Count); + var valuesIndex = 0; - try + try + { + lock (_bufLock) { - lock (_bufLock) + lock (_lock) { - lock (_lock) - { - // Swap bufs even if hotBuf is empty to set new hotBufExpTime. - SwapBufs(now); - FlushColdBuf(); + // Swap bufs even if hotBuf is empty to set new hotBufExpTime. + SwapBufs(now); + FlushColdBuf(); - count = _count; - sum = _sum; + count = _count; + sum = _sum; - for (var i = 0; i < _sortedObjectives.Length; i++) - { - var quantile = _sortedObjectives[i]; - var value = _headStream.Count == 0 ? double.NaN : _headStream.Query(quantile); + for (var i = 0; i < _parent._sortedObjectives.Length; i++) + { + var quantile = _parent._sortedObjectives[i]; + var value = _headStream.Count == 0 ? double.NaN : _headStream.Query(quantile); - values[valuesIndex++] = (quantile, value); - } + values[valuesIndex++] = (quantile, value); } } + } + await serializer.WriteMetricPointAsync( + Parent.NameBytes, + FlattenedLabelsBytes, + CanonicalLabel.Empty, + cancel, + sum, + ObservedExemplar.Empty, + suffix: SumSuffix); + await serializer.WriteMetricPointAsync( + Parent.NameBytes, + FlattenedLabelsBytes, + CanonicalLabel.Empty, + cancel, + count, + ObservedExemplar.Empty, + suffix: CountSuffix); + + for (var i = 0; i < _parent._objectives.Count; i++) + { await serializer.WriteMetricPointAsync( Parent.NameBytes, FlattenedLabelsBytes, - CanonicalLabel.Empty, - cancel, - sum, - ObservedExemplar.Empty, - suffix: SumSuffix); - await serializer.WriteMetricPointAsync( - Parent.NameBytes, - FlattenedLabelsBytes, - CanonicalLabel.Empty, + _parent._quantileLabels[i], cancel, - count, - ObservedExemplar.Empty, - suffix: CountSuffix); - - for (var i = 0; i < _objectives.Count; i++) - { - await serializer.WriteMetricPointAsync( - Parent.NameBytes, - FlattenedLabelsBytes, - _quantileLabels[i], - cancel, - values[i].value, - ObservedExemplar.Empty); - } - } - finally - { - ArrayPool<(double quantile, double value)>.Shared.Return(values); + values[i].value, + ObservedExemplar.Empty); } } - - // Objectives defines the quantile rank estimates with their respective - // absolute error. If Objectives[q] = e, then the value reported - // for q will be the φ-quantile value for some φ between q-e and q+e. - // The default value is DefObjectives. - private IReadOnlyList _objectives = new List(); - private double[] _sortedObjectives; - private double _sum; - private long _count; - private SampleBuffer _hotBuf; - private SampleBuffer _coldBuf; - private QuantileStream[] _streams; - private TimeSpan _streamDuration; - private QuantileStream _headStream; - private int _headStreamIdx; - private DateTime _headStreamExpTime; - private DateTime _hotBufExpTime; - - // Protects hotBuf and hotBufExpTime. - private readonly object _bufLock = new object(); - - // Protects every other moving part. - // Lock bufMtx before mtx if both are needed. - private readonly object _lock = new object(); - - // MaxAge defines the duration for which an observation stays relevant - // for the summary. Must be positive. The default value is DefMaxAge. - private TimeSpan _maxAge; - - // AgeBuckets is the number of buckets used to exclude observations that - // are older than MaxAge from the summary. A higher number has a - // resource penalty, so only increase it if the higher resolution is - // really required. For very high observation rates, you might want to - // reduce the number of age buckets. With only one age bucket, you will - // effectively see a complete reset of the summary each time MaxAge has - // passed. The default value is DefAgeBuckets. - private int _ageBuckets; - - // BufCap defines the default sample stream buffer size. The default - // value of DefBufCap should suffice for most uses. If there is a need - // to increase the value, a multiple of 500 is recommended (because that - // is the internal buffer size of the underlying package - // "github.com/bmizerany/perks/quantile"). - private int _bufCap; - - public void Observe(double val) + finally { - Observe(val, DateTime.UtcNow); + ArrayPool<(double quantile, double value)>.Shared.Return(values); } + } - /// - /// For unit tests only - /// - internal void Observe(double val, DateTime now) - { - if (double.IsNaN(val)) - return; + private double _sum; + private long _count; + private SampleBuffer _hotBuf; + private SampleBuffer _coldBuf; + private readonly QuantileStream[] _streams; + private readonly TimeSpan _streamDuration; + private QuantileStream _headStream; + private int _headStreamIdx; + private DateTime _headStreamExpTime; + private DateTime _hotBufExpTime; - lock (_bufLock) - { - if (now > _hotBufExpTime) - Flush(now); + // Protects hotBuf and hotBufExpTime. + private readonly object _bufLock = new(); - _hotBuf.Append(val); + // Protects every other moving part. + // Lock bufMtx before mtx if both are needed. + private readonly object _lock = new(); - if (_hotBuf.IsFull) - Flush(now); - } + public void Observe(double val) + { + Observe(val, DateTime.UtcNow); + } - Publish(); - } + /// + /// For unit tests only + /// + internal void Observe(double val, DateTime now) + { + if (double.IsNaN(val)) + return; - // Flush needs bufMtx locked. - private void Flush(DateTime now) + lock (_bufLock) { - lock (_lock) - { - SwapBufs(now); + if (now > _hotBufExpTime) + Flush(now); - // Go version flushes on a separate goroutine, but doing this on another - // thread actually makes the benchmark tests slower in .net - FlushColdBuf(); - } + _hotBuf.Append(val); + + if (_hotBuf.IsFull) + Flush(now); } - // SwapBufs needs mtx AND bufMtx locked, coldBuf must be empty. - private void SwapBufs(DateTime now) - { - if (!_coldBuf.IsEmpty) - throw new InvalidOperationException("coldBuf is not empty"); + Publish(); + } - var temp = _hotBuf; - _hotBuf = _coldBuf; - _coldBuf = temp; + // Flush needs bufMtx locked. + private void Flush(DateTime now) + { + lock (_lock) + { + SwapBufs(now); - // hotBuf is now empty and gets new expiration set. - while (now > _hotBufExpTime) - { - _hotBufExpTime = _hotBufExpTime.Add(_streamDuration); - } + // Go version flushes on a separate goroutine, but doing this on another + // thread actually makes the benchmark tests slower in .net + FlushColdBuf(); } + } + + // SwapBufs needs mtx AND bufMtx locked, coldBuf must be empty. + private void SwapBufs(DateTime now) + { + if (!_coldBuf.IsEmpty) + throw new InvalidOperationException("coldBuf is not empty"); - // FlushColdBuf needs mtx locked. - private void FlushColdBuf() + var temp = _hotBuf; + _hotBuf = _coldBuf; + _coldBuf = temp; + + // hotBuf is now empty and gets new expiration set. + while (now > _hotBufExpTime) { - for (var bufIdx = 0; bufIdx < _coldBuf.Position; bufIdx++) - { - var value = _coldBuf[bufIdx]; + _hotBufExpTime = _hotBufExpTime.Add(_streamDuration); + } + } - for (var streamIdx = 0; streamIdx < _streams.Length; streamIdx++) - { - _streams[streamIdx].Insert(value); - } + // FlushColdBuf needs mtx locked. + private void FlushColdBuf() + { + for (var bufIdx = 0; bufIdx < _coldBuf.Position; bufIdx++) + { + var value = _coldBuf[bufIdx]; - _count++; - _sum += value; + for (var streamIdx = 0; streamIdx < _streams.Length; streamIdx++) + { + _streams[streamIdx].Insert(value); } - _coldBuf.Reset(); - MaybeRotateStreams(); + _count++; + _sum += value; } - // MaybeRotateStreams needs mtx AND bufMtx locked. - private void MaybeRotateStreams() + _coldBuf.Reset(); + MaybeRotateStreams(); + } + + // MaybeRotateStreams needs mtx AND bufMtx locked. + private void MaybeRotateStreams() + { + while (!_hotBufExpTime.Equals(_headStreamExpTime)) { - while (!_hotBufExpTime.Equals(_headStreamExpTime)) - { - _headStream.Reset(); - _headStreamIdx++; + _headStream.Reset(); + _headStreamIdx++; - if (_headStreamIdx >= _streams.Length) - _headStreamIdx = 0; + if (_headStreamIdx >= _streams.Length) + _headStreamIdx = 0; - _headStream = _streams[_headStreamIdx]; - _headStreamExpTime = _headStreamExpTime.Add(_streamDuration); - } + _headStream = _streams[_headStreamIdx]; + _headStreamExpTime = _headStreamExpTime.Add(_streamDuration); } } + } - public void Observe(double val) - { - Unlabelled.Observe(val); - } + public void Observe(double val) + { + Unlabelled.Observe(val); + } - public void Publish() => Unlabelled.Publish(); - public void Unpublish() => Unlabelled.Unpublish(); + public void Publish() => Unlabelled.Publish(); + public void Unpublish() => Unlabelled.Unpublish(); - // count + sum + objectives - internal override int TimeseriesCount => ChildCount * (2 + _objectives.Count); - } + // count + sum + objectives + internal override int TimeseriesCount => ChildCount * (2 + _objectives.Count); } \ No newline at end of file From ba821a052075cee22f9ba94d889e810bae0e1736 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 30 Nov 2023 15:03:25 +0200 Subject: [PATCH 187/230] Minor tidy in Summary, avoid allocations if not needed --- Prometheus/Summary.cs | 56 ++--- Prometheus/SummaryImpl/QuantileStream.cs | 257 ++++++++++------------- Prometheus/SummaryImpl/SampleBuffer.cs | 65 +++--- Prometheus/SummaryImpl/SampleStream.cs | 163 +++++++------- Tests.NetCore/QuantileStreamTests.cs | 18 -- 5 files changed, 258 insertions(+), 301 deletions(-) diff --git a/Prometheus/Summary.cs b/Prometheus/Summary.cs index d576c46e..64450c0b 100644 --- a/Prometheus/Summary.cs +++ b/Prometheus/Summary.cs @@ -90,16 +90,24 @@ internal Summary( if (instanceLabelNames.Contains(QuantileLabel)) throw new ArgumentException($"{QuantileLabel} is a reserved label name"); - _sortedObjectives = new double[_objectives.Count]; - _quantileLabels = new CanonicalLabel[_objectives.Count]; - - for (var i = 0; i < _objectives.Count; i++) + if (_objectives.Count == 0) { - _sortedObjectives[i] = _objectives[i].Quantile; - _quantileLabels[i] = TextSerializer.EncodeValueAsCanonicalLabel(QuantileLabelName, _objectives[i].Quantile); + _sortedObjectives = []; + _quantileLabels = []; } + else + { + _sortedObjectives = new double[_objectives.Count]; + _quantileLabels = new CanonicalLabel[_objectives.Count]; - Array.Sort(_sortedObjectives); + for (var i = 0; i < _objectives.Count; i++) + { + _sortedObjectives[i] = _objectives[i].Quantile; + _quantileLabels[i] = TextSerializer.EncodeValueAsCanonicalLabel(QuantileLabelName, _objectives[i].Quantile); + } + + Array.Sort(_sortedObjectives); + } } private protected override Child NewChild(LabelSequence instanceLabels, LabelSequence flattenedLabels, bool publish, ExemplarBehavior exemplarBehavior) @@ -119,8 +127,8 @@ internal Child(Summary parent, LabelSequence instanceLabels, LabelSequence flatt _hotBuf = new SampleBuffer(_parent._bufCap); _coldBuf = new SampleBuffer(_parent._bufCap); _streamDuration = new TimeSpan(_parent._maxAge.Ticks / _parent._ageBuckets); - _headStreamExpTime = DateTime.UtcNow.Add(_streamDuration); - _hotBufExpTime = _headStreamExpTime; + _headStreamExpUnixtimeSeconds = LowGranularityTimeSource.GetSecondsFromUnixEpoch() + _streamDuration.TotalSeconds; + _hotBufExpUnixtimeSeconds = _headStreamExpUnixtimeSeconds; _streams = new QuantileStream[_parent._ageBuckets]; for (var i = 0; i < _parent._ageBuckets; i++) @@ -146,7 +154,7 @@ private protected override async ValueTask CollectAndSerializeImplAsync(IMetrics // We output count. // We output quantiles. - var now = DateTime.UtcNow; + var now = LowGranularityTimeSource.GetSecondsFromUnixEpoch(); long count; double sum; @@ -219,8 +227,8 @@ await serializer.WriteMetricPointAsync( private readonly TimeSpan _streamDuration; private QuantileStream _headStream; private int _headStreamIdx; - private DateTime _headStreamExpTime; - private DateTime _hotBufExpTime; + private double _headStreamExpUnixtimeSeconds; + private double _hotBufExpUnixtimeSeconds; // Protects hotBuf and hotBufExpTime. private readonly object _bufLock = new(); @@ -231,37 +239,37 @@ await serializer.WriteMetricPointAsync( public void Observe(double val) { - Observe(val, DateTime.UtcNow); + Observe(val, LowGranularityTimeSource.GetSecondsFromUnixEpoch()); } /// /// For unit tests only /// - internal void Observe(double val, DateTime now) + internal void Observe(double val, double nowUnixtimeSeconds) { if (double.IsNaN(val)) return; lock (_bufLock) { - if (now > _hotBufExpTime) - Flush(now); + if (nowUnixtimeSeconds > _hotBufExpUnixtimeSeconds) + Flush(nowUnixtimeSeconds); _hotBuf.Append(val); if (_hotBuf.IsFull) - Flush(now); + Flush(nowUnixtimeSeconds); } Publish(); } // Flush needs bufMtx locked. - private void Flush(DateTime now) + private void Flush(double nowUnixtimeSeconds) { lock (_lock) { - SwapBufs(now); + SwapBufs(nowUnixtimeSeconds); // Go version flushes on a separate goroutine, but doing this on another // thread actually makes the benchmark tests slower in .net @@ -270,7 +278,7 @@ private void Flush(DateTime now) } // SwapBufs needs mtx AND bufMtx locked, coldBuf must be empty. - private void SwapBufs(DateTime now) + private void SwapBufs(double nowUnixtimeSeconds) { if (!_coldBuf.IsEmpty) throw new InvalidOperationException("coldBuf is not empty"); @@ -280,9 +288,9 @@ private void SwapBufs(DateTime now) _coldBuf = temp; // hotBuf is now empty and gets new expiration set. - while (now > _hotBufExpTime) + while (nowUnixtimeSeconds > _hotBufExpUnixtimeSeconds) { - _hotBufExpTime = _hotBufExpTime.Add(_streamDuration); + _hotBufExpUnixtimeSeconds += _streamDuration.TotalSeconds; } } @@ -309,7 +317,7 @@ private void FlushColdBuf() // MaybeRotateStreams needs mtx AND bufMtx locked. private void MaybeRotateStreams() { - while (!_hotBufExpTime.Equals(_headStreamExpTime)) + while (!_hotBufExpUnixtimeSeconds.Equals(_headStreamExpUnixtimeSeconds)) { _headStream.Reset(); _headStreamIdx++; @@ -318,7 +326,7 @@ private void MaybeRotateStreams() _headStreamIdx = 0; _headStream = _streams[_headStreamIdx]; - _headStreamExpTime = _headStreamExpTime.Add(_streamDuration); + _headStreamExpUnixtimeSeconds += _streamDuration.TotalSeconds; } } } diff --git a/Prometheus/SummaryImpl/QuantileStream.cs b/Prometheus/SummaryImpl/QuantileStream.cs index 0f34637e..bb34f7bf 100644 --- a/Prometheus/SummaryImpl/QuantileStream.cs +++ b/Prometheus/SummaryImpl/QuantileStream.cs @@ -1,175 +1,144 @@ -namespace Prometheus.SummaryImpl +namespace Prometheus.SummaryImpl; + +// Ported from https://github.com/beorn7/perks/blob/master/quantile/stream.go + +// Package quantile computes approximate quantiles over an unbounded data +// stream within low memory and CPU bounds. +// +// A small amount of accuracy is traded to achieve the above properties. +// +// Multiple streams can be merged before calling Query to generate a single set +// of results. This is meaningful when the streams represent the same type of +// data. See Merge and Samples. +// +// For more detailed information about the algorithm used, see: +// +// Effective Computation of Biased Quantiles over Data Streams +// +// http://www.cs.rutgers.edu/~muthu/bquant.pdf + +internal delegate double Invariant(SampleStream stream, double r); + +internal sealed class QuantileStream { - // Ported from https://github.com/beorn7/perks/blob/master/quantile/stream.go + private readonly SampleStream _sampleStream; + private readonly List _samples; + private bool _sorted; - // Package quantile computes approximate quantiles over an unbounded data - // stream within low memory and CPU bounds. - // - // A small amount of accuracy is traded to achieve the above properties. - // - // Multiple streams can be merged before calling Query to generate a single set - // of results. This is meaningful when the streams represent the same type of - // data. See Merge and Samples. - // - // For more detailed information about the algorithm used, see: - // - // Effective Computation of Biased Quantiles over Data Streams - // - // http://www.cs.rutgers.edu/~muthu/bquant.pdf - - internal delegate double Invariant(SampleStream stream, double r); - - internal class QuantileStream + private QuantileStream(SampleStream sampleStream, List samples, bool sorted) { - private readonly SampleStream _sampleStream; - private readonly List _samples; - private bool _sorted; - - private QuantileStream(SampleStream sampleStream, List samples, bool sorted) - { - _sampleStream = sampleStream; - _samples = samples; - _sorted = sorted; - } - - public static QuantileStream NewStream(Invariant invariant) - { - return new QuantileStream(new SampleStream(invariant), new List { Capacity = 500 }, true); - } + _sampleStream = sampleStream; + _samples = samples; + _sorted = sorted; + } - // NewLowBiased returns an initialized Stream for low-biased quantiles - // (e.g. 0.01, 0.1, 0.5) where the needed quantiles are not known a priori, but - // error guarantees can still be given even for the lower ranks of the data - // distribution. - // - // The provided epsilon is a relative error, i.e. the true quantile of a value - // returned by a query is guaranteed to be within (1±Epsilon)*Quantile. - // - // See http://www.cs.rutgers.edu/~muthu/bquant.pdf for time, space, and error - // properties. - public static QuantileStream NewLowBiased(double epsilon) - { - return NewStream((stream, r) => 2 * epsilon * r); - } + public static QuantileStream NewStream(Invariant invariant) + { + return new QuantileStream(new SampleStream(invariant), new List { Capacity = 500 }, true); + } - // NewHighBiased returns an initialized Stream for high-biased quantiles - // (e.g. 0.01, 0.1, 0.5) where the needed quantiles are not known a priori, but - // error guarantees can still be given even for the higher ranks of the data - // distribution. - // - // The provided epsilon is a relative error, i.e. the true quantile of a value - // returned by a query is guaranteed to be within 1-(1±Epsilon)*(1-Quantile). - // - // See http://www.cs.rutgers.edu/~muthu/bquant.pdf for time, space, and error - // properties. - public static QuantileStream NewHighBiased(double epsilon) + // NewTargeted returns an initialized Stream concerned with a particular set of + // quantile values that are supplied a priori. Knowing these a priori reduces + // space and computation time. The targets map maps the desired quantiles to + // their absolute errors, i.e. the true quantile of a value returned by a query + // is guaranteed to be within (Quantile±Epsilon). + // + // See http://www.cs.rutgers.edu/~muthu/bquant.pdf for time, space, and error properties. + public static QuantileStream NewTargeted(IReadOnlyList targets) + { + return NewStream((stream, r) => { - return NewStream((stream, r) => 2 * epsilon * (stream.N - r)); - } + var m = double.MaxValue; - // NewTargeted returns an initialized Stream concerned with a particular set of - // quantile values that are supplied a priori. Knowing these a priori reduces - // space and computation time. The targets map maps the desired quantiles to - // their absolute errors, i.e. the true quantile of a value returned by a query - // is guaranteed to be within (Quantile±Epsilon). - // - // See http://www.cs.rutgers.edu/~muthu/bquant.pdf for time, space, and error properties. - public static QuantileStream NewTargeted(IReadOnlyList targets) - { - return NewStream((stream, r) => + for (var i = 0; i < targets.Count; i++) { - var m = double.MaxValue; - - for (var i = 0; i < targets.Count; i++) - { - var target = targets[i]; + var target = targets[i]; - double f; - if (target.Quantile * stream.N <= r) - f = (2 * target.Epsilon * r) / target.Quantile; - else - f = (2 * target.Epsilon * (stream.N - r)) / (1 - target.Quantile); + double f; + if (target.Quantile * stream.N <= r) + f = (2 * target.Epsilon * r) / target.Quantile; + else + f = (2 * target.Epsilon * (stream.N - r)) / (1 - target.Quantile); - if (f < m) - m = f; - } + if (f < m) + m = f; + } - return m; - }); - } + return m; + }); + } - public void Insert(double value) - { - Insert(new Sample { Value = value, Width = 1 }); - } + public void Insert(double value) + { + Insert(new Sample { Value = value, Width = 1 }); + } - private void Insert(Sample sample) - { - _samples.Add(sample); - _sorted = false; - if (_samples.Count == _samples.Capacity) - Flush(); - } + private void Insert(Sample sample) + { + _samples.Add(sample); + _sorted = false; + if (_samples.Count == _samples.Capacity) + Flush(); + } - private void Flush() - { - MaybeSort(); - _sampleStream.Merge(_samples); - _samples.Clear(); - } + private void Flush() + { + MaybeSort(); + _sampleStream.Merge(_samples); + _samples.Clear(); + } - private void MaybeSort() + private void MaybeSort() + { + if (!_sorted) { - if (!_sorted) - { - _sorted = true; - _samples.Sort(SampleComparison); - } + _sorted = true; + _samples.Sort(SampleComparison); } + } - private static int SampleComparison(Sample lhs, Sample rhs) - { - return lhs.Value.CompareTo(rhs.Value); - } + private static int SampleComparison(Sample lhs, Sample rhs) + { + return lhs.Value.CompareTo(rhs.Value); + } - public void Reset() - { - _sampleStream.Reset(); - _samples.Clear(); - } + public void Reset() + { + _sampleStream.Reset(); + _samples.Clear(); + } - // Count returns the total number of samples observed in the stream since initialization. - public int Count => _samples.Count + _sampleStream.Count; + // Count returns the total number of samples observed in the stream since initialization. + public int Count => _samples.Count + _sampleStream.Count; - public int SamplesCount => _samples.Count; + public int SamplesCount => _samples.Count; - public bool Flushed => _sampleStream.SampleCount > 0; + public bool Flushed => _sampleStream.SampleCount > 0; - // Query returns the computed qth percentiles value. If s was created with - // NewTargeted, and q is not in the set of quantiles provided a priori, Query - // will return an unspecified result. - public double Query(double q) + // Query returns the computed qth percentiles value. If s was created with + // NewTargeted, and q is not in the set of quantiles provided a priori, Query + // will return an unspecified result. + public double Query(double q) + { + if (!Flushed) { - if (!Flushed) - { - // Fast path when there hasn't been enough data for a flush; - // this also yields better accuracy for small sets of data. + // Fast path when there hasn't been enough data for a flush; + // this also yields better accuracy for small sets of data. - var l = _samples.Count; + var l = _samples.Count; - if (l == 0) - return 0; + if (l == 0) + return 0; - var i = (int)(l * q); - if (i > 0) - i -= 1; - - MaybeSort(); - return _samples[i].Value; - } + var i = (int)(l * q); + if (i > 0) + i -= 1; - Flush(); - return _sampleStream.Query(q); + MaybeSort(); + return _samples[i].Value; } + + Flush(); + return _sampleStream.Query(q); } } diff --git a/Prometheus/SummaryImpl/SampleBuffer.cs b/Prometheus/SummaryImpl/SampleBuffer.cs index c1afda5a..61fc531b 100644 --- a/Prometheus/SummaryImpl/SampleBuffer.cs +++ b/Prometheus/SummaryImpl/SampleBuffer.cs @@ -1,46 +1,45 @@ -namespace Prometheus.SummaryImpl +namespace Prometheus.SummaryImpl; + +internal sealed class SampleBuffer { - internal class SampleBuffer - { - private readonly double[] _buffer; + private readonly double[] _buffer; - public SampleBuffer(int capacity) - { - if (capacity <= 0) - throw new ArgumentOutOfRangeException(nameof(capacity), "Must be > 0"); + public SampleBuffer(int capacity) + { + if (capacity <= 0) + throw new ArgumentOutOfRangeException(nameof(capacity), "Must be > 0"); - _buffer = new double[capacity]; - Position = 0; - } + _buffer = new double[capacity]; + Position = 0; + } - public void Append(double value) - { - if (Position >= Capacity) - throw new InvalidOperationException("Buffer is full"); + public void Append(double value) + { + if (Position >= Capacity) + throw new InvalidOperationException("Buffer is full"); - _buffer[Position++] = value; - } + _buffer[Position++] = value; + } - public double this[int index] + public double this[int index] + { + get { - get - { - if (index > Position) - throw new ArgumentOutOfRangeException(nameof(index), "Index is greater than position"); + if (index > Position) + throw new ArgumentOutOfRangeException(nameof(index), "Index is greater than position"); - return _buffer[index]; - } + return _buffer[index]; } + } - public void Reset() - { - Position = 0; - } + public void Reset() + { + Position = 0; + } - public int Position { get; private set; } + public int Position { get; private set; } - public int Capacity => _buffer.Length; - public bool IsFull => Position == Capacity; - public bool IsEmpty => Position == 0; - } + public int Capacity => _buffer.Length; + public bool IsFull => Position == Capacity; + public bool IsEmpty => Position == 0; } diff --git a/Prometheus/SummaryImpl/SampleStream.cs b/Prometheus/SummaryImpl/SampleStream.cs index 98e6377f..08ff2a84 100644 --- a/Prometheus/SummaryImpl/SampleStream.cs +++ b/Prometheus/SummaryImpl/SampleStream.cs @@ -1,113 +1,112 @@ -namespace Prometheus.SummaryImpl +namespace Prometheus.SummaryImpl; + +internal sealed class SampleStream { - internal class SampleStream + public double N; + private readonly List _samples = new List(); + private readonly Invariant _invariant; + + public SampleStream(Invariant invariant) { - public double N; - private readonly List _samples = new List(); - private readonly Invariant _invariant; + _invariant = invariant; + } - public SampleStream(Invariant invariant) - { - _invariant = invariant; - } + public void Merge(List samples) + { + // TODO(beorn7): This tries to merge not only individual samples, but + // whole summaries. The paper doesn't mention merging summaries at + // all. Unittests show that the merging is inaccurate. Find out how to + // do merges properly. - public void Merge(List samples) - { - // TODO(beorn7): This tries to merge not only individual samples, but - // whole summaries. The paper doesn't mention merging summaries at - // all. Unittests show that the merging is inaccurate. Find out how to - // do merges properly. + double r = 0; + var i = 0; - double r = 0; - var i = 0; + for (var sampleIdx = 0; sampleIdx < samples.Count; sampleIdx++) + { + var sample = samples[sampleIdx]; - for (var sampleIdx = 0; sampleIdx < samples.Count; sampleIdx++) + for (; i < _samples.Count; i++) { - var sample = samples[sampleIdx]; + var c = _samples[i]; - for (; i < _samples.Count; i++) + if (c.Value > sample.Value) { - var c = _samples[i]; - - if (c.Value > sample.Value) - { - // Insert at position i - _samples.Insert(i, new Sample { Value = sample.Value, Width = sample.Width, Delta = Math.Max(sample.Delta, Math.Floor(_invariant(this, r)) - 1) }); - i++; - goto inserted; - } - r += c.Width; + // Insert at position i + _samples.Insert(i, new Sample { Value = sample.Value, Width = sample.Width, Delta = Math.Max(sample.Delta, Math.Floor(_invariant(this, r)) - 1) }); + i++; + goto inserted; } - _samples.Add(new Sample { Value = sample.Value, Width = sample.Width, Delta = 0 }); - i++; - - inserted: - N += sample.Width; - r += sample.Width; + r += c.Width; } + _samples.Add(new Sample { Value = sample.Value, Width = sample.Width, Delta = 0 }); + i++; - Compress(); + inserted: + N += sample.Width; + r += sample.Width; } - private void Compress() - { - if (_samples.Count < 2) - return; + Compress(); + } - var x = _samples[_samples.Count - 1]; - var xi = _samples.Count - 1; - var r = N - 1 - x.Width; + private void Compress() + { + if (_samples.Count < 2) + return; - for (var i = _samples.Count - 2; i >= 0; i--) - { - var c = _samples[i]; + var x = _samples[_samples.Count - 1]; + var xi = _samples.Count - 1; + var r = N - 1 - x.Width; - if (c.Width + x.Width + x.Delta <= _invariant(this, r)) - { - x.Width += c.Width; - _samples[xi] = x; - _samples.RemoveAt(i); - xi -= 1; - } - else - { - x = c; - xi = i; - } + for (var i = _samples.Count - 2; i >= 0; i--) + { + var c = _samples[i]; - r -= c.Width; + if (c.Width + x.Width + x.Delta <= _invariant(this, r)) + { + x.Width += c.Width; + _samples[xi] = x; + _samples.RemoveAt(i); + xi -= 1; + } + else + { + x = c; + xi = i; } - } - public void Reset() - { - _samples.Clear(); - N = 0; + r -= c.Width; } + } - public int Count => (int)N; + public void Reset() + { + _samples.Clear(); + N = 0; + } - public double Query(double q) - { - var t = Math.Ceiling(q * N); - t += Math.Ceiling(_invariant(this, t) / 2); - var p = _samples[0]; - double r = 0; + public int Count => (int)N; - for (var i = 1; i < _samples.Count; i++) - { - var c = _samples[i]; - r += p.Width; + public double Query(double q) + { + var t = Math.Ceiling(q * N); + t += Math.Ceiling(_invariant(this, t) / 2); + var p = _samples[0]; + double r = 0; - if (r + c.Width + c.Delta > t) - return p.Value; + for (var i = 1; i < _samples.Count; i++) + { + var c = _samples[i]; + r += p.Width; - p = c; - } + if (r + c.Width + c.Delta > t) + return p.Value; - return p.Value; + p = c; } - public int SampleCount => _samples.Count; + return p.Value; } + + public int SampleCount => _samples.Count; } \ No newline at end of file diff --git a/Tests.NetCore/QuantileStreamTests.cs b/Tests.NetCore/QuantileStreamTests.cs index 42690abe..f28d0144 100644 --- a/Tests.NetCore/QuantileStreamTests.cs +++ b/Tests.NetCore/QuantileStreamTests.cs @@ -32,24 +32,6 @@ public void TestTargetedQuery() VerifyPercsWithAbsoluteEpsilon(a, s); } - [TestMethod] - public void TestLowBiasedQuery() - { - var random = new Random(42); - var s = QuantileStream.NewLowBiased(RelativeEpsilon); - var a = PopulateStream(s, random); - VerifyLowPercsWithRelativeEpsilon(a, s); - } - - [TestMethod] - public void TestHighBiasedQuery() - { - var random = new Random(42); - var s = QuantileStream.NewHighBiased(RelativeEpsilon); - var a = PopulateStream(s, random); - VerifyHighPercsWithRelativeEpsilon(a, s); - } - [TestMethod] public void TestUncompressed() { From 3c79c68da783a10e6f8f9e3f99a9f680587572b0 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 30 Nov 2023 17:15:30 +0200 Subject: [PATCH 188/230] Do not bother benchmarking Summary in typical cases - it is legacy slowness --- Benchmark.NetCore/MeasurementBenchmarks.cs | 3 ++- Benchmark.NetCore/MetricCreationBenchmarks.cs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Benchmark.NetCore/MeasurementBenchmarks.cs b/Benchmark.NetCore/MeasurementBenchmarks.cs index f13945f6..e9db3e3f 100644 --- a/Benchmark.NetCore/MeasurementBenchmarks.cs +++ b/Benchmark.NetCore/MeasurementBenchmarks.cs @@ -165,7 +165,8 @@ public void WideHistogram() } } - [Benchmark] + // Disabled because it is slow and Summary is a legacy metric type that is not recommended for new usage. + //[Benchmark] public void Summary() { for (var i = 0; i < MeasurementCount; i++) diff --git a/Benchmark.NetCore/MetricCreationBenchmarks.cs b/Benchmark.NetCore/MetricCreationBenchmarks.cs index 40ba1ee2..68d3de66 100644 --- a/Benchmark.NetCore/MetricCreationBenchmarks.cs +++ b/Benchmark.NetCore/MetricCreationBenchmarks.cs @@ -108,7 +108,8 @@ public void Gauge() } } - [Benchmark] + // Disabled because it is slow and Summary is a legacy metric type that is not recommended for new usage. + //[Benchmark] public void Summary() { for (var dupe = 0; dupe < DuplicateCount; dupe++) From 5d9a9c80e8db66a9a43c284b602a158200b60439 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 30 Nov 2023 17:22:31 +0200 Subject: [PATCH 189/230] "in" seems to help a little here --- Benchmark.NetCore/MeterAdapterBenchmarks.cs | 1 + Prometheus/MeterAdapter.cs | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Benchmark.NetCore/MeterAdapterBenchmarks.cs b/Benchmark.NetCore/MeterAdapterBenchmarks.cs index f28d9933..b3abf76f 100644 --- a/Benchmark.NetCore/MeterAdapterBenchmarks.cs +++ b/Benchmark.NetCore/MeterAdapterBenchmarks.cs @@ -8,6 +8,7 @@ namespace Benchmark.NetCore; /// Equivalent of MeasurementBenchmarks, except we publish the data via .NET Meters API and convert via MeterAdapter. /// [MemoryDiagnoser] +//[EventPipeProfiler(BenchmarkDotNet.Diagnosers.EventPipeProfile.GcVerbose)] public class MeterAdapterBenchmarks { [Params(100_000)] diff --git a/Prometheus/MeterAdapter.cs b/Prometheus/MeterAdapter.cs index 0dc74da5..306ca7b4 100644 --- a/Prometheus/MeterAdapter.cs +++ b/Prometheus/MeterAdapter.cs @@ -236,10 +236,10 @@ private sealed class MetricContext( private readonly Dictionary> _histogramCache = new(); private readonly ReaderWriterLockSlim _histogramCacheLock = new(); - private MetricContext GetOrCreateGaugeContext(Instrument instrument, ReadOnlySpan> tags) + private MetricContext GetOrCreateGaugeContext(Instrument instrument, in ReadOnlySpan> tags) => GetOrCreateMetricContext(instrument, tags, _createGaugeFunc, _gaugeCacheLock, _gaugeCache); - private MetricContext GetOrCreateHistogramContext(Instrument instrument, ReadOnlySpan> tags) + private MetricContext GetOrCreateHistogramContext(Instrument instrument, in ReadOnlySpan> tags) => GetOrCreateMetricContext(instrument, tags, _createHistogramFunc, _histogramCacheLock, _histogramCache); private IManagedLifetimeMetricHandle CreateGauge(Instrument instrument, string name, string help, string[] labelNames) @@ -256,7 +256,7 @@ private IManagedLifetimeMetricHandle CreateHistogram(Instrument inst private MetricContext GetOrCreateMetricContext( Instrument instrument, - ReadOnlySpan> tags, + in ReadOnlySpan> tags, Func> metricFactory, ReaderWriterLockSlim cacheLock, Dictionary> cache) @@ -299,7 +299,7 @@ private MetricContext GetOrCreateMetricContext private MetricContext CreateMetricContext( Instrument instrument, - ReadOnlySpan> tags, + in ReadOnlySpan> tags, Func> metricFactory, ReaderWriterLockSlim cacheLock, Dictionary> cache) @@ -336,7 +336,7 @@ private MetricContext CreateMetricContext( } private void DeterminePrometheusLabels( - ReadOnlySpan> tags, + in ReadOnlySpan> tags, out string[] prometheusLabelNames, out int[] prometheusLabelValueIndexes) { From ad89f4c3f7038ffd214cb9cc605c1cc312ed38d7 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 30 Nov 2023 22:32:45 +0200 Subject: [PATCH 190/230] Irrelevant tidy --- Prometheus/StringSequence.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Prometheus/StringSequence.cs b/Prometheus/StringSequence.cs index cecd3370..7ca97c06 100644 --- a/Prometheus/StringSequence.cs +++ b/Prometheus/StringSequence.cs @@ -33,7 +33,7 @@ public ref struct Enumerator private ReadOnlySpan _currentArray; private string _current; - public string Current + public readonly string Current { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _current; From fe36c2c6de9d4136a275d15dd41cd8ef54f32102 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 30 Nov 2023 22:32:59 +0200 Subject: [PATCH 191/230] Fix missing IEquatable decalration to reduce memory allocation for cache keys --- Prometheus/MeterAdapter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Prometheus/MeterAdapter.cs b/Prometheus/MeterAdapter.cs index 306ca7b4..b4484456 100644 --- a/Prometheus/MeterAdapter.cs +++ b/Prometheus/MeterAdapter.cs @@ -202,7 +202,7 @@ or ObservableUpDownCounter // Cache key: Instrument + user-ordered list of label names. // NB! The same Instrument may be cached multiple times, with the same label names in a different order! - private readonly struct CacheKey(Instrument instrument, StringSequence meterLabelNames) + private readonly struct CacheKey(Instrument instrument, StringSequence meterLabelNames) : IEquatable { public Instrument Instrument { get; } = instrument; From 51fb4680827ca1a38add16537782fb5a69ec9394 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 30 Nov 2023 23:00:30 +0200 Subject: [PATCH 192/230] Remove measurement boxing to reduce memory allocation in MeterAdapter --- Prometheus/MeterAdapter.cs | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/Prometheus/MeterAdapter.cs b/Prometheus/MeterAdapter.cs index b4484456..876ab088 100644 --- a/Prometheus/MeterAdapter.cs +++ b/Prometheus/MeterAdapter.cs @@ -122,19 +122,33 @@ private void OnInstrumentPublished(Instrument instrument, MeterListener listener } } - private void OnMeasurementRecorded(Instrument instrument, T measurement, ReadOnlySpan> tags, object? state) - where T : struct + private void OnMeasurementRecorded( + Instrument instrument, + TMeasurement measurement, + ReadOnlySpan> tags, + object? state) + where TMeasurement : struct { // NOTE: If we throw an exception from this, it can lead to the instrument becoming inoperable (no longer measured). Let's not do that. try { - var value = Convert.ToDouble(measurement); + double value = unchecked(measurement switch + { + byte x => (double)x, + short x => (double)x, + int x => (double)x, + long x => (double)x, + float x => (double)x, + double x => x, + decimal x => (double)x, + _ => throw new NotSupportedException($"Measurement type {typeof(TMeasurement).Name} is not supported.") + }); // We do not represent any of the "counter" style .NET meter types as counters because // they may be re-created on the .NET Meters side at any time, decrementing the value! - if (instrument is Counter) + if (instrument is Counter) { var context = GetOrCreateGaugeContext(instrument, tags); var labelValues = TagsToLabelValues(context, tags); @@ -142,7 +156,7 @@ private void OnMeasurementRecorded(Instrument instrument, T measurement, Read // A measurement is the increment. context.MetricInstanceHandle.WithLease(_incrementGaugeFunc, value, labelValues); } - else if (instrument is ObservableCounter) + else if (instrument is ObservableCounter) { var context = GetOrCreateGaugeContext(instrument, tags); var labelValues = TagsToLabelValues(context, tags); @@ -151,7 +165,7 @@ private void OnMeasurementRecorded(Instrument instrument, T measurement, Read context.MetricInstanceHandle.WithLease(_setGaugeFunc, value, labelValues); } #if NET7_0_OR_GREATER - else if (instrument is UpDownCounter) + else if (instrument is UpDownCounter) { var context = GetOrCreateGaugeContext(instrument, tags); var labelValues = TagsToLabelValues(context, tags); @@ -160,19 +174,19 @@ private void OnMeasurementRecorded(Instrument instrument, T measurement, Read context.MetricInstanceHandle.WithLease(_incrementGaugeFunc, value, labelValues); } #endif - else if (instrument is ObservableGauge + else if (instrument is ObservableGauge #if NET7_0_OR_GREATER - or ObservableUpDownCounter + or ObservableUpDownCounter #endif ) { var context = GetOrCreateGaugeContext(instrument, tags); var labelValues = TagsToLabelValues(context, tags); - + // A measurement is the current value. context.MetricInstanceHandle.WithLease(_setGaugeFunc, value, labelValues); } - else if (instrument is Histogram) + else if (instrument is Histogram) { var context = GetOrCreateHistogramContext(instrument, tags); var labelValues = TagsToLabelValues(context, tags); From e865c72bf9f5e0607426fe925b6a516f80c93637 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 30 Nov 2023 23:03:21 +0200 Subject: [PATCH 193/230] Remove some unnecessary casts --- Prometheus/MeterAdapter.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Prometheus/MeterAdapter.cs b/Prometheus/MeterAdapter.cs index 876ab088..b307774b 100644 --- a/Prometheus/MeterAdapter.cs +++ b/Prometheus/MeterAdapter.cs @@ -135,10 +135,10 @@ private void OnMeasurementRecorded( { double value = unchecked(measurement switch { - byte x => (double)x, - short x => (double)x, - int x => (double)x, - long x => (double)x, + byte x => x, + short x => x, + int x => x, + long x => x, float x => (double)x, double x => x, decimal x => (double)x, From d4badca74905a7df126728cdfcd727813aa74148 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Thu, 30 Nov 2023 23:39:54 +0200 Subject: [PATCH 194/230] Reduce memory allocation in MeterAdapter pipeline by only allocating new label value arrays when the metric instance is not already known --- Prometheus/Collector.cs | 48 +++++++++- Prometheus/IManagedLifetimeMetricHandle.cs | 66 +++++++++++++ .../LabelEnrichingManagedLifetimeCounter.cs | 94 ++++++++++++++++++- .../LabelEnrichingManagedLifetimeGauge.cs | 94 ++++++++++++++++++- .../LabelEnrichingManagedLifetimeHistogram.cs | 94 ++++++++++++++++++- .../LabelEnrichingManagedLifetimeSummary.cs | 94 ++++++++++++++++++- Prometheus/ManagedLifetimeMetricHandle.cs | 42 +++++++++ Prometheus/MeterAdapter.cs | 42 +++++---- 8 files changed, 551 insertions(+), 23 deletions(-) diff --git a/Prometheus/Collector.cs b/Prometheus/Collector.cs index 18c8e89e..81e4f747 100644 --- a/Prometheus/Collector.cs +++ b/Prometheus/Collector.cs @@ -185,7 +185,7 @@ public abstract class Collector : Collector, ICollector // it will often for whatever reason focus on LabelNames instead of Labels, leading to tiny but persistent frustration. // Having WithLabels() instead eliminates the other candidate and allows for a frustration-free typing experience. public TChild WithLabels(params string[] labelValues) => Labels(labelValues); - + // Discourage it as it can create confusion. But it works fine, so no reason to mark it obsolete, really. [EditorBrowsable(EditorBrowsableState.Never)] public TChild Labels(params string[] labelValues) @@ -197,6 +197,33 @@ public TChild Labels(params string[] labelValues) return GetOrAddLabelled(labels); } + internal TChild WithLabels(ReadOnlySpan labelValues) + { + // This is used only by MeterAdapter for now, until we stabilize this feature and have confidence it makes sense in general. + // We first use a pooled buffer to create the LabelSequence in the hopes that an instance with these labels already exists. + // This avoids allocating a string[] for the label values if we can avoid it. We only allocate if we create a new instance. + + var buffer = ArrayPool.Shared.Rent(labelValues.Length); + + try + { + labelValues.CopyTo(buffer); + + var temporaryLabels = LabelSequence.From(InstanceLabelNames, StringSequence.From(buffer.AsMemory(0, labelValues.Length))); + + if (TryGetLabelled(temporaryLabels, out var existing)) + return existing!; + } + finally + { + ArrayPool.Shared.Return(buffer); + } + + // If we got this far, we did not succeed in finding an existing instance. We need to allocate a long-lived string[] for the label values. + var labels = LabelSequence.From(InstanceLabelNames, StringSequence.From(labelValues.ToArray())); + return CreateLabelled(labels); + } + public void RemoveLabelled(params string[] labelValues) { if (labelValues == null) @@ -297,19 +324,36 @@ private TChild GetOrAddLabelled(LabelSequence instanceLabels) // If we somehow end up registering the same metric with the same label names in different order, we will publish it twice, in two orders... // That is not ideal but also not that big of a deal to justify a lookup every time a metric instance is registered. + if (TryGetLabelled(instanceLabels, out var existing)) + return existing!; + + return CreateLabelled(instanceLabels); + } + + private bool TryGetLabelled(LabelSequence instanceLabels, out TChild? child) + { // First try to find an existing instance. This is the fast path, if we are re-looking-up an existing one. _childrenLock.EnterReadLock(); try { if (_children.TryGetValue(instanceLabels, out var existing)) - return existing; + { + child = existing; + return true; + } + + child = null; + return false; } finally { _childrenLock.ExitReadLock(); } + } + private TChild CreateLabelled(LabelSequence instanceLabels) + { // If no existing one found, grab the write lock and create a new one if needed. _childrenLock.EnterWriteLock(); diff --git a/Prometheus/IManagedLifetimeMetricHandle.cs b/Prometheus/IManagedLifetimeMetricHandle.cs index 20418a8c..03b7e27a 100644 --- a/Prometheus/IManagedLifetimeMetricHandle.cs +++ b/Prometheus/IManagedLifetimeMetricHandle.cs @@ -7,6 +7,7 @@ public interface IManagedLifetimeMetricHandle where TMetricInterface : ICollectorChild { + #region Lease(string[]) /// /// Takes a lifetime-extending lease on the metric, scoped to a specific combination of label values. /// @@ -92,6 +93,71 @@ public interface IManagedLifetimeMetricHandle /// Re-publishing may return a new instance of the metric (data collected via expired instances will not be published). /// Task WithLeaseAsync(Func> action, params string[] labelValues); + #endregion + + #region Lease(ReadOnlySpan) + /// + /// Takes a lifetime-extending lease on the metric, scoped to a specific combination of label values. + /// + /// The typical pattern is that the metric value is only modified when the caller is holding a lease on the metric. + /// Automatic removal of the metric will not occur until all leases on the metric are disposed and the expiration duration elapses. + /// + /// + /// Acquiring a new lease after the metric has been removed will re-publish the metric without preserving the old value. + /// Re-publishing may return a new instance of the metric (data collected via expired instances will not be published). + /// + IDisposable AcquireLease(out TMetricInterface metric, ReadOnlySpan labelValues); + + /// + /// Takes a lifetime-extending lease on the metric, scoped to a specific combination of label values. + /// The lease is returned as a stack-only struct, which is faster than the IDisposable version. + /// + /// The typical pattern is that the metric value is only modified when the caller is holding a lease on the metric. + /// Automatic removal of the metric will not occur until all leases on the metric are disposed and the expiration duration elapses. + /// + /// + /// Acquiring a new lease after the metric has been removed will re-publish the metric without preserving the old value. + /// Re-publishing may return a new instance of the metric (data collected via expired instances will not be published). + /// + RefLease AcquireRefLease(out TMetricInterface metric, ReadOnlySpan labelValues); + + /// + /// While executing an action, holds a lifetime-extending lease on the metric, scoped to a specific combination of label values. + /// + /// The typical pattern is that the metric value is only modified when the caller is holding a lease on the metric. + /// Automatic removal of the metric will not occur until all leases on the metric are disposed and the expiration duration elapses. + /// + /// + /// Acquiring a new lease after the metric has been removed will re-publish the metric without preserving the old value. + /// Re-publishing may return a new instance of the metric (data collected via expired instances will not be published). + /// + void WithLease(Action action, ReadOnlySpan labelValues); + + /// + /// While executing an action, holds a lifetime-extending lease on the metric, scoped to a specific combination of label values. + /// Passes a given argument to the callback. + /// + /// The typical pattern is that the metric value is only modified when the caller is holding a lease on the metric. + /// Automatic removal of the metric will not occur until all leases on the metric are disposed and the expiration duration elapses. + /// + /// + /// Acquiring a new lease after the metric has been removed will re-publish the metric without preserving the old value. + /// Re-publishing may return a new instance of the metric (data collected via expired instances will not be published). + /// + void WithLease(Action action, TArg arg, ReadOnlySpan labelValues); + + /// + /// While executing a function, holds a lifetime-extending lease on the metric, scoped to a specific combination of label values. + /// + /// The typical pattern is that the metric value is only modified when the caller is holding a lease on the metric. + /// Automatic removal of the metric will not occur until all leases on the metric are disposed and the expiration duration elapses. + /// + /// + /// Acquiring a new lease after the metric has been removed will re-publish the metric without preserving the old value. + /// Re-publishing may return a new instance of the metric (data collected via expired instances will not be published). + /// + TResult WithLease(Func func, ReadOnlySpan labelValues); + #endregion /// /// Returns a metric instance that automatically extends the lifetime of the timeseries whenever the value is changed. diff --git a/Prometheus/LabelEnrichingManagedLifetimeCounter.cs b/Prometheus/LabelEnrichingManagedLifetimeCounter.cs index 219026ab..a8214c8b 100644 --- a/Prometheus/LabelEnrichingManagedLifetimeCounter.cs +++ b/Prometheus/LabelEnrichingManagedLifetimeCounter.cs @@ -1,4 +1,6 @@ -namespace Prometheus; +using System.Buffers; + +namespace Prometheus; internal sealed class LabelEnrichingManagedLifetimeCounter : IManagedLifetimeMetricHandle { @@ -12,6 +14,7 @@ public LabelEnrichingManagedLifetimeCounter(IManagedLifetimeMetricHandle _inner; private readonly string[] _enrichWithLabelValues; + #region Lease(string[]) public IDisposable AcquireLease(out ICounter metric, params string[] labelValues) { return _inner.AcquireLease(out metric, WithEnrichedLabelValues(labelValues)); @@ -51,9 +54,98 @@ public Task WithLeaseAsync(Func> actio { return _inner.WithLeaseAsync(action, WithEnrichedLabelValues(labelValues)); } + #endregion private string[] WithEnrichedLabelValues(string[] instanceLabelValues) { return _enrichWithLabelValues.Concat(instanceLabelValues).ToArray(); } + + #region Lease(ReadOnlySpan) + public IDisposable AcquireLease(out ICounter metric, ReadOnlySpan labelValues) + { + var buffer = RentBufferForEnrichedLabelValues(labelValues); + + try + { + var enrichedLabelValues = AssembleEnrichedLabelValues(labelValues, buffer); + return _inner.AcquireLease(out metric, enrichedLabelValues); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + public RefLease AcquireRefLease(out ICounter metric, ReadOnlySpan labelValues) + { + var buffer = RentBufferForEnrichedLabelValues(labelValues); + + try + { + var enrichedLabelValues = AssembleEnrichedLabelValues(labelValues, buffer); + return _inner.AcquireRefLease(out metric, enrichedLabelValues); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + public void WithLease(Action action, ReadOnlySpan labelValues) + { + var buffer = RentBufferForEnrichedLabelValues(labelValues); + + try + { + var enrichedLabelValues = AssembleEnrichedLabelValues(labelValues, buffer); + _inner.WithLease(action, enrichedLabelValues); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + public void WithLease(Action action, TArg arg, ReadOnlySpan labelValues) + { + var buffer = RentBufferForEnrichedLabelValues(labelValues); + + try + { + var enrichedLabelValues = AssembleEnrichedLabelValues(labelValues, buffer); + _inner.WithLease(action, arg, enrichedLabelValues); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + public TResult WithLease(Func func, ReadOnlySpan labelValues) + { + var buffer = RentBufferForEnrichedLabelValues(labelValues); + + try + { + var enrichedLabelValues = AssembleEnrichedLabelValues(labelValues, buffer); + return _inner.WithLease(func, enrichedLabelValues); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + #endregion + + private string[] RentBufferForEnrichedLabelValues(ReadOnlySpan instanceLabelValues) + => ArrayPool.Shared.Rent(instanceLabelValues.Length + _enrichWithLabelValues.Length); + + private ReadOnlySpan AssembleEnrichedLabelValues(ReadOnlySpan instanceLabelValues, string[] buffer) + { + _enrichWithLabelValues.CopyTo(buffer, 0); + instanceLabelValues.CopyTo(buffer.AsSpan(_enrichWithLabelValues.Length)); + + return buffer.AsSpan(0, _enrichWithLabelValues.Length + instanceLabelValues.Length); + } } diff --git a/Prometheus/LabelEnrichingManagedLifetimeGauge.cs b/Prometheus/LabelEnrichingManagedLifetimeGauge.cs index edd55da5..5de68b4e 100644 --- a/Prometheus/LabelEnrichingManagedLifetimeGauge.cs +++ b/Prometheus/LabelEnrichingManagedLifetimeGauge.cs @@ -1,4 +1,6 @@ -namespace Prometheus; +using System.Buffers; + +namespace Prometheus; internal sealed class LabelEnrichingManagedLifetimeGauge : IManagedLifetimeMetricHandle { @@ -11,6 +13,7 @@ public LabelEnrichingManagedLifetimeGauge(IManagedLifetimeMetricHandle i private readonly IManagedLifetimeMetricHandle _inner; private readonly string[] _enrichWithLabelValues; + #region Lease(string[]) public IDisposable AcquireLease(out IGauge metric, params string[] labelValues) { return _inner.AcquireLease(out metric, WithEnrichedLabelValues(labelValues)); @@ -50,9 +53,98 @@ public Task WithLeaseAsync(Func> action, { return _inner.WithLeaseAsync(action, WithEnrichedLabelValues(labelValues)); } + #endregion private string[] WithEnrichedLabelValues(string[] instanceLabelValues) { return _enrichWithLabelValues.Concat(instanceLabelValues).ToArray(); } + + #region Lease(ReadOnlySpan) + public IDisposable AcquireLease(out IGauge metric, ReadOnlySpan labelValues) + { + var buffer = RentBufferForEnrichedLabelValues(labelValues); + + try + { + var enrichedLabelValues = AssembleEnrichedLabelValues(labelValues, buffer); + return _inner.AcquireLease(out metric, enrichedLabelValues); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + public RefLease AcquireRefLease(out IGauge metric, ReadOnlySpan labelValues) + { + var buffer = RentBufferForEnrichedLabelValues(labelValues); + + try + { + var enrichedLabelValues = AssembleEnrichedLabelValues(labelValues, buffer); + return _inner.AcquireRefLease(out metric, enrichedLabelValues); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + public void WithLease(Action action, ReadOnlySpan labelValues) + { + var buffer = RentBufferForEnrichedLabelValues(labelValues); + + try + { + var enrichedLabelValues = AssembleEnrichedLabelValues(labelValues, buffer); + _inner.WithLease(action, enrichedLabelValues); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + public void WithLease(Action action, TArg arg, ReadOnlySpan labelValues) + { + var buffer = RentBufferForEnrichedLabelValues(labelValues); + + try + { + var enrichedLabelValues = AssembleEnrichedLabelValues(labelValues, buffer); + _inner.WithLease(action, arg, enrichedLabelValues); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + public TResult WithLease(Func func, ReadOnlySpan labelValues) + { + var buffer = RentBufferForEnrichedLabelValues(labelValues); + + try + { + var enrichedLabelValues = AssembleEnrichedLabelValues(labelValues, buffer); + return _inner.WithLease(func, enrichedLabelValues); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + #endregion + + private string[] RentBufferForEnrichedLabelValues(ReadOnlySpan instanceLabelValues) + => ArrayPool.Shared.Rent(instanceLabelValues.Length + _enrichWithLabelValues.Length); + + private ReadOnlySpan AssembleEnrichedLabelValues(ReadOnlySpan instanceLabelValues, string[] buffer) + { + _enrichWithLabelValues.CopyTo(buffer, 0); + instanceLabelValues.CopyTo(buffer.AsSpan(_enrichWithLabelValues.Length)); + + return buffer.AsSpan(0, _enrichWithLabelValues.Length + instanceLabelValues.Length); + } } diff --git a/Prometheus/LabelEnrichingManagedLifetimeHistogram.cs b/Prometheus/LabelEnrichingManagedLifetimeHistogram.cs index 5e69c790..9199dc3b 100644 --- a/Prometheus/LabelEnrichingManagedLifetimeHistogram.cs +++ b/Prometheus/LabelEnrichingManagedLifetimeHistogram.cs @@ -1,4 +1,6 @@ -namespace Prometheus; +using System.Buffers; + +namespace Prometheus; internal sealed class LabelEnrichingManagedLifetimeHistogram : IManagedLifetimeMetricHandle { @@ -11,6 +13,7 @@ public LabelEnrichingManagedLifetimeHistogram(IManagedLifetimeMetricHandle _inner; private readonly string[] _enrichWithLabelValues; + #region Lease(string[]) public IDisposable AcquireLease(out IHistogram metric, params string[] labelValues) { return _inner.AcquireLease(out metric, WithEnrichedLabelValues(labelValues)); @@ -50,9 +53,98 @@ public Task WithLeaseAsync(Func> act { return _inner.WithLeaseAsync(action, WithEnrichedLabelValues(labelValues)); } + #endregion private string[] WithEnrichedLabelValues(string[] instanceLabelValues) { return _enrichWithLabelValues.Concat(instanceLabelValues).ToArray(); } + + #region Lease(ReadOnlySpan) + public IDisposable AcquireLease(out IHistogram metric, ReadOnlySpan labelValues) + { + var buffer = RentBufferForEnrichedLabelValues(labelValues); + + try + { + var enrichedLabelValues = AssembleEnrichedLabelValues(labelValues, buffer); + return _inner.AcquireLease(out metric, enrichedLabelValues); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + public RefLease AcquireRefLease(out IHistogram metric, ReadOnlySpan labelValues) + { + var buffer = RentBufferForEnrichedLabelValues(labelValues); + + try + { + var enrichedLabelValues = AssembleEnrichedLabelValues(labelValues, buffer); + return _inner.AcquireRefLease(out metric, enrichedLabelValues); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + public void WithLease(Action action, ReadOnlySpan labelValues) + { + var buffer = RentBufferForEnrichedLabelValues(labelValues); + + try + { + var enrichedLabelValues = AssembleEnrichedLabelValues(labelValues, buffer); + _inner.WithLease(action, enrichedLabelValues); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + public void WithLease(Action action, TArg arg, ReadOnlySpan labelValues) + { + var buffer = RentBufferForEnrichedLabelValues(labelValues); + + try + { + var enrichedLabelValues = AssembleEnrichedLabelValues(labelValues, buffer); + _inner.WithLease(action, arg, enrichedLabelValues); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + public TResult WithLease(Func func, ReadOnlySpan labelValues) + { + var buffer = RentBufferForEnrichedLabelValues(labelValues); + + try + { + var enrichedLabelValues = AssembleEnrichedLabelValues(labelValues, buffer); + return _inner.WithLease(func, enrichedLabelValues); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + #endregion + + private string[] RentBufferForEnrichedLabelValues(ReadOnlySpan instanceLabelValues) + => ArrayPool.Shared.Rent(instanceLabelValues.Length + _enrichWithLabelValues.Length); + + private ReadOnlySpan AssembleEnrichedLabelValues(ReadOnlySpan instanceLabelValues, string[] buffer) + { + _enrichWithLabelValues.CopyTo(buffer, 0); + instanceLabelValues.CopyTo(buffer.AsSpan(_enrichWithLabelValues.Length)); + + return buffer.AsSpan(0, _enrichWithLabelValues.Length + instanceLabelValues.Length); + } } diff --git a/Prometheus/LabelEnrichingManagedLifetimeSummary.cs b/Prometheus/LabelEnrichingManagedLifetimeSummary.cs index b5d5d094..bca06701 100644 --- a/Prometheus/LabelEnrichingManagedLifetimeSummary.cs +++ b/Prometheus/LabelEnrichingManagedLifetimeSummary.cs @@ -1,4 +1,6 @@ -namespace Prometheus; +using System.Buffers; + +namespace Prometheus; internal sealed class LabelEnrichingManagedLifetimeSummary : IManagedLifetimeMetricHandle { @@ -11,6 +13,7 @@ public LabelEnrichingManagedLifetimeSummary(IManagedLifetimeMetricHandle _inner; private readonly string[] _enrichWithLabelValues; + #region Lease(string[]) public IDisposable AcquireLease(out ISummary metric, params string[] labelValues) { return _inner.AcquireLease(out metric, WithEnrichedLabelValues(labelValues)); @@ -50,9 +53,98 @@ public Task WithLeaseAsync(Func> actio { return _inner.WithLeaseAsync(action, WithEnrichedLabelValues(labelValues)); } + #endregion private string[] WithEnrichedLabelValues(string[] instanceLabelValues) { return _enrichWithLabelValues.Concat(instanceLabelValues).ToArray(); } + + #region Lease(ReadOnlySpan) + public IDisposable AcquireLease(out ISummary metric, ReadOnlySpan labelValues) + { + var buffer = RentBufferForEnrichedLabelValues(labelValues); + + try + { + var enrichedLabelValues = AssembleEnrichedLabelValues(labelValues, buffer); + return _inner.AcquireLease(out metric, enrichedLabelValues); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + public RefLease AcquireRefLease(out ISummary metric, ReadOnlySpan labelValues) + { + var buffer = RentBufferForEnrichedLabelValues(labelValues); + + try + { + var enrichedLabelValues = AssembleEnrichedLabelValues(labelValues, buffer); + return _inner.AcquireRefLease(out metric, enrichedLabelValues); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + public void WithLease(Action action, ReadOnlySpan labelValues) + { + var buffer = RentBufferForEnrichedLabelValues(labelValues); + + try + { + var enrichedLabelValues = AssembleEnrichedLabelValues(labelValues, buffer); + _inner.WithLease(action, enrichedLabelValues); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + public void WithLease(Action action, TArg arg, ReadOnlySpan labelValues) + { + var buffer = RentBufferForEnrichedLabelValues(labelValues); + + try + { + var enrichedLabelValues = AssembleEnrichedLabelValues(labelValues, buffer); + _inner.WithLease(action, arg, enrichedLabelValues); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + public TResult WithLease(Func func, ReadOnlySpan labelValues) + { + var buffer = RentBufferForEnrichedLabelValues(labelValues); + + try + { + var enrichedLabelValues = AssembleEnrichedLabelValues(labelValues, buffer); + return _inner.WithLease(func, enrichedLabelValues); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + #endregion + + private string[] RentBufferForEnrichedLabelValues(ReadOnlySpan instanceLabelValues) + => ArrayPool.Shared.Rent(instanceLabelValues.Length + _enrichWithLabelValues.Length); + + private ReadOnlySpan AssembleEnrichedLabelValues(ReadOnlySpan instanceLabelValues, string[] buffer) + { + _enrichWithLabelValues.CopyTo(buffer, 0); + instanceLabelValues.CopyTo(buffer.AsSpan(_enrichWithLabelValues.Length)); + + return buffer.AsSpan(0, _enrichWithLabelValues.Length + instanceLabelValues.Length); + } } diff --git a/Prometheus/ManagedLifetimeMetricHandle.cs b/Prometheus/ManagedLifetimeMetricHandle.cs index 6b49cb24..e729cd53 100644 --- a/Prometheus/ManagedLifetimeMetricHandle.cs +++ b/Prometheus/ManagedLifetimeMetricHandle.cs @@ -26,6 +26,7 @@ internal ManagedLifetimeMetricHandle(Collector metric, TimeSpan expiresA protected readonly Collector _metric; protected readonly TimeSpan _expiresAfter; + #region Lease(string[]) public IDisposable AcquireLease(out TMetricInterface metric, params string[] labelValues) { var child = _metric.WithLabels(labelValues); @@ -75,6 +76,47 @@ public async Task WithLeaseAsync(Func) + public IDisposable AcquireLease(out TMetricInterface metric, ReadOnlySpan labelValues) + { + var child = _metric.WithLabels(labelValues); + metric = child; + + return TakeLease(child); + } + + public RefLease AcquireRefLease(out TMetricInterface metric, ReadOnlySpan labelValues) + { + var child = _metric.WithLabels(labelValues); + metric = child; + + return TakeRefLease(child); + } + + public void WithLease(Action action, ReadOnlySpan labelValues) + { + var child = _metric.WithLabels(labelValues); + using var lease = TakeRefLease(child); + + action(child); + } + + public void WithLease(Action action, TArg arg, ReadOnlySpan labelValues) + { + var child = _metric.WithLabels(labelValues); + using var lease = TakeRefLease(child); + + action(arg, child); + } + + public TResult WithLease(Func func, ReadOnlySpan labelValues) + { + using var lease = AcquireLease(out var metric, labelValues); + return func(metric); + } + #endregion public abstract ICollector WithExtendLifetimeOnUse(); diff --git a/Prometheus/MeterAdapter.cs b/Prometheus/MeterAdapter.cs index b307774b..00b7d8eb 100644 --- a/Prometheus/MeterAdapter.cs +++ b/Prometheus/MeterAdapter.cs @@ -44,7 +44,7 @@ private MeterAdapter(MeterAdapterOptions options) var baseFactory = options.MetricFactory ?? Metrics.WithCustomRegistry(_options.Registry); _factory = (ManagedLifetimeMetricFactory)baseFactory.WithManagedLifetime(expiresAfter: options.MetricsExpireAfter); - _inheritedStaticLabelNames = ((ManagedLifetimeMetricFactory)_factory).GetAllStaticLabelNames().ToArray(); + _inheritedStaticLabelNames = _factory.GetAllStaticLabelNames().ToArray(); _listener.InstrumentPublished = OnInstrumentPublished; _listener.MeasurementsCompleted += OnMeasurementsCompleted; @@ -76,7 +76,7 @@ private MeterAdapter(MeterAdapterOptions options) private readonly MeterAdapterOptions _options; private readonly CollectorRegistry _registry; - private readonly IManagedLifetimeMetricFactory _factory; + private readonly ManagedLifetimeMetricFactory _factory; private readonly string[] _inheritedStaticLabelNames; private readonly Gauge _instrumentsConnected; @@ -131,6 +131,10 @@ private void OnMeasurementRecorded( { // NOTE: If we throw an exception from this, it can lead to the instrument becoming inoperable (no longer measured). Let's not do that. + // We assemble and sort the label values in a temporary buffer. If the metric instance is already known + // to prometheus-net, this means no further memory allocation for the label values is required below. + var labelValuesBuffer = ArrayPool.Shared.Rent(tags.Length); + try { double value = unchecked(measurement switch @@ -151,7 +155,7 @@ private void OnMeasurementRecorded( if (instrument is Counter) { var context = GetOrCreateGaugeContext(instrument, tags); - var labelValues = TagsToLabelValues(context, tags); + var labelValues = CopyTagValuesToLabelValues(context.PrometheusLabelValueIndexes, tags, labelValuesBuffer.AsSpan()); // A measurement is the increment. context.MetricInstanceHandle.WithLease(_incrementGaugeFunc, value, labelValues); @@ -159,7 +163,7 @@ private void OnMeasurementRecorded( else if (instrument is ObservableCounter) { var context = GetOrCreateGaugeContext(instrument, tags); - var labelValues = TagsToLabelValues(context, tags); + var labelValues = CopyTagValuesToLabelValues(context.PrometheusLabelValueIndexes, tags, labelValuesBuffer.AsSpan()); // A measurement is the current value. We transform it into a Set() to allow the counter to reset itself (unusual but who are we to say no). context.MetricInstanceHandle.WithLease(_setGaugeFunc, value, labelValues); @@ -168,7 +172,7 @@ private void OnMeasurementRecorded( else if (instrument is UpDownCounter) { var context = GetOrCreateGaugeContext(instrument, tags); - var labelValues = TagsToLabelValues(context, tags); + var labelValues = CopyTagValuesToLabelValues(context.PrometheusLabelValueIndexes, tags, labelValuesBuffer.AsSpan()); // A measurement is the increment. context.MetricInstanceHandle.WithLease(_incrementGaugeFunc, value, labelValues); @@ -181,7 +185,7 @@ or ObservableUpDownCounter ) { var context = GetOrCreateGaugeContext(instrument, tags); - var labelValues = TagsToLabelValues(context, tags); + var labelValues = CopyTagValuesToLabelValues(context.PrometheusLabelValueIndexes, tags, labelValuesBuffer.AsSpan()); // A measurement is the current value. context.MetricInstanceHandle.WithLease(_setGaugeFunc, value, labelValues); @@ -189,7 +193,7 @@ or ObservableUpDownCounter else if (instrument is Histogram) { var context = GetOrCreateHistogramContext(instrument, tags); - var labelValues = TagsToLabelValues(context, tags); + var labelValues = CopyTagValuesToLabelValues(context.PrometheusLabelValueIndexes, tags, labelValuesBuffer.AsSpan()); // A measurement is the observed value. context.MetricInstanceHandle.WithLease(_observeHistogramFunc, value, labelValues); @@ -203,6 +207,10 @@ or ObservableUpDownCounter { Trace.WriteLine($"{instrument.Name} collection failed: {ex.Message}"); } + finally + { + ArrayPool.Shared.Return(labelValuesBuffer); + } } private static void IncrementGauge(double value, IGauge gauge) => gauge.Inc(value); @@ -257,8 +265,8 @@ private MetricContext GetOrCreateHistogramContext(Instrument instrum => GetOrCreateMetricContext(instrument, tags, _createHistogramFunc, _histogramCacheLock, _histogramCache); private IManagedLifetimeMetricHandle CreateGauge(Instrument instrument, string name, string help, string[] labelNames) - => _factory.CreateGauge(name, help, labelNames); - private Func> _createGaugeFunc; + => _factory.CreateGauge(name, help, labelNames, null); + private readonly Func> _createGaugeFunc; private IManagedLifetimeMetricHandle CreateHistogram(Instrument instrument, string name, string help, string[] labelNames) => _factory.CreateHistogram(name, help, labelNames, new HistogramConfiguration @@ -266,7 +274,7 @@ private IManagedLifetimeMetricHandle CreateHistogram(Instrument inst // We outsource the bucket definition to the callback in options, as it might need to be different for different instruments. Buckets = _options.ResolveHistogramBuckets(instrument) }); - private Func> _createHistogramFunc; + private readonly Func> _createHistogramFunc; private MetricContext GetOrCreateMetricContext( Instrument instrument, @@ -427,18 +435,18 @@ private void OnMeasurementsCompleted(Instrument instrument, object? state) // The entire adapter data set will be collected when the Prometheus registry itself is garbage collected. } - private static string[] TagsToLabelValues(MetricContext context, ReadOnlySpan> tags) - where TMetricInstance : ICollectorChild + private static ReadOnlySpan CopyTagValuesToLabelValues( + int[] prometheusLabelValueIndexes, + ReadOnlySpan> tags, + Span labelValues) { - var labelValues = new string[context.PrometheusLabelValueIndexes.Length]; - - for (var i = 0; i < labelValues.Length; i++) + for (var i = 0; i < prometheusLabelValueIndexes.Length; i++) { - var index = context.PrometheusLabelValueIndexes[i]; + var index = prometheusLabelValueIndexes[i]; labelValues[i] = tags[index].Value?.ToString() ?? ""; } - return labelValues; + return labelValues[..prometheusLabelValueIndexes.Length]; } // We use these dictionaries to register Prometheus metrics on-demand for different instruments. From 8cff3f2f131e48d4c14610096d40821b1f12498c Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Fri, 1 Dec 2023 11:15:26 +0200 Subject: [PATCH 195/230] Reduce resource cost of metric serialization by reducing number of write calls --- Benchmark.NetCore/SerializationBenchmarks.cs | 3 +- Prometheus/LabelSequence.cs | 8 +- Prometheus/TextSerializer.Net.cs | 488 ++++++++++++++----- 3 files changed, 379 insertions(+), 120 deletions(-) diff --git a/Benchmark.NetCore/SerializationBenchmarks.cs b/Benchmark.NetCore/SerializationBenchmarks.cs index 825aa1fa..96bf2542 100644 --- a/Benchmark.NetCore/SerializationBenchmarks.cs +++ b/Benchmark.NetCore/SerializationBenchmarks.cs @@ -5,6 +5,7 @@ namespace Benchmark.NetCore; [MemoryDiagnoser] +//[EventPipeProfiler(BenchmarkDotNet.Diagnosers.EventPipeProfile.CpuSampling)] public class SerializationBenchmarks { public enum OutputStreamType @@ -189,7 +190,7 @@ public async Task CollectAndSerialize() await _registry.CollectAndSerializeAsync(new TextSerializer(_outputStream), default); } - //[Benchmark] + [Benchmark] public async Task CollectAndSerializeOpenMetrics() { await _registry.CollectAndSerializeAsync(new TextSerializer(_outputStream, ExpositionFormat.OpenMetricsText), default); diff --git a/Prometheus/LabelSequence.cs b/Prometheus/LabelSequence.cs index d3fc5538..c3266e9f 100644 --- a/Prometheus/LabelSequence.cs +++ b/Prometheus/LabelSequence.cs @@ -138,22 +138,22 @@ public byte[] Serialize() #if NET if (i != 0) { - TextSerializer.Comma.CopyTo(bytes.AsMemory(index)); + TextSerializer.Comma.CopyTo(bytes.AsSpan(index)); index += TextSerializer.Comma.Length; } index += PrometheusConstants.ExportEncoding.GetBytes(nameEnumerator.Current, 0, nameEnumerator.Current.Length, bytes, index); - TextSerializer.Equal.CopyTo(bytes.AsMemory(index)); + TextSerializer.Equal.CopyTo(bytes.AsSpan(index)); index += TextSerializer.Equal.Length; - TextSerializer.Quote.CopyTo(bytes.AsMemory(index)); + TextSerializer.Quote.CopyTo(bytes.AsSpan(index)); index += TextSerializer.Quote.Length; var escapedLabelValue = EscapeLabelValue(valueEnumerator.Current); index += PrometheusConstants.ExportEncoding.GetBytes(escapedLabelValue, 0, escapedLabelValue.Length, bytes, index); - TextSerializer.Quote.CopyTo(bytes.AsMemory(index)); + TextSerializer.Quote.CopyTo(bytes.AsSpan(index)); index += TextSerializer.Quote.Length; #else if (i != 0) diff --git a/Prometheus/TextSerializer.Net.cs b/Prometheus/TextSerializer.Net.cs index 0bf6c8ad..6e7bc653 100644 --- a/Prometheus/TextSerializer.Net.cs +++ b/Prometheus/TextSerializer.Net.cs @@ -1,4 +1,6 @@ #if NET +using System; +using System.Buffers; using System.Globalization; using System.Runtime.CompilerServices; @@ -9,31 +11,30 @@ namespace Prometheus; /// internal sealed class TextSerializer : IMetricsSerializer { - internal static readonly ReadOnlyMemory NewLine = new byte[] { (byte)'\n' }; - internal static readonly ReadOnlyMemory Quote = new byte[] { (byte)'"' }; - internal static readonly ReadOnlyMemory Equal = new byte[] { (byte)'=' }; - internal static readonly ReadOnlyMemory Comma = new byte[] { (byte)',' }; - internal static readonly ReadOnlyMemory Underscore = new byte[] { (byte)'_' }; - internal static readonly ReadOnlyMemory LeftBrace = new byte[] { (byte)'{' }; - internal static readonly ReadOnlyMemory RightBraceSpace = new byte[] { (byte)'}', (byte)' ' }; - internal static readonly ReadOnlyMemory Space = new byte[] { (byte)' ' }; - internal static readonly ReadOnlyMemory SpaceHashSpaceLeftBrace = new byte[] { (byte)' ', (byte)'#', (byte)' ', (byte)'{' }; - internal static readonly ReadOnlyMemory PositiveInfinity = PrometheusConstants.ExportEncoding.GetBytes("+Inf"); - internal static readonly ReadOnlyMemory NegativeInfinity = PrometheusConstants.ExportEncoding.GetBytes("-Inf"); - internal static readonly ReadOnlyMemory NotANumber = PrometheusConstants.ExportEncoding.GetBytes("NaN"); - internal static readonly ReadOnlyMemory DotZero = PrometheusConstants.ExportEncoding.GetBytes(".0"); - internal static readonly ReadOnlyMemory FloatPositiveOne = PrometheusConstants.ExportEncoding.GetBytes("1.0"); - internal static readonly ReadOnlyMemory FloatZero = PrometheusConstants.ExportEncoding.GetBytes("0.0"); - internal static readonly ReadOnlyMemory FloatNegativeOne = PrometheusConstants.ExportEncoding.GetBytes("-1.0"); - internal static readonly ReadOnlyMemory IntPositiveOne = PrometheusConstants.ExportEncoding.GetBytes("1"); - internal static readonly ReadOnlyMemory IntZero = PrometheusConstants.ExportEncoding.GetBytes("0"); - internal static readonly ReadOnlyMemory IntNegativeOne = PrometheusConstants.ExportEncoding.GetBytes("-1"); - internal static readonly ReadOnlyMemory EofNewLine = PrometheusConstants.ExportEncoding.GetBytes("# EOF\n"); - internal static readonly ReadOnlyMemory HashHelpSpace = PrometheusConstants.ExportEncoding.GetBytes("# HELP "); - internal static readonly ReadOnlyMemory NewlineHashTypeSpace = PrometheusConstants.ExportEncoding.GetBytes("\n# TYPE "); - - internal static readonly byte[] Unknown = PrometheusConstants.ExportEncoding.GetBytes("unknown"); - + internal static ReadOnlySpan NewLine => [(byte)'\n']; + internal static ReadOnlySpan Quote => [(byte)'"']; + internal static ReadOnlySpan Equal => [(byte)'=']; + internal static ReadOnlySpan Comma => [(byte)',']; + internal static ReadOnlySpan Underscore => [(byte)'_']; + internal static ReadOnlySpan LeftBrace => [(byte)'{']; + internal static ReadOnlySpan RightBraceSpace => [(byte)'}', (byte)' ']; + internal static ReadOnlySpan Space => [(byte)' ']; + internal static ReadOnlySpan SpaceHashSpaceLeftBrace => [(byte)' ', (byte)'#', (byte)' ', (byte)'{']; + internal static ReadOnlySpan PositiveInfinity => [(byte)'+', (byte)'I', (byte)'n', (byte)'f']; + internal static ReadOnlySpan NegativeInfinity => [(byte)'-', (byte)'I', (byte)'n', (byte)'f']; + internal static ReadOnlySpan NotANumber => [(byte)'N', (byte)'a', (byte)'N']; + internal static ReadOnlySpan DotZero => [(byte)'.', (byte)'0']; + internal static ReadOnlySpan FloatPositiveOne => [(byte)'1', (byte)'.', (byte)'0']; + internal static ReadOnlySpan FloatZero => [(byte)'0', (byte)'.', (byte)'0']; + internal static ReadOnlySpan FloatNegativeOne => [(byte)'-', (byte)'1', (byte)'.', (byte)'0']; + internal static ReadOnlySpan IntPositiveOne => [(byte)'1']; + internal static ReadOnlySpan IntZero => [(byte)'0']; + internal static ReadOnlySpan IntNegativeOne => [(byte)'-', (byte)'1']; + internal static ReadOnlySpan HashHelpSpace => [(byte)'#', (byte)' ', (byte)'H', (byte)'E', (byte)'L', (byte)'P', (byte)' ']; + internal static ReadOnlySpan NewlineHashTypeSpace => [(byte)'\n', (byte)'#', (byte)' ', (byte)'T', (byte)'Y', (byte)'P', (byte)'E', (byte)' ']; + + internal static readonly byte[] UnknownBytes = PrometheusConstants.ExportEncoding.GetBytes("unknown"); + internal static readonly byte[] EofNewLineBytes = { (byte)'#', (byte)' ', (byte)'E', (byte)'O', (byte)'F', (byte)'\n' }; internal static readonly byte[] PositiveInfinityBytes = PrometheusConstants.ExportEncoding.GetBytes("+Inf"); internal static readonly Dictionary MetricTypeToBytes = new() @@ -86,7 +87,54 @@ public async Task FlushAsync(CancellationToken cancel) public async ValueTask WriteFamilyDeclarationAsync(string name, byte[] nameBytes, byte[] helpBytes, MetricType type, byte[] typeBytes, CancellationToken cancel) { + var bufferLength = MeasureFamilyDeclarationLength(name, nameBytes, helpBytes, type, typeBytes); + var buffer = ArrayPool.Shared.Rent(bufferLength); + + try + { + var nameLen = nameBytes.Length; + if (_expositionFormat == ExpositionFormat.OpenMetricsText && type == MetricType.Counter) + { + if (name.EndsWith("_total")) + { + nameLen -= 6; // in OpenMetrics the counter name does not include the _total prefix. + } + else + { + typeBytes = UnknownBytes; // if the total prefix is missing the _total prefix it is out of spec + } + } + + var position = 0; + AppendToBufferAndIncrementPosition(HashHelpSpace, buffer, ref position); + AppendToBufferAndIncrementPosition(nameBytes.AsSpan(0, nameLen), buffer, ref position); + // The space after the name in "HELP" is mandatory as per ABNF, even if there is no help text. + AppendToBufferAndIncrementPosition(Space, buffer, ref position); + if (helpBytes.Length > 0) + { + AppendToBufferAndIncrementPosition(helpBytes, buffer, ref position); + } + AppendToBufferAndIncrementPosition(NewlineHashTypeSpace, buffer, ref position); + AppendToBufferAndIncrementPosition(nameBytes.AsSpan(0, nameLen), buffer, ref position); + AppendToBufferAndIncrementPosition(Space, buffer, ref position); + AppendToBufferAndIncrementPosition(typeBytes, buffer, ref position); + AppendToBufferAndIncrementPosition(NewLine, buffer, ref position); + + await _stream.Value.WriteAsync(buffer.AsMemory(0, position), cancel); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + public int MeasureFamilyDeclarationLength(string name, byte[] nameBytes, byte[] helpBytes, MetricType type, byte[] typeBytes) + { + // We mirror the logic in the Write() call but just measure how many bytes of buffer we need. + var length = 0; + var nameLen = nameBytes.Length; + if (_expositionFormat == ExpositionFormat.OpenMetricsText && type == MetricType.Counter) { if (name.EndsWith("_total")) @@ -95,149 +143,262 @@ public async ValueTask WriteFamilyDeclarationAsync(string name, byte[] nameBytes } else { - typeBytes = Unknown; // if the total prefix is missing the _total prefix it is out of spec + typeBytes = UnknownBytes; // if the total prefix is missing the _total prefix it is out of spec } } - await _stream.Value.WriteAsync(HashHelpSpace, cancel); - await _stream.Value.WriteAsync(nameBytes.AsMemory(0, nameLen), cancel); + length += HashHelpSpace.Length; + length += nameLen; // The space after the name in "HELP" is mandatory as per ABNF, even if there is no help text. - await _stream.Value.WriteAsync(Space, cancel); - if (helpBytes.Length > 0) - { - await _stream.Value.WriteAsync(helpBytes, cancel); - } - await _stream.Value.WriteAsync(NewlineHashTypeSpace, cancel); - await _stream.Value.WriteAsync(nameBytes.AsMemory(0, nameLen), cancel); - await _stream.Value.WriteAsync(Space, cancel); - await _stream.Value.WriteAsync(typeBytes, cancel); - await _stream.Value.WriteAsync(NewLine, cancel); + length += Space.Length; + length += helpBytes.Length; + length += NewlineHashTypeSpace.Length; + length += nameLen; + length += Space.Length; + length += typeBytes.Length; + length += NewLine.Length; + + return length; } public async ValueTask WriteEnd(CancellationToken cancel) { if (_expositionFormat == ExpositionFormat.OpenMetricsText) - await _stream.Value.WriteAsync(EofNewLine, cancel); + await _stream.Value.WriteAsync(EofNewLineBytes, cancel); } [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] public async ValueTask WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, CanonicalLabel canonicalLabel, CancellationToken cancel, double value, ObservedExemplar exemplar, byte[]? suffix = null) { - await WriteIdentifierPartAsync(name, flattenedLabels, cancel, canonicalLabel, suffix); + // This is a max length because we do not know ahead of time how many bytes the actual value will consume. + var bufferMaxLength = MeasureIdentifierPartLength(name, flattenedLabels, canonicalLabel, suffix) + MeasureValueMaxLength(value) + NewLine.Length; - await WriteValue(value, cancel); if (_expositionFormat == ExpositionFormat.OpenMetricsText && exemplar.IsValid) + bufferMaxLength += MeasureExemplarMaxLength(exemplar); + + var buffer = ArrayPool.Shared.Rent(bufferMaxLength); + + try { - await WriteExemplarAsync(cancel, exemplar); - } + var position = WriteIdentifierPart(buffer, name, flattenedLabels, canonicalLabel, suffix); + + position += WriteValue(buffer.AsSpan(position..), value); + + if (_expositionFormat == ExpositionFormat.OpenMetricsText && exemplar.IsValid) + { + position += WriteExemplar(buffer.AsSpan(position..), exemplar); + } + + AppendToBufferAndIncrementPosition(NewLine, buffer, ref position); + + ValidateBufferMaxLengthAndPosition(bufferMaxLength, position); - await _stream.Value.WriteAsync(NewLine, cancel); + await _stream.Value.WriteAsync(buffer.AsMemory(0, position), cancel); + } + finally + { + ArrayPool.Shared.Return(buffer); + } } [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] public async ValueTask WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, CanonicalLabel canonicalLabel, CancellationToken cancel, long value, ObservedExemplar exemplar, byte[]? suffix = null) { - await WriteIdentifierPartAsync(name, flattenedLabels, cancel, canonicalLabel, suffix); + // This is a max length because we do not know ahead of time how many bytes the actual value will consume. + var bufferMaxLength = MeasureIdentifierPartLength(name, flattenedLabels, canonicalLabel, suffix) + MeasureValueMaxLength(value) + NewLine.Length; - await WriteValue(value, cancel); if (_expositionFormat == ExpositionFormat.OpenMetricsText && exemplar.IsValid) + bufferMaxLength += MeasureExemplarMaxLength(exemplar); + + var buffer = ArrayPool.Shared.Rent(bufferMaxLength); + + try { - await WriteExemplarAsync(cancel, exemplar); + var position = WriteIdentifierPart(buffer, name, flattenedLabels, canonicalLabel, suffix); + + position += WriteValue(buffer.AsSpan(position..), value); + + if (_expositionFormat == ExpositionFormat.OpenMetricsText && exemplar.IsValid) + { + position += WriteExemplar(buffer.AsSpan(position..), exemplar); + } + + AppendToBufferAndIncrementPosition(NewLine, buffer, ref position); + + ValidateBufferMaxLengthAndPosition(bufferMaxLength, position); + + await _stream.Value.WriteAsync(buffer.AsMemory(0, position), cancel); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + + private int WriteExemplar(Span buffer, ObservedExemplar exemplar) + { + var position = 0; + + AppendToBufferAndIncrementPosition(SpaceHashSpaceLeftBrace, buffer, ref position); + for (var i = 0; i < exemplar.Labels!.Length; i++) + { + if (i > 0) + AppendToBufferAndIncrementPosition(Comma, buffer, ref position); + + position += WriteExemplarLabel(buffer[position..], exemplar.Labels!.Buffer[i].KeyBytes, exemplar.Labels!.Buffer[i].ValueBytes); } - await _stream.Value.WriteAsync(NewLine, cancel); + AppendToBufferAndIncrementPosition(RightBraceSpace, buffer, ref position); + position += WriteValue(buffer[position..], exemplar.Value); + AppendToBufferAndIncrementPosition(Space, buffer, ref position); + position += WriteValue(buffer[position..], exemplar.Timestamp); + + return position; } - [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] - private async ValueTask WriteExemplarAsync(CancellationToken cancel, ObservedExemplar exemplar) + private int MeasureExemplarMaxLength(ObservedExemplar exemplar) { - await _stream.Value.WriteAsync(SpaceHashSpaceLeftBrace, cancel); + // We mirror the logic in the Write() call but just measure how many bytes of buffer we need. + var length = 0; + + length += SpaceHashSpaceLeftBrace.Length; for (var i = 0; i < exemplar.Labels!.Length; i++) { if (i > 0) - await _stream.Value.WriteAsync(Comma, cancel); - await WriteLabel(exemplar.Labels!.Buffer[i].KeyBytes, exemplar.Labels!.Buffer[i].ValueBytes, cancel); + length += Comma.Length; + + length += MeasureExemplarLabelLength(exemplar.Labels!.Buffer[i].KeyBytes, exemplar.Labels!.Buffer[i].ValueBytes); } - await _stream.Value.WriteAsync(RightBraceSpace, cancel); - await WriteValue(exemplar.Value, cancel); - await _stream.Value.WriteAsync(Space, cancel); - await WriteValue(exemplar.Timestamp, cancel); + length += RightBraceSpace.Length; + length += MeasureValueMaxLength(exemplar.Value); + length += Space.Length; + length += MeasureValueMaxLength(exemplar.Timestamp); + + return length; } - [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] - private async ValueTask WriteLabel(byte[] label, byte[] value, CancellationToken cancel) + private int WriteExemplarLabel(Span buffer, byte[] label, byte[] value) { - await _stream.Value.WriteAsync(label, cancel); - await _stream.Value.WriteAsync(Equal, cancel); - await _stream.Value.WriteAsync(Quote, cancel); - await _stream.Value.WriteAsync(value, cancel); - await _stream.Value.WriteAsync(Quote, cancel); + var position = 0; + + AppendToBufferAndIncrementPosition(label, buffer, ref position); + AppendToBufferAndIncrementPosition(Equal, buffer, ref position); + AppendToBufferAndIncrementPosition(Quote, buffer, ref position); + AppendToBufferAndIncrementPosition(value, buffer, ref position); + AppendToBufferAndIncrementPosition(Quote, buffer, ref position); + + return position; } - [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] - private async ValueTask WriteValue(double value, CancellationToken cancel) + private int MeasureExemplarLabelLength(byte[] label, byte[] value) + { + // We mirror the logic in the Write() call but just measure how many bytes of buffer we need. + var length = 0; + + length += label.Length; + length += Equal.Length; + length += Quote.Length; + length += value.Length; + length += Quote.Length; + + return length; + } + + private int WriteValue(Span buffer, double value) { + var position = 0; + if (_expositionFormat == ExpositionFormat.OpenMetricsText) { switch (value) { case 0: - await _stream.Value.WriteAsync(FloatZero, cancel); - return; + AppendToBufferAndIncrementPosition(FloatZero, buffer, ref position); + return position; case 1: - await _stream.Value.WriteAsync(FloatPositiveOne, cancel); - return; + AppendToBufferAndIncrementPosition(FloatPositiveOne, buffer, ref position); + return position; case -1: - await _stream.Value.WriteAsync(FloatNegativeOne, cancel); - return; + AppendToBufferAndIncrementPosition(FloatNegativeOne, buffer, ref position); + return position; case double.PositiveInfinity: - await _stream.Value.WriteAsync(PositiveInfinity, cancel); - return; + AppendToBufferAndIncrementPosition(PositiveInfinity, buffer, ref position); + return position; case double.NegativeInfinity: - await _stream.Value.WriteAsync(NegativeInfinity, cancel); - return; + AppendToBufferAndIncrementPosition(NegativeInfinity, buffer, ref position); + return position; case double.NaN: - await _stream.Value.WriteAsync(NotANumber, cancel); - return; + AppendToBufferAndIncrementPosition(NotANumber, buffer, ref position); + return position; } } - static bool RequiresDotZero(char[] buffer, int length) - { - return buffer.AsSpan(0..length).IndexOfAny(DotEChar) == -1; /* did not contain .|e */ - } - // Size limit guided by https://stackoverflow.com/questions/21146544/what-is-the-maximum-length-of-double-tostringd if (!value.TryFormat(_stringCharsBuffer, out var charsWritten, "g", CultureInfo.InvariantCulture)) throw new Exception("Failed to encode floating point value as string."); var encodedBytes = PrometheusConstants.ExportEncoding.GetBytes(_stringCharsBuffer, 0, charsWritten, _stringBytesBuffer, 0); - await _stream.Value.WriteAsync(_stringBytesBuffer.AsMemory(0, encodedBytes), cancel); + AppendToBufferAndIncrementPosition(_stringBytesBuffer.AsSpan(0, encodedBytes), buffer, ref position); // In certain places (e.g. "le" label) we need floating point values to actually have the decimal point in them for OpenMetrics. - if (_expositionFormat == ExpositionFormat.OpenMetricsText && RequiresDotZero(_stringCharsBuffer, charsWritten)) - await _stream.Value.WriteAsync(DotZero, cancel); + if (_expositionFormat == ExpositionFormat.OpenMetricsText && RequiresOpenMetricsDotZero(_stringCharsBuffer, charsWritten)) + AppendToBufferAndIncrementPosition(DotZero, buffer, ref position); + + return position; } - [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] - private async ValueTask WriteValue(long value, CancellationToken cancel) + static bool RequiresOpenMetricsDotZero(char[] buffer, int length) + { + return buffer.AsSpan(0..length).IndexOfAny(DotEChar) == -1; /* did not contain .|e, so needs a .0 to turn it into a floating-point value */ + } + + private int MeasureValueMaxLength(double value) { + // We mirror the logic in the Write() call but just measure how many bytes of buffer we need. if (_expositionFormat == ExpositionFormat.OpenMetricsText) { switch (value) { case 0: - await _stream.Value.WriteAsync(IntZero, cancel); - return; + return FloatZero.Length; case 1: - await _stream.Value.WriteAsync(IntPositiveOne, cancel); - return; + return FloatPositiveOne.Length; case -1: - await _stream.Value.WriteAsync(IntNegativeOne, cancel); - return; + return FloatNegativeOne.Length; + case double.PositiveInfinity: + return PositiveInfinity.Length; + case double.NegativeInfinity: + return NegativeInfinity.Length; + case double.NaN: + return NotANumber.Length; + } + } + + // We do not want to spend time formatting the value just to measure the length and throw away the result. + // Therefore we just consider the max length and return it. The max length is just the length of the value-encoding buffer. + return _stringBytesBuffer.Length; + } + + private int WriteValue(Span buffer, long value) + { + var position = 0; + + if (_expositionFormat == ExpositionFormat.OpenMetricsText) + { + switch (value) + { + case 0: + AppendToBufferAndIncrementPosition(IntZero, buffer, ref position); + return position; + case 1: + AppendToBufferAndIncrementPosition(IntPositiveOne, buffer, ref position); + return position; + case -1: + AppendToBufferAndIncrementPosition(IntNegativeOne, buffer, ref position); + return position; } } @@ -245,7 +406,30 @@ private async ValueTask WriteValue(long value, CancellationToken cancel) throw new Exception("Failed to encode integer value as string."); var encodedBytes = PrometheusConstants.ExportEncoding.GetBytes(_stringCharsBuffer, 0, charsWritten, _stringBytesBuffer, 0); - await _stream.Value.WriteAsync(_stringBytesBuffer.AsMemory(0, encodedBytes), cancel); + AppendToBufferAndIncrementPosition(_stringBytesBuffer.AsSpan(0, encodedBytes), buffer, ref position); + + return position; + } + + private int MeasureValueMaxLength(long value) + { + // We mirror the logic in the Write() call but just measure how many bytes of buffer we need. + if (_expositionFormat == ExpositionFormat.OpenMetricsText) + { + switch (value) + { + case 0: + return IntZero.Length; + case 1: + return IntPositiveOne.Length; + case -1: + return IntNegativeOne.Length; + } + } + + // We do not want to spend time formatting the value just to measure the length and throw away the result. + // Therefore we just consider the max length and return it. The max length is just the length of the value-encoding buffer. + return _stringBytesBuffer.Length; } // Reuse a buffer to do the serialization and UTF-8 encoding. @@ -255,54 +439,128 @@ private async ValueTask WriteValue(long value, CancellationToken cancel) private readonly ExpositionFormat _expositionFormat; + private static void AppendToBufferAndIncrementPosition(ReadOnlySpan from, Span to, ref int position) + { + from.CopyTo(to[position..]); + position += from.Length; + } + + private static void ValidateBufferLengthAndPosition(int bufferLength, int position) + { + if (position != bufferLength) + throw new Exception("Internal error: counting the same bytes twice got us a different value."); + } + + private static void ValidateBufferMaxLengthAndPosition(int bufferMaxLength, int position) + { + if (position > bufferMaxLength) + throw new Exception("Internal error: counting the same bytes twice got us a different value."); + } + /// /// Creates a metric identifier, with an optional name postfix and an optional extra label to append to the end. /// familyname_postfix{labelkey1="labelvalue1",labelkey2="labelvalue2"} /// Note: Terminates with a SPACE /// - [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] - private async ValueTask WriteIdentifierPartAsync(byte[] name, byte[] flattenedLabels, CancellationToken cancel, - CanonicalLabel canonicalLabel, byte[]? suffix = null) + private int WriteIdentifierPart(Span buffer, byte[] name, byte[] flattenedLabels, CanonicalLabel extraLabel, byte[]? suffix = null) { - await _stream.Value.WriteAsync(name, cancel); + var position = 0; + + AppendToBufferAndIncrementPosition(name, buffer, ref position); + if (suffix != null && suffix.Length > 0) { - await _stream.Value.WriteAsync(Underscore, cancel); - await _stream.Value.WriteAsync(suffix, cancel); + AppendToBufferAndIncrementPosition(Underscore, buffer, ref position); + AppendToBufferAndIncrementPosition(suffix, buffer, ref position); } - if (flattenedLabels.Length > 0 || canonicalLabel.IsNotEmpty) + if (flattenedLabels.Length > 0 || extraLabel.IsNotEmpty) { - await _stream.Value.WriteAsync(LeftBrace, cancel); + AppendToBufferAndIncrementPosition(LeftBrace, buffer, ref position); if (flattenedLabels.Length > 0) { - await _stream.Value.WriteAsync(flattenedLabels, cancel); + AppendToBufferAndIncrementPosition(flattenedLabels, buffer, ref position); } // Extra labels go to the end (i.e. they are deepest to inherit from). - if (canonicalLabel.IsNotEmpty) + if (extraLabel.IsNotEmpty) { if (flattenedLabels.Length > 0) { - await _stream.Value.WriteAsync(Comma, cancel); + AppendToBufferAndIncrementPosition(Comma, buffer, ref position); } - await _stream.Value.WriteAsync(canonicalLabel.Name.AsMemory(0, canonicalLabel.Name.Length), cancel); - await _stream.Value.WriteAsync(Equal, cancel); - await _stream.Value.WriteAsync(Quote, cancel); + AppendToBufferAndIncrementPosition(extraLabel.Name, buffer, ref position); + AppendToBufferAndIncrementPosition(Equal, buffer, ref position); + AppendToBufferAndIncrementPosition(Quote, buffer, ref position); + if (_expositionFormat == ExpositionFormat.OpenMetricsText) - await _stream.Value.WriteAsync(canonicalLabel.OpenMetrics.AsMemory(0, canonicalLabel.OpenMetrics.Length), cancel); + AppendToBufferAndIncrementPosition(extraLabel.OpenMetrics, buffer, ref position); else - await _stream.Value.WriteAsync(canonicalLabel.Prometheus.AsMemory(0, canonicalLabel.Prometheus.Length), cancel); - await _stream.Value.WriteAsync(Quote, cancel); + AppendToBufferAndIncrementPosition(extraLabel.Prometheus, buffer, ref position); + + AppendToBufferAndIncrementPosition(Quote, buffer, ref position); } - await _stream.Value.WriteAsync(RightBraceSpace, cancel); + AppendToBufferAndIncrementPosition(RightBraceSpace, buffer, ref position); } else { - await _stream.Value.WriteAsync(Space, cancel); + AppendToBufferAndIncrementPosition(Space, buffer, ref position); + } + + return position; + } + + private int MeasureIdentifierPartLength(byte[] name, byte[] flattenedLabels, CanonicalLabel extraLabel, byte[]? suffix = null) + { + // We mirror the logic in the Write() call but just measure how many bytes of buffer we need. + var length = 0; + + length += name.Length; + + if (suffix != null && suffix.Length > 0) + { + length += Underscore.Length; + length += suffix.Length; } + + if (flattenedLabels.Length > 0 || extraLabel.IsNotEmpty) + { + length += LeftBrace.Length; + if (flattenedLabels.Length > 0) + { + length += flattenedLabels.Length; + } + + // Extra labels go to the end (i.e. they are deepest to inherit from). + if (extraLabel.IsNotEmpty) + { + if (flattenedLabels.Length > 0) + { + length += Comma.Length; + } + + length += extraLabel.Name.Length; + length += Equal.Length; + length += Quote.Length; + + if (_expositionFormat == ExpositionFormat.OpenMetricsText) + length += extraLabel.OpenMetrics.Length; + else + length += extraLabel.Prometheus.Length; + + length += Quote.Length; + } + + length += RightBraceSpace.Length; + } + else + { + length += Space.Length; + } + + return length; } /// @@ -343,7 +601,7 @@ internal static CanonicalLabel EncodeValueAsCanonicalLabel(byte[] name, double v openMetricsBytes = new byte[openMetricsByteCount]; Array.Copy(prometheusBytes, openMetricsBytes, prometheusByteCount); - DotZero.CopyTo(openMetricsBytes.AsMemory(prometheusByteCount)); + DotZero.CopyTo(openMetricsBytes.AsSpan(prometheusByteCount)); } else { From ebb3a64bd0bd3c25692f9be2cd0231a4a9d248a8 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Fri, 1 Dec 2023 13:01:45 +0200 Subject: [PATCH 196/230] Bugfix: GetResponseBodyStream() was called too early in AspNetCore server --- Prometheus.AspNetCore/MetricServerMiddleware.cs | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/Prometheus.AspNetCore/MetricServerMiddleware.cs b/Prometheus.AspNetCore/MetricServerMiddleware.cs index 620d7856..0999c32d 100644 --- a/Prometheus.AspNetCore/MetricServerMiddleware.cs +++ b/Prometheus.AspNetCore/MetricServerMiddleware.cs @@ -33,19 +33,9 @@ public sealed class Settings private readonly CollectorRegistry _registry; private readonly bool _enableOpenMetrics; - private sealed record ProtocolNegotiationResult - { - public ExpositionFormat ExpositionFormat { get; } - public string ContentType { get; } - - public ProtocolNegotiationResult(ExpositionFormat expositionFormat, string contentType) - { - ExpositionFormat = expositionFormat; - ContentType = contentType; - } - } + private readonly record struct ProtocolNegotiationResult(ExpositionFormat ExpositionFormat, string ContentType); - private IEnumerable ExtractAcceptableMediaTypes(string acceptHeaderValue) + private static IEnumerable ExtractAcceptableMediaTypes(string acceptHeaderValue) { var candidates = acceptHeaderValue.Split(','); @@ -102,7 +92,7 @@ Stream GetResponseBodyStream() return response.Body; } - var serializer = new TextSerializer(GetResponseBodyStream(), negotiationResult.ExpositionFormat); + var serializer = new TextSerializer(GetResponseBodyStream, negotiationResult.ExpositionFormat); await _registry.CollectAndSerializeAsync(serializer, context.RequestAborted); } From 2cd94801cae9690a2ab7444a514b193a789002b1 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Fri, 1 Dec 2023 13:01:54 +0200 Subject: [PATCH 197/230] Bugfix: filename != class --- ...strelExporterBenchmarks.cs => AspNetCoreExporterBenchmarks.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Benchmark.NetCore/{KestrelExporterBenchmarks.cs => AspNetCoreExporterBenchmarks.cs} (100%) diff --git a/Benchmark.NetCore/KestrelExporterBenchmarks.cs b/Benchmark.NetCore/AspNetCoreExporterBenchmarks.cs similarity index 100% rename from Benchmark.NetCore/KestrelExporterBenchmarks.cs rename to Benchmark.NetCore/AspNetCoreExporterBenchmarks.cs From 32ead35e6302d234ca9c989a0e8db9b6f8c8c3fb Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Fri, 1 Dec 2023 13:31:52 +0200 Subject: [PATCH 198/230] Code tidy --- .../AspNetCoreExporterBenchmarks.cs | 2 + Benchmark.NetCore/MeasurementBenchmarks.cs | 14 +- .../MetricServerMiddleware.cs | 5 +- Prometheus/Collector.cs | 15 +- Prometheus/CollectorRegistry.cs | 20 +- Prometheus/Counter.cs | 5 +- Prometheus/Gauge.cs | 2 +- Prometheus/Histogram.cs | 20 +- Prometheus/IMetricsSerializer.cs | 4 +- Prometheus/StringSequence.cs | 17 +- Prometheus/Summary.cs | 17 +- Prometheus/TextSerializer.Net.cs | 18 +- Prometheus/TextSerializer.NetStandardFx.cs | 32 +- Prometheus/ThreadSafeDouble.cs | 4 +- Tests.NetCore/HistogramTests.cs | 8 +- Tests.NetCore/MeterAdapterTests.cs | 40 +- Tests.NetCore/MetricInitializationTests.cs | 753 +++++++++--------- Tests.NetCore/MetricsTests.cs | 26 +- 18 files changed, 499 insertions(+), 503 deletions(-) diff --git a/Benchmark.NetCore/AspNetCoreExporterBenchmarks.cs b/Benchmark.NetCore/AspNetCoreExporterBenchmarks.cs index 27e710c0..0fd98f6b 100644 --- a/Benchmark.NetCore/AspNetCoreExporterBenchmarks.cs +++ b/Benchmark.NetCore/AspNetCoreExporterBenchmarks.cs @@ -44,6 +44,7 @@ public void Cleanup() private sealed class EntryPoint { +#pragma warning disable CA1822 // Mark members as static public void ConfigureServices(IServiceCollection services) { services.AddRouting(); @@ -66,6 +67,7 @@ public void Configure(IApplicationBuilder app) }); }); } +#pragma warning restore CA1822 // Mark members as static } [Benchmark] diff --git a/Benchmark.NetCore/MeasurementBenchmarks.cs b/Benchmark.NetCore/MeasurementBenchmarks.cs index e9db3e3f..b24bd49b 100644 --- a/Benchmark.NetCore/MeasurementBenchmarks.cs +++ b/Benchmark.NetCore/MeasurementBenchmarks.cs @@ -62,15 +62,17 @@ public enum ExemplarMode // Same but for the regular histogram. private readonly int _regularHistogramMaxValue; + private static readonly string[] labelNames = ["label"]; + public MeasurementBenchmarks() { _registry = Metrics.NewCustomRegistry(); _factory = Metrics.WithCustomRegistry(_registry); // We add a label to each, as labeled usage is the typical usage. - var counterTemplate = _factory.CreateCounter("counter", "test counter", new[] { "label" }); - var gaugeTemplate = _factory.CreateGauge("gauge", "test gauge", new[] { "label" }); - var summaryTemplate = _factory.CreateSummary("summary", "test summary", new[] { "label" }, new SummaryConfiguration + var counterTemplate = _factory.CreateCounter("counter", "test counter", labelNames); + var gaugeTemplate = _factory.CreateGauge("gauge", "test gauge", labelNames); + var summaryTemplate = _factory.CreateSummary("summary", "test summary", labelNames, new SummaryConfiguration { Objectives = new QuantileEpsilonPair[] { @@ -84,14 +86,14 @@ public MeasurementBenchmarks() var regularHistogramBuckets = Prometheus.Histogram.ExponentialBuckets(0.001, 2, 16); // Last one is +inf, so take the second-to-last. - _regularHistogramMaxValue = (int)regularHistogramBuckets[regularHistogramBuckets.Length - 2]; + _regularHistogramMaxValue = (int)regularHistogramBuckets[^2]; - var histogramTemplate = _factory.CreateHistogram("histogram", "test histogram", new[] { "label" }, new HistogramConfiguration + var histogramTemplate = _factory.CreateHistogram("histogram", "test histogram", labelNames, new HistogramConfiguration { Buckets = regularHistogramBuckets }); - var wideHistogramTemplate = _factory.CreateHistogram("wide_histogram", "test histogram", new[] { "label" }, new HistogramConfiguration + var wideHistogramTemplate = _factory.CreateHistogram("wide_histogram", "test histogram", labelNames, new HistogramConfiguration { Buckets = Prometheus.Histogram.LinearBuckets(1, WideHistogramMaxValue / 128, 128) }); diff --git a/Prometheus.AspNetCore/MetricServerMiddleware.cs b/Prometheus.AspNetCore/MetricServerMiddleware.cs index 0999c32d..e7ef2af5 100644 --- a/Prometheus.AspNetCore/MetricServerMiddleware.cs +++ b/Prometheus.AspNetCore/MetricServerMiddleware.cs @@ -108,9 +108,8 @@ Stream GetResponseBodyStream() if (!string.IsNullOrWhiteSpace(ex.Message)) { - using (var writer = new StreamWriter(response.Body, PrometheusConstants.ExportEncoding, - bufferSize: -1, leaveOpen: true)) - await writer.WriteAsync(ex.Message); + using var writer = new StreamWriter(response.Body, PrometheusConstants.ExportEncoding, bufferSize: -1, leaveOpen: true); + await writer.WriteAsync(ex.Message); } } } diff --git a/Prometheus/Collector.cs b/Prometheus/Collector.cs index 81e4f747..425d27d8 100644 --- a/Prometheus/Collector.cs +++ b/Prometheus/Collector.cs @@ -72,9 +72,9 @@ private static void AssignHelpBytes(Collector instance) => private const string ValidLabelNameExpression = "^[a-zA-Z_][a-zA-Z0-9_]*$"; private const string ReservedLabelNameExpression = "^__.*$"; - private static readonly Regex MetricNameRegex = new Regex(ValidMetricNameExpression, RegexOptions.Compiled); - private static readonly Regex LabelNameRegex = new Regex(ValidLabelNameExpression, RegexOptions.Compiled); - private static readonly Regex ReservedLabelRegex = new Regex(ReservedLabelNameExpression, RegexOptions.Compiled); + private static readonly Regex MetricNameRegex = new(ValidMetricNameExpression, RegexOptions.Compiled); + private static readonly Regex LabelNameRegex = new(ValidLabelNameExpression, RegexOptions.Compiled); + private static readonly Regex ReservedLabelRegex = new(ReservedLabelNameExpression, RegexOptions.Compiled); internal Collector(string name, string help, StringSequence instanceLabelNames, LabelSequence staticLabels) { @@ -94,13 +94,10 @@ internal Collector(string name, string help, StringSequence instanceLabelNames, try { - var labelNameEnumerator = FlattenedLabelNames.GetEnumerator(); - while (labelNameEnumerator.MoveNext()) + foreach (var labelName in FlattenedLabelNames) { - var labelName = labelNameEnumerator.Current; - if (labelName == null) - throw new ArgumentNullException("Label name was null."); + throw new ArgumentException("One of the label names was null."); ValidateLabelName(labelName); uniqueLabelNames.Add(labelName); @@ -163,7 +160,7 @@ public abstract class Collector : Collector, ICollector where TChild : ChildBase { // Keyed by the instance labels (not by flattened labels!). - private readonly Dictionary _children = new(); + private readonly Dictionary _children = []; private readonly ReaderWriterLockSlim _childrenLock = new(); // Lazy-initialized since not every collector will use a child with no labels. diff --git a/Prometheus/CollectorRegistry.cs b/Prometheus/CollectorRegistry.cs index 067a394e..b91ebb6c 100644 --- a/Prometheus/CollectorRegistry.cs +++ b/Prometheus/CollectorRegistry.cs @@ -57,8 +57,8 @@ public void AddBeforeCollectCallback(Func callback) _beforeCollectAsyncCallbacks.Add(callback); } - private readonly ConcurrentBag _beforeCollectCallbacks = new ConcurrentBag(); - private readonly ConcurrentBag> _beforeCollectAsyncCallbacks = new ConcurrentBag>(); + private readonly ConcurrentBag _beforeCollectCallbacks = []; + private readonly ConcurrentBag> _beforeCollectAsyncCallbacks = []; #endregion #region Static labels @@ -115,7 +115,7 @@ public void SetStaticLabels(IDictionary labels) } private LabelSequence _staticLabels; - private readonly ReaderWriterLockSlim _staticLabelsLock = new ReaderWriterLockSlim(); + private readonly ReaderWriterLockSlim _staticLabelsLock = new(); internal LabelSequence GetStaticLabels() { @@ -237,7 +237,7 @@ internal void SetBeforeFirstCollectCallback(Action a) /// private Action? _beforeFirstCollectCallback; private bool _hasPerformedFirstCollect; - private readonly object _firstCollectLock = new object(); + private readonly object _firstCollectLock = new(); /// /// Collects metrics from all the registered collectors and sends them to the specified serializer. @@ -327,13 +327,13 @@ internal void StartCollectingRegistryMetrics() { var factory = Metrics.WithCustomRegistry(this); - _metricFamilies = factory.CreateGauge("prometheus_net_metric_families", "Number of metric families currently registered.", labelNames: new[] { MetricTypeDebugLabel }); - _metricInstances = factory.CreateGauge("prometheus_net_metric_instances", "Number of metric instances currently registered across all metric families.", labelNames: new[] { MetricTypeDebugLabel }); - _metricTimeseries = factory.CreateGauge("prometheus_net_metric_timeseries", "Number of metric timeseries currently generated from all metric instances.", labelNames: new[] { MetricTypeDebugLabel }); + _metricFamilies = factory.CreateGauge("prometheus_net_metric_families", "Number of metric families currently registered.", labelNames: [MetricTypeDebugLabel]); + _metricInstances = factory.CreateGauge("prometheus_net_metric_instances", "Number of metric instances currently registered across all metric families.", labelNames: [MetricTypeDebugLabel]); + _metricTimeseries = factory.CreateGauge("prometheus_net_metric_timeseries", "Number of metric timeseries currently generated from all metric instances.", labelNames: [MetricTypeDebugLabel]); - _metricFamiliesPerType = new(); - _metricInstancesPerType = new(); - _metricTimeseriesPerType = new(); + _metricFamiliesPerType = []; + _metricInstancesPerType = []; + _metricTimeseriesPerType = []; foreach (MetricType type in Enum.GetValues(typeof(MetricType))) { diff --git a/Prometheus/Counter.cs b/Prometheus/Counter.cs index bad13fac..3fc48e76 100644 --- a/Prometheus/Counter.cs +++ b/Prometheus/Counter.cs @@ -25,9 +25,10 @@ await serializer.WriteMetricPointAsync( Parent.NameBytes, FlattenedLabelsBytes, CanonicalLabel.Empty, - cancel, Value, - exemplar); + exemplar, + null, + cancel); ReturnBorrowedExemplar(ref _observedExemplar, exemplar); } diff --git a/Prometheus/Gauge.cs b/Prometheus/Gauge.cs index a3e97ce8..c0ed8a12 100644 --- a/Prometheus/Gauge.cs +++ b/Prometheus/Gauge.cs @@ -14,7 +14,7 @@ internal Child(Collector parent, LabelSequence instanceLabels, LabelSequence fla private protected override ValueTask CollectAndSerializeImplAsync(IMetricsSerializer serializer, CancellationToken cancel) { return serializer.WriteMetricPointAsync( - Parent.NameBytes, FlattenedLabelsBytes, CanonicalLabel.Empty, cancel, Value, ObservedExemplar.Empty); + Parent.NameBytes, FlattenedLabelsBytes, CanonicalLabel.Empty, Value, ObservedExemplar.Empty, null, cancel); } public void Inc(double increment = 1) diff --git a/Prometheus/Histogram.cs b/Prometheus/Histogram.cs index 655a9519..15ffc5a7 100644 --- a/Prometheus/Histogram.cs +++ b/Prometheus/Histogram.cs @@ -14,7 +14,7 @@ namespace Prometheus; /// public sealed class Histogram : Collector, IHistogram { - private static readonly double[] DefaultBuckets = { .005, .01, .025, .05, .075, .1, .25, .5, .75, 1, 2.5, 5, 7.5, 10 }; + private static readonly double[] DefaultBuckets = [.005, .01, .025, .05, .075, .1, .25, .5, .75, 1, 2.5, 5, 7.5, 10]; private readonly double[] _buckets; @@ -50,7 +50,7 @@ internal Histogram(string name, string help, StringSequence instanceLabelNames, if (!double.IsPositiveInfinity(_buckets[_buckets.Length - 1])) { - _buckets = _buckets.Concat(new[] { double.PositiveInfinity }).ToArray(); + _buckets = [.. _buckets, double.PositiveInfinity]; } for (int i = 1; i < _buckets.Length; i++) @@ -116,7 +116,7 @@ internal Child(Histogram parent, LabelSequence instanceLabels, LabelSequence fla internal new readonly Histogram Parent; - private ThreadSafeDouble _sum = new ThreadSafeDouble(0.0D); + private ThreadSafeDouble _sum = new(0.0D); private readonly ThreadSafeLong[] _bucketCounts; private static readonly byte[] SumSuffix = PrometheusConstants.ExportEncoding.GetBytes("sum"); private static readonly byte[] CountSuffix = PrometheusConstants.ExportEncoding.GetBytes("count"); @@ -136,18 +136,18 @@ await serializer.WriteMetricPointAsync( Parent.NameBytes, FlattenedLabelsBytes, CanonicalLabel.Empty, - cancel, _sum.Value, ObservedExemplar.Empty, - suffix: SumSuffix); + SumSuffix, + cancel); await serializer.WriteMetricPointAsync( Parent.NameBytes, FlattenedLabelsBytes, CanonicalLabel.Empty, - cancel, Count, ObservedExemplar.Empty, - suffix: CountSuffix); + CountSuffix, + cancel); var cumulativeCount = 0L; @@ -160,10 +160,10 @@ await serializer.WriteMetricPointAsync( Parent.NameBytes, FlattenedLabelsBytes, Parent._leLabels[i], - cancel, cumulativeCount, exemplar, - suffix: BucketSuffix); + BucketSuffix, + cancel); ReturnBorrowedExemplar(ref _exemplars[i], exemplar); } @@ -395,7 +395,7 @@ public static double[] PowersOfTenDividedBuckets(int startPower, int endPower, i } } - return buckets.ToArray(); + return [.. buckets]; } // sum + count + buckets diff --git a/Prometheus/IMetricsSerializer.cs b/Prometheus/IMetricsSerializer.cs index a6ae7d3b..3a949596 100644 --- a/Prometheus/IMetricsSerializer.cs +++ b/Prometheus/IMetricsSerializer.cs @@ -17,13 +17,13 @@ ValueTask WriteFamilyDeclarationAsync(string name, byte[] nameBytes, byte[] help /// Writes out a single metric point with a floating point value. /// ValueTask WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, CanonicalLabel canonicalLabel, - CancellationToken cancel, double value, ObservedExemplar exemplar, byte[]? suffix = null); + double value, ObservedExemplar exemplar, byte[]? suffix, CancellationToken cancel); /// /// Writes out a single metric point with an integer value. /// ValueTask WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, CanonicalLabel canonicalLabel, - CancellationToken cancel, long value, ObservedExemplar exemplar, byte[]? suffix = null); + long value, ObservedExemplar exemplar, byte[]? suffix, CancellationToken cancel); /// /// Writes out terminal lines diff --git a/Prometheus/StringSequence.cs b/Prometheus/StringSequence.cs index 7ca97c06..00066fe3 100644 --- a/Prometheus/StringSequence.cs +++ b/Prometheus/StringSequence.cs @@ -262,12 +262,11 @@ private int CalculateHashCode() { int hashCode = 0; - var enumerator = GetEnumerator(); - while (enumerator.MoveNext()) + foreach (var item in this) { unchecked { - hashCode ^= (enumerator.Current.GetHashCode() * 397); + hashCode ^= (item.GetHashCode() * 397); } } @@ -276,10 +275,9 @@ private int CalculateHashCode() public bool Contains(string value) { - var enumerator = GetEnumerator(); - while (enumerator.MoveNext()) + foreach (var item in this) { - if (enumerator.Current.Equals(value, StringComparison.Ordinal)) + if (item.Equals(value, StringComparison.Ordinal)) return true; } @@ -293,13 +291,10 @@ public string[] ToArray() { var result = new string[Length]; - var enumerator = GetEnumerator(); var index = 0; - while (enumerator.MoveNext()) - { - result[index++] = enumerator.Current; - } + foreach (var item in this) + result[index++] = item; return result; } diff --git a/Prometheus/Summary.cs b/Prometheus/Summary.cs index 64450c0b..a04b37cd 100644 --- a/Prometheus/Summary.cs +++ b/Prometheus/Summary.cs @@ -189,18 +189,18 @@ await serializer.WriteMetricPointAsync( Parent.NameBytes, FlattenedLabelsBytes, CanonicalLabel.Empty, - cancel, sum, ObservedExemplar.Empty, - suffix: SumSuffix); + SumSuffix, + cancel); await serializer.WriteMetricPointAsync( Parent.NameBytes, FlattenedLabelsBytes, CanonicalLabel.Empty, - cancel, count, ObservedExemplar.Empty, - suffix: CountSuffix); + CountSuffix, + cancel); for (var i = 0; i < _parent._objectives.Count; i++) { @@ -208,9 +208,10 @@ await serializer.WriteMetricPointAsync( Parent.NameBytes, FlattenedLabelsBytes, _parent._quantileLabels[i], - cancel, values[i].value, - ObservedExemplar.Empty); + ObservedExemplar.Empty, + null, + cancel); } } finally @@ -283,9 +284,7 @@ private void SwapBufs(double nowUnixtimeSeconds) if (!_coldBuf.IsEmpty) throw new InvalidOperationException("coldBuf is not empty"); - var temp = _hotBuf; - _hotBuf = _coldBuf; - _coldBuf = temp; + (_coldBuf, _hotBuf) = (_hotBuf, _coldBuf); // hotBuf is now empty and gets new expiration set. while (nowUnixtimeSeconds > _hotBufExpUnixtimeSeconds) diff --git a/Prometheus/TextSerializer.Net.cs b/Prometheus/TextSerializer.Net.cs index 6e7bc653..38615b9a 100644 --- a/Prometheus/TextSerializer.Net.cs +++ b/Prometheus/TextSerializer.Net.cs @@ -34,8 +34,8 @@ internal sealed class TextSerializer : IMetricsSerializer internal static ReadOnlySpan NewlineHashTypeSpace => [(byte)'\n', (byte)'#', (byte)' ', (byte)'T', (byte)'Y', (byte)'P', (byte)'E', (byte)' ']; internal static readonly byte[] UnknownBytes = PrometheusConstants.ExportEncoding.GetBytes("unknown"); - internal static readonly byte[] EofNewLineBytes = { (byte)'#', (byte)' ', (byte)'E', (byte)'O', (byte)'F', (byte)'\n' }; - internal static readonly byte[] PositiveInfinityBytes = PrometheusConstants.ExportEncoding.GetBytes("+Inf"); + internal static readonly byte[] EofNewLineBytes = [(byte)'#', (byte)' ', (byte)'E', (byte)'O', (byte)'F', (byte)'\n']; + internal static readonly byte[] PositiveInfinityBytes = [(byte)'+', (byte)'I', (byte)'n', (byte)'f']; internal static readonly Dictionary MetricTypeToBytes = new() { @@ -45,7 +45,7 @@ internal sealed class TextSerializer : IMetricsSerializer { MetricType.Summary, PrometheusConstants.ExportEncoding.GetBytes("summary") }, }; - private static readonly char[] DotEChar = { '.', 'e' }; + private static readonly char[] DotEChar = ['.', 'e']; public TextSerializer(Stream stream, ExpositionFormat fmt = ExpositionFormat.PrometheusText) { @@ -169,7 +169,7 @@ public async ValueTask WriteEnd(CancellationToken cancel) [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] public async ValueTask WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, CanonicalLabel canonicalLabel, - CancellationToken cancel, double value, ObservedExemplar exemplar, byte[]? suffix = null) + double value, ObservedExemplar exemplar, byte[]? suffix, CancellationToken cancel) { // This is a max length because we do not know ahead of time how many bytes the actual value will consume. var bufferMaxLength = MeasureIdentifierPartLength(name, flattenedLabels, canonicalLabel, suffix) + MeasureValueMaxLength(value) + NewLine.Length; @@ -204,7 +204,7 @@ public async ValueTask WriteMetricPointAsync(byte[] name, byte[] flattenedLabels [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] public async ValueTask WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, CanonicalLabel canonicalLabel, - CancellationToken cancel, long value, ObservedExemplar exemplar, byte[]? suffix = null) + long value, ObservedExemplar exemplar, byte[]? suffix, CancellationToken cancel) { // This is a max length because we do not know ahead of time how many bytes the actual value will consume. var bufferMaxLength = MeasureIdentifierPartLength(name, flattenedLabels, canonicalLabel, suffix) + MeasureValueMaxLength(value) + NewLine.Length; @@ -247,7 +247,7 @@ private int WriteExemplar(Span buffer, ObservedExemplar exemplar) if (i > 0) AppendToBufferAndIncrementPosition(Comma, buffer, ref position); - position += WriteExemplarLabel(buffer[position..], exemplar.Labels!.Buffer[i].KeyBytes, exemplar.Labels!.Buffer[i].ValueBytes); + position += TextSerializer.WriteExemplarLabel(buffer[position..], exemplar.Labels!.Buffer[i].KeyBytes, exemplar.Labels!.Buffer[i].ValueBytes); } AppendToBufferAndIncrementPosition(RightBraceSpace, buffer, ref position); @@ -269,7 +269,7 @@ private int MeasureExemplarMaxLength(ObservedExemplar exemplar) if (i > 0) length += Comma.Length; - length += MeasureExemplarLabelLength(exemplar.Labels!.Buffer[i].KeyBytes, exemplar.Labels!.Buffer[i].ValueBytes); + length += TextSerializer.MeasureExemplarLabelLength(exemplar.Labels!.Buffer[i].KeyBytes, exemplar.Labels!.Buffer[i].ValueBytes); } length += RightBraceSpace.Length; @@ -280,7 +280,7 @@ private int MeasureExemplarMaxLength(ObservedExemplar exemplar) return length; } - private int WriteExemplarLabel(Span buffer, byte[] label, byte[] value) + private static int WriteExemplarLabel(Span buffer, byte[] label, byte[] value) { var position = 0; @@ -293,7 +293,7 @@ private int WriteExemplarLabel(Span buffer, byte[] label, byte[] value) return position; } - private int MeasureExemplarLabelLength(byte[] label, byte[] value) + private static int MeasureExemplarLabelLength(byte[] label, byte[] value) { // We mirror the logic in the Write() call but just measure how many bytes of buffer we need. var length = 0; diff --git a/Prometheus/TextSerializer.NetStandardFx.cs b/Prometheus/TextSerializer.NetStandardFx.cs index fa262ca0..fd3c7ad4 100644 --- a/Prometheus/TextSerializer.NetStandardFx.cs +++ b/Prometheus/TextSerializer.NetStandardFx.cs @@ -8,15 +8,15 @@ namespace Prometheus; /// internal sealed class TextSerializer : IMetricsSerializer { - internal static readonly byte[] NewLine = { (byte)'\n' }; - internal static readonly byte[] Quote = { (byte)'"' }; - internal static readonly byte[] Equal = { (byte)'=' }; - internal static readonly byte[] Comma = { (byte)',' }; - internal static readonly byte[] Underscore = { (byte)'_' }; - internal static readonly byte[] LeftBrace = { (byte)'{' }; - internal static readonly byte[] RightBraceSpace = { (byte)'}', (byte)' ' }; - internal static readonly byte[] Space = { (byte)' ' }; - internal static readonly byte[] SpaceHashSpaceLeftBrace = { (byte)' ', (byte)'#', (byte)' ', (byte)'{' }; + internal static readonly byte[] NewLine = [(byte)'\n']; + internal static readonly byte[] Quote = [(byte)'"']; + internal static readonly byte[] Equal = [(byte)'=']; + internal static readonly byte[] Comma = [(byte)',']; + internal static readonly byte[] Underscore = [(byte)'_']; + internal static readonly byte[] LeftBrace = [(byte)'{']; + internal static readonly byte[] RightBraceSpace = [(byte)'}', (byte)' ']; + internal static readonly byte[] Space = [(byte)' ']; + internal static readonly byte[] SpaceHashSpaceLeftBrace = [(byte)' ', (byte)'#', (byte)' ', (byte)'{']; internal static readonly byte[] PositiveInfinity = PrometheusConstants.ExportEncoding.GetBytes("+Inf"); internal static readonly byte[] NegativeInfinity = PrometheusConstants.ExportEncoding.GetBytes("-Inf"); internal static readonly byte[] NotANumber = PrometheusConstants.ExportEncoding.GetBytes("NaN"); @@ -43,7 +43,7 @@ internal sealed class TextSerializer : IMetricsSerializer { MetricType.Summary, PrometheusConstants.ExportEncoding.GetBytes("summary") }, }; - private static readonly char[] DotEChar = { '.', 'e' }; + private static readonly char[] DotEChar = ['.', 'e']; public TextSerializer(Stream stream, ExpositionFormat fmt = ExpositionFormat.PrometheusText) { @@ -119,9 +119,9 @@ public async ValueTask WriteEnd(CancellationToken cancel) } public async ValueTask WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, CanonicalLabel canonicalLabel, - CancellationToken cancel, double value, ObservedExemplar exemplar, byte[]? suffix = null) + double value, ObservedExemplar exemplar, byte[]? suffix, CancellationToken cancel) { - await WriteIdentifierPartAsync(name, flattenedLabels, cancel, canonicalLabel, suffix); + await WriteIdentifierPartAsync(name, flattenedLabels, canonicalLabel, suffix, cancel); await WriteValue(value, cancel); if (_expositionFormat == ExpositionFormat.OpenMetricsText && exemplar.IsValid) @@ -133,9 +133,9 @@ public async ValueTask WriteMetricPointAsync(byte[] name, byte[] flattenedLabels } public async ValueTask WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, CanonicalLabel canonicalLabel, - CancellationToken cancel, long value, ObservedExemplar exemplar, byte[]? suffix = null) + long value, ObservedExemplar exemplar, byte[]? suffix, CancellationToken cancel) { - await WriteIdentifierPartAsync(name, flattenedLabels, cancel, canonicalLabel, suffix); + await WriteIdentifierPartAsync(name, flattenedLabels, canonicalLabel, suffix, cancel); await WriteValue(value, cancel); if (_expositionFormat == ExpositionFormat.OpenMetricsText && exemplar.IsValid) @@ -243,8 +243,8 @@ private async Task WriteValue(long value, CancellationToken cancel) /// familyname_postfix{labelkey1="labelvalue1",labelkey2="labelvalue2"} /// Note: Terminates with a SPACE /// - private async Task WriteIdentifierPartAsync(byte[] name, byte[] flattenedLabels, CancellationToken cancel, - CanonicalLabel canonicalLabel, byte[]? suffix = null) + private async Task WriteIdentifierPartAsync(byte[] name, byte[] flattenedLabels, + CanonicalLabel canonicalLabel, byte[]? suffix, CancellationToken cancel) { await _stream.Value.WriteAsync(name, 0, name.Length, cancel); if (suffix != null && suffix.Length > 0) diff --git a/Prometheus/ThreadSafeDouble.cs b/Prometheus/ThreadSafeDouble.cs index d78b9590..5f24e99b 100644 --- a/Prometheus/ThreadSafeDouble.cs +++ b/Prometheus/ThreadSafeDouble.cs @@ -78,8 +78,8 @@ public override string ToString() public override bool Equals(object? obj) { - if (obj is ThreadSafeDouble) - return Value.Equals(((ThreadSafeDouble)obj).Value); + if (obj is ThreadSafeDouble other) + return Value.Equals(other.Value); return Value.Equals(obj); } diff --git a/Tests.NetCore/HistogramTests.cs b/Tests.NetCore/HistogramTests.cs index 94878a55..36c75583 100644 --- a/Tests.NetCore/HistogramTests.cs +++ b/Tests.NetCore/HistogramTests.cs @@ -89,18 +89,18 @@ public async Task Observe_IncrementsCorrectBucketsAndCountAndSum() await histogram.CollectAndSerializeAsync(serializer, true, default); // Sum - await serializer.Received().WriteMetricPointAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), 5.0, Arg.Any(), Arg.Any()); + await serializer.Received().WriteMetricPointAsync(Arg.Any(), Arg.Any(), Arg.Any(), 5.0, Arg.Any(), Arg.Any(), Arg.Any()); // 1.0 bucket - await serializer.Received().WriteMetricPointAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), 0, Arg.Any(), Arg.Any()); + await serializer.Received().WriteMetricPointAsync(Arg.Any(), Arg.Any(), Arg.Any(), 0, Arg.Any(), Arg.Any(), Arg.Any()); // 2.0 bucket - await serializer.Received().WriteMetricPointAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), 1, Arg.Any(), Arg.Any()); + await serializer.Received().WriteMetricPointAsync(Arg.Any(), Arg.Any(), Arg.Any(), 1, Arg.Any(), Arg.Any(), Arg.Any()); // Count // 3.0 bucket // +inf bucket - await serializer.Received(requiredNumberOfCalls: 3).WriteMetricPointAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), 2, Arg.Any(), Arg.Any()); + await serializer.Received(requiredNumberOfCalls: 3).WriteMetricPointAsync(Arg.Any(), Arg.Any(), Arg.Any(), 2, Arg.Any(), Arg.Any(), Arg.Any()); } [TestMethod] diff --git a/Tests.NetCore/MeterAdapterTests.cs b/Tests.NetCore/MeterAdapterTests.cs index cc4bc7cc..a3a022a4 100644 --- a/Tests.NetCore/MeterAdapterTests.cs +++ b/Tests.NetCore/MeterAdapterTests.cs @@ -10,15 +10,14 @@ namespace Prometheus.Tests; [TestClass] -public class MeterAdapterTests: IDisposable +public sealed class MeterAdapterTests : IDisposable { - private CollectorRegistry _registry; - private MetricFactory _metrics; + private readonly CollectorRegistry _registry; + private readonly MetricFactory _metrics; private readonly SDM.Meter _meter = new("test"); private readonly SDM.Counter _intCounter; private readonly SDM.Counter _floatCounter; - private readonly SDM.Histogram _histogram; - private IDisposable _adapter; + private readonly IDisposable _adapter; public MeterAdapterTests() { @@ -27,13 +26,14 @@ public MeterAdapterTests() _intCounter = _meter.CreateCounter("int_counter"); _floatCounter = _meter.CreateCounter("float_counter"); - _histogram = _meter.CreateHistogram("histogram"); _registry = Metrics.NewCustomRegistry(); _metrics = Metrics.WithCustomRegistry(_registry); - _adapter = MeterAdapter.StartListening(new MeterAdapterOptions { - InstrumentFilterPredicate = instrument => { + _adapter = MeterAdapter.StartListening(new MeterAdapterOptions + { + InstrumentFilterPredicate = instrument => + { return instrument.Meter == _meter; }, Registry = _registry, @@ -42,7 +42,7 @@ public MeterAdapterTests() }); } - private FakeSerializer SerializeMetrics(CollectorRegistry registry) + private static FakeSerializer SerializeMetrics(CollectorRegistry registry) { var serializer = new FakeSerializer(); registry.CollectAndSerializeAsync(serializer, default).Wait(); @@ -68,6 +68,7 @@ private double GetValue(CollectorRegistry registry, string meterName, params (st } if (serializer.Data.Any(d => d.name == meterName)) throw new Exception($"Metric {meterName}{{{labelsString}}} not found, only these labels were found: {string.Join(" / ", serializer.Data.Where(d => d.name == meterName).Select(d => d.labels))}"); + throw new Exception($"Metric {meterName} not found, only these metrics were found: {string.Join(" / ", serializer.Data.Select(d => d.name).Distinct())}"); } @@ -92,12 +93,12 @@ public void CounterFloat() [TestMethod] public void CounterLabels() { - _intCounter.Add(1, new ("l1", "value"), new ("l2", 111)); + _intCounter.Add(1, new("l1", "value"), new("l2", 111)); Assert.AreEqual(1, GetValue("test_int_counter", ("l1", "value"), ("l2", "111"))); _intCounter.Add(1000); - _intCounter.Add(1000, new ("l1", "value"), new ("l2", 0)); + _intCounter.Add(1000, new("l1", "value"), new("l2", 0)); _intCounter.Add(1000, new KeyValuePair("l1", "value")); - _intCounter.Add(1, new ("l2", 111), new ("l1", "value")); + _intCounter.Add(1, new("l2", 111), new("l1", "value")); Assert.AreEqual(2, GetValue("test_int_counter", ("l1", "value"), ("l2", "111"))); Assert.AreEqual(1000, GetValue("test_int_counter", ("l1", "value"), ("l2", "0"))); Assert.AreEqual(1000, GetValue("test_int_counter", ("l1", "value"))); @@ -107,7 +108,7 @@ public void CounterLabels() [TestMethod] public void LabelRenaming() { - _intCounter.Add(1, new ("my-label", 1), new ("Another.Label", 1)); + _intCounter.Add(1, new("my-label", 1), new("Another.Label", 1)); Assert.AreEqual(1, GetValue("test_int_counter", ("another_label", "1"), ("my_label", "1"))); } @@ -120,8 +121,10 @@ public void MultipleInstances() var registry2 = Metrics.NewCustomRegistry(); var metrics2 = Metrics.WithCustomRegistry(registry2); - var adapter2 = MeterAdapter.StartListening(new MeterAdapterOptions { - InstrumentFilterPredicate = instrument => { + var adapter2 = MeterAdapter.StartListening(new MeterAdapterOptions + { + InstrumentFilterPredicate = instrument => + { return instrument.Meter == _meter; }, Registry = registry2, @@ -140,7 +143,6 @@ public void MultipleInstances() Assert.AreEqual(1, GetValue(registry2, "test_int_counter")); } - public void Dispose() { _adapter.Dispose(); @@ -154,7 +156,7 @@ class FakeSerializer : IMetricsSerializer public ValueTask WriteFamilyDeclarationAsync(string name, byte[] nameBytes, byte[] helpBytes, MetricType type, byte[] typeBytes, CancellationToken cancel) => default; - public ValueTask WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, CanonicalLabel canonicalLabel, CancellationToken cancel, double value, ObservedExemplar exemplar, byte[] suffix = null) + public ValueTask WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, CanonicalLabel canonicalLabel, double value, ObservedExemplar exemplar, byte[] suffix, CancellationToken cancel) { Data.Add(( name: Encoding.UTF8.GetString(name), @@ -166,7 +168,7 @@ public ValueTask WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, Cano return default; } - public ValueTask WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, CanonicalLabel canonicalLabel, CancellationToken cancel, long value, ObservedExemplar exemplar, byte[] suffix = null) => - WriteMetricPointAsync(name, flattenedLabels, canonicalLabel, cancel, (double)value, exemplar, suffix); + public ValueTask WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, CanonicalLabel canonicalLabel, long value, ObservedExemplar exemplar, byte[] suffix, CancellationToken cancel) => + WriteMetricPointAsync(name, flattenedLabels, canonicalLabel, (double)value, exemplar, suffix, cancel); } } diff --git a/Tests.NetCore/MetricInitializationTests.cs b/Tests.NetCore/MetricInitializationTests.cs index 8d42a1d8..04ceac5c 100644 --- a/Tests.NetCore/MetricInitializationTests.cs +++ b/Tests.NetCore/MetricInitializationTests.cs @@ -2,422 +2,421 @@ using NSubstitute; using System.Threading.Tasks; -namespace Prometheus.Tests +namespace Prometheus.Tests; + +[TestClass] +public sealed class MetricInitializationTests { - [TestClass] - public sealed class MetricInitializationTests + private static HistogramConfiguration NewHistogramConfiguration() => new() { - private static HistogramConfiguration NewHistogramConfiguration() => new HistogramConfiguration - { - // This results in 4 metrics - sum, count, 1.0, +Inf - Buckets = new[] { 1.0 } - }; + // This results in 4 metrics - sum, count, 1.0, +Inf + Buckets = new[] { 1.0 } + }; - private static SummaryConfiguration NewSummaryConfiguration() => new SummaryConfiguration - { - // This results in 3 metrics - sum, count, 0.1 - Objectives = new[] - { - new QuantileEpsilonPair(0.1, 0.05) - } - }; - - #region Unlabelled logic - [TestMethod] - public async Task CreatingUnlabelledMetric_WithoutObservingAnyData_ExportsImmediately() + private static SummaryConfiguration NewSummaryConfiguration() => new() + { + // This results in 3 metrics - sum, count, 0.1 + Objectives = new[] { - var registry = Metrics.NewCustomRegistry(); - var factory = Metrics.WithCustomRegistry(registry); - - var summaryConfig = NewSummaryConfiguration(); - var histogramConfig = NewHistogramConfiguration(); - - var gauge = factory.CreateGauge("gauge", "", new GaugeConfiguration - { - }); - var counter = factory.CreateCounter("counter", "", new CounterConfiguration - { - }); - var summary = factory.CreateSummary("summary", "", summaryConfig); - var histogram = factory.CreateHistogram("histogram", "", histogramConfig); - // 4 families with 9 metrics total. - - var serializer = Substitute.For(); - await registry.CollectAndSerializeAsync(serializer, default); - - // Without touching any metrics, there should be output for all because default config publishes immediately. - - await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default, default, default, default, default); - // gauge + counter + summary.sum + histogram.sum + summary quantile - await serializer.ReceivedWithAnyArgs(5).WriteMetricPointAsync(default, default, default, default, default(double), default); - // summary.count + 2x histogram bucket + histogram count - await serializer.ReceivedWithAnyArgs(4).WriteMetricPointAsync(default, default, default, default, default(long), default); + new QuantileEpsilonPair(0.1, 0.05) } + }; + + #region Unlabelled logic + [TestMethod] + public async Task CreatingUnlabelledMetric_WithoutObservingAnyData_ExportsImmediately() + { + var registry = Metrics.NewCustomRegistry(); + var factory = Metrics.WithCustomRegistry(registry); + + var summaryConfig = NewSummaryConfiguration(); + var histogramConfig = NewHistogramConfiguration(); - [TestMethod] - public async Task CreatingUnlabelledMetric_WithInitialValueSuppression_ExportsNothingByDefault() + var gauge = factory.CreateGauge("gauge", "", new GaugeConfiguration { - var registry = Metrics.NewCustomRegistry(); - var factory = Metrics.WithCustomRegistry(registry); - - var sumamryConfig = NewSummaryConfiguration(); - sumamryConfig.SuppressInitialValue = true; - - var histogramConfig = NewHistogramConfiguration(); - histogramConfig.SuppressInitialValue = true; - - var gauge = factory.CreateGauge("gauge", "", new GaugeConfiguration - { - SuppressInitialValue = true - }); - var counter = factory.CreateCounter("counter", "", new CounterConfiguration - { - SuppressInitialValue = true - }); - var summary = factory.CreateSummary("summary", "", sumamryConfig); - var histogram = factory.CreateHistogram("histogram", "", histogramConfig); - // 4 families with 9 metrics total. - - var serializer = Substitute.For(); - await registry.CollectAndSerializeAsync(serializer, default); - - // There is a family for each of the above, in each family we expect to see 0 metrics. - await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default, default, default, default, default); - await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default(double), default); - await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default(long), default); - } + }); + var counter = factory.CreateCounter("counter", "", new CounterConfiguration + { + }); + var summary = factory.CreateSummary("summary", "", summaryConfig); + var histogram = factory.CreateHistogram("histogram", "", histogramConfig); + // 4 families with 9 metrics total. + + var serializer = Substitute.For(); + await registry.CollectAndSerializeAsync(serializer, default); - [TestMethod] - public async Task CreatingUnlabelledMetric_WithInitialValueSuppression_ExportsAfterValueChange() + // Without touching any metrics, there should be output for all because default config publishes immediately. + + await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default, default, default, default, default); + // gauge + counter + summary.sum + histogram.sum + summary quantile + await serializer.ReceivedWithAnyArgs(5).WriteMetricPointAsync(default, default, default, default(double), default, default, default); + // summary.count + 2x histogram bucket + histogram count + await serializer.ReceivedWithAnyArgs(4).WriteMetricPointAsync(default, default, default, default, default, default, default); + } + + [TestMethod] + public async Task CreatingUnlabelledMetric_WithInitialValueSuppression_ExportsNothingByDefault() + { + var registry = Metrics.NewCustomRegistry(); + var factory = Metrics.WithCustomRegistry(registry); + + var sumamryConfig = NewSummaryConfiguration(); + sumamryConfig.SuppressInitialValue = true; + + var histogramConfig = NewHistogramConfiguration(); + histogramConfig.SuppressInitialValue = true; + + var gauge = factory.CreateGauge("gauge", "", new GaugeConfiguration { - var registry = Metrics.NewCustomRegistry(); - var factory = Metrics.WithCustomRegistry(registry); - - var sumamryConfig = NewSummaryConfiguration(); - sumamryConfig.SuppressInitialValue = true; - - var histogramConfig = NewHistogramConfiguration(); - histogramConfig.SuppressInitialValue = true; - - var gauge = factory.CreateGauge("gauge", "", new GaugeConfiguration - { - SuppressInitialValue = true - }); - var counter = factory.CreateCounter("counter", "", new CounterConfiguration - { - SuppressInitialValue = true - }); - var summary = factory.CreateSummary("summary", "", sumamryConfig); - var histogram = factory.CreateHistogram("histogram", "", histogramConfig); - // 4 families with 9 metrics total. - - gauge.Set(123); - counter.Inc(); - summary.Observe(123); - histogram.Observe(31); - - var serializer = Substitute.For(); - await registry.CollectAndSerializeAsync(serializer, default); - - // Even though suppressed, they all now have values so should all be published. - await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default, default, default, default, default); - // gauge + counter + summary.sum + histogram.sum + summary quantile - await serializer.ReceivedWithAnyArgs(5).WriteMetricPointAsync(default, default, default, default, default(double), default); - // summary.count + 2x histogram bucket + histogram count - await serializer.ReceivedWithAnyArgs(4).WriteMetricPointAsync(default, default, default, default, default(long), default); - } + SuppressInitialValue = true + }); + var counter = factory.CreateCounter("counter", "", new CounterConfiguration + { + SuppressInitialValue = true + }); + var summary = factory.CreateSummary("summary", "", sumamryConfig); + var histogram = factory.CreateHistogram("histogram", "", histogramConfig); + // 4 families with 9 metrics total. + + var serializer = Substitute.For(); + await registry.CollectAndSerializeAsync(serializer, default); + + // There is a family for each of the above, in each family we expect to see 0 metrics. + await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default, default, default, default, default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default(double), default, default, default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default, default, default); + } + + [TestMethod] + public async Task CreatingUnlabelledMetric_WithInitialValueSuppression_ExportsAfterValueChange() + { + var registry = Metrics.NewCustomRegistry(); + var factory = Metrics.WithCustomRegistry(registry); + + var sumamryConfig = NewSummaryConfiguration(); + sumamryConfig.SuppressInitialValue = true; + + var histogramConfig = NewHistogramConfiguration(); + histogramConfig.SuppressInitialValue = true; - [TestMethod] - public async Task CreatingUnlabelledMetric_WithInitialValueSuppression_ExportsAfterPublish() + var gauge = factory.CreateGauge("gauge", "", new GaugeConfiguration { - var registry = Metrics.NewCustomRegistry(); - var factory = Metrics.WithCustomRegistry(registry); - - var sumamryConfig = NewSummaryConfiguration(); - sumamryConfig.SuppressInitialValue = true; - - var histogramConfig = NewHistogramConfiguration(); - histogramConfig.SuppressInitialValue = true; - - var gauge = factory.CreateGauge("gauge", "", new GaugeConfiguration - { - SuppressInitialValue = true - }); - var counter = factory.CreateCounter("counter", "", new CounterConfiguration - { - SuppressInitialValue = true - }); - var summary = factory.CreateSummary("summary", "", sumamryConfig); - var histogram = factory.CreateHistogram("histogram", "", histogramConfig); - // 4 families with 9 metrics total. - - gauge.Publish(); - counter.Publish(); - summary.Publish(); - histogram.Publish(); - - var serializer = Substitute.For(); - await registry.CollectAndSerializeAsync(serializer, default); - - // Even though suppressed, they were all explicitly published. - await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default, default, default, default, default); - - // gauge + counter + summary.sum + histogram.sum + summary quantile - await serializer.ReceivedWithAnyArgs(5).WriteMetricPointAsync(default, default, default, default, default(double), default); - // summary.count + 2x histogram bucket + histogram count - await serializer.ReceivedWithAnyArgs(4).WriteMetricPointAsync(default, default, default, default, default(long), default); - } - #endregion + SuppressInitialValue = true + }); + var counter = factory.CreateCounter("counter", "", new CounterConfiguration + { + SuppressInitialValue = true + }); + var summary = factory.CreateSummary("summary", "", sumamryConfig); + var histogram = factory.CreateHistogram("histogram", "", histogramConfig); + // 4 families with 9 metrics total. + + gauge.Set(123); + counter.Inc(); + summary.Observe(123); + histogram.Observe(31); + + var serializer = Substitute.For(); + await registry.CollectAndSerializeAsync(serializer, default); + + // Even though suppressed, they all now have values so should all be published. + await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default, default, default, default, default); + // gauge + counter + summary.sum + histogram.sum + summary quantile + await serializer.ReceivedWithAnyArgs(5).WriteMetricPointAsync(default, default, default, default(double), default, default, default); + // summary.count + 2x histogram bucket + histogram count + await serializer.ReceivedWithAnyArgs(4).WriteMetricPointAsync(default, default, default, default, default, default, default); + } + + [TestMethod] + public async Task CreatingUnlabelledMetric_WithInitialValueSuppression_ExportsAfterPublish() + { + var registry = Metrics.NewCustomRegistry(); + var factory = Metrics.WithCustomRegistry(registry); + + var sumamryConfig = NewSummaryConfiguration(); + sumamryConfig.SuppressInitialValue = true; + + var histogramConfig = NewHistogramConfiguration(); + histogramConfig.SuppressInitialValue = true; - #region Labelled logic - [TestMethod] - public async Task CreatingLabelledMetric_WithoutObservingAnyData_ExportsImmediately() + var gauge = factory.CreateGauge("gauge", "", new GaugeConfiguration { - var registry = Metrics.NewCustomRegistry(); - var factory = Metrics.WithCustomRegistry(registry); + SuppressInitialValue = true + }); + var counter = factory.CreateCounter("counter", "", new CounterConfiguration + { + SuppressInitialValue = true + }); + var summary = factory.CreateSummary("summary", "", sumamryConfig); + var histogram = factory.CreateHistogram("histogram", "", histogramConfig); + // 4 families with 9 metrics total. + + gauge.Publish(); + counter.Publish(); + summary.Publish(); + histogram.Publish(); + + var serializer = Substitute.For(); + await registry.CollectAndSerializeAsync(serializer, default); + + // Even though suppressed, they were all explicitly published. + await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default, default, default, default, default); + + // gauge + counter + summary.sum + histogram.sum + summary quantile + await serializer.ReceivedWithAnyArgs(5).WriteMetricPointAsync(default, default, default, default(double), default, default, default); + // summary.count + 2x histogram bucket + histogram count + await serializer.ReceivedWithAnyArgs(4).WriteMetricPointAsync(default, default, default, default(long), default, default, default); + } + #endregion - var sumamryConfig = NewSummaryConfiguration(); + #region Labelled logic + [TestMethod] + public async Task CreatingLabelledMetric_WithoutObservingAnyData_ExportsImmediately() + { + var registry = Metrics.NewCustomRegistry(); + var factory = Metrics.WithCustomRegistry(registry); - var histogramConfig = NewHistogramConfiguration(); + var sumamryConfig = NewSummaryConfiguration(); - var gauge = factory.CreateGauge("gauge", "", new[] { "foo" }).WithLabels("bar"); - var counter = factory.CreateCounter("counter", "", new[] { "foo" }).WithLabels("bar"); - var summary = factory.CreateSummary("summary", "", new[] { "foo" }, sumamryConfig).WithLabels("bar"); - var histogram = factory.CreateHistogram("histogram", "", new[] { "foo" }, histogramConfig).WithLabels("bar"); - // 4 families with 9 metrics total. + var histogramConfig = NewHistogramConfiguration(); - var serializer = Substitute.For(); - await registry.CollectAndSerializeAsync(serializer, default); + var gauge = factory.CreateGauge("gauge", "", new[] { "foo" }).WithLabels("bar"); + var counter = factory.CreateCounter("counter", "", new[] { "foo" }).WithLabels("bar"); + var summary = factory.CreateSummary("summary", "", new[] { "foo" }, sumamryConfig).WithLabels("bar"); + var histogram = factory.CreateHistogram("histogram", "", new[] { "foo" }, histogramConfig).WithLabels("bar"); + // 4 families with 9 metrics total. - // Metrics are published as soon as label values are defined. - await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default, default, default, default, default); - // gauge + counter + summary.sum + histogram.sum + summary quantile - await serializer.ReceivedWithAnyArgs(5).WriteMetricPointAsync(default, default, default, default, default(double), default); - // summary.count + 2x histogram bucket + histogram count - await serializer.ReceivedWithAnyArgs(4).WriteMetricPointAsync(default, default, default, default, default(long), default); - } + var serializer = Substitute.For(); + await registry.CollectAndSerializeAsync(serializer, default); - [TestMethod] - public async Task CreatingLabelledMetric_WithInitialValueSuppression_ExportsNothingByDefault() - { - var registry = Metrics.NewCustomRegistry(); - var factory = Metrics.WithCustomRegistry(registry); - - var sumamryConfig = NewSummaryConfiguration(); - sumamryConfig.SuppressInitialValue = true; - - var histogramConfig = NewHistogramConfiguration(); - histogramConfig.SuppressInitialValue = true; - - var gauge = factory.CreateGauge("gauge", "", new[] { "foo" }, new GaugeConfiguration - { - SuppressInitialValue = true, - }).WithLabels("bar"); - var counter = factory.CreateCounter("counter", "", new[] { "foo" }, new CounterConfiguration - { - SuppressInitialValue = true, - }).WithLabels("bar"); - var summary = factory.CreateSummary("summary", "", new[] { "foo" }, sumamryConfig).WithLabels("bar"); - var histogram = factory.CreateHistogram("histogram", "", new[] { "foo" }, histogramConfig).WithLabels("bar"); - // 4 families with 9 metrics total. - - var serializer = Substitute.For(); - await registry.CollectAndSerializeAsync(serializer, default); - - // Publishing was suppressed. - await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default, default, default, default, default); - await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default(double), default); - await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default(long), default); - } + // Metrics are published as soon as label values are defined. + await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default, default, default, default, default); + // gauge + counter + summary.sum + histogram.sum + summary quantile + await serializer.ReceivedWithAnyArgs(5).WriteMetricPointAsync(default, default, default, default(double), default, default, default); + // summary.count + 2x histogram bucket + histogram count + await serializer.ReceivedWithAnyArgs(4).WriteMetricPointAsync(default, default, default, default(long), default, default, default); + } + + [TestMethod] + public async Task CreatingLabelledMetric_WithInitialValueSuppression_ExportsNothingByDefault() + { + var registry = Metrics.NewCustomRegistry(); + var factory = Metrics.WithCustomRegistry(registry); + + var sumamryConfig = NewSummaryConfiguration(); + sumamryConfig.SuppressInitialValue = true; + + var histogramConfig = NewHistogramConfiguration(); + histogramConfig.SuppressInitialValue = true; - [TestMethod] - public async Task CreatingLabelledMetric_WithInitialValueSuppression_ExportsAfterValueChange() + var gauge = factory.CreateGauge("gauge", "", new[] { "foo" }, new GaugeConfiguration { - var registry = Metrics.NewCustomRegistry(); - var factory = Metrics.WithCustomRegistry(registry); - - var sumamryConfig = NewSummaryConfiguration(); - sumamryConfig.SuppressInitialValue = true; - - var histogramConfig = NewHistogramConfiguration(); - histogramConfig.SuppressInitialValue = true; - - var gauge = factory.CreateGauge("gauge", "", new[] { "foo" }, new GaugeConfiguration - { - SuppressInitialValue = true, - }).WithLabels("bar"); - var counter = factory.CreateCounter("counter", "", new[] { "foo" }, new CounterConfiguration - { - SuppressInitialValue = true, - }).WithLabels("bar"); - var summary = factory.CreateSummary("summary", "", new[] { "foo" }, sumamryConfig).WithLabels("bar"); - var histogram = factory.CreateHistogram("histogram", "", new[] { "foo" }, histogramConfig).WithLabels("bar"); - // 4 families with 9 metrics total. - - gauge.Set(123); - counter.Inc(); - summary.Observe(123); - histogram.Observe(31); - - var serializer = Substitute.For(); - await registry.CollectAndSerializeAsync(serializer, default); - - // Metrics are published because value was set. - await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default, default, default, default, default); - // gauge + counter + summary.sum + histogram.sum + summary quantile - await serializer.ReceivedWithAnyArgs(5).WriteMetricPointAsync(default, default, default, default, default(double), default); - // summary.count + 2x histogram bucket + histogram count - await serializer.ReceivedWithAnyArgs(4).WriteMetricPointAsync(default, default, default, default, default(long), default); - } + SuppressInitialValue = true, + }).WithLabels("bar"); + var counter = factory.CreateCounter("counter", "", new[] { "foo" }, new CounterConfiguration + { + SuppressInitialValue = true, + }).WithLabels("bar"); + var summary = factory.CreateSummary("summary", "", new[] { "foo" }, sumamryConfig).WithLabels("bar"); + var histogram = factory.CreateHistogram("histogram", "", new[] { "foo" }, histogramConfig).WithLabels("bar"); + // 4 families with 9 metrics total. + + var serializer = Substitute.For(); + await registry.CollectAndSerializeAsync(serializer, default); + + // Publishing was suppressed. + await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default, default, default, default, default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default(double), default, default, default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default(long), default, default, default); + } + + [TestMethod] + public async Task CreatingLabelledMetric_WithInitialValueSuppression_ExportsAfterValueChange() + { + var registry = Metrics.NewCustomRegistry(); + var factory = Metrics.WithCustomRegistry(registry); + + var sumamryConfig = NewSummaryConfiguration(); + sumamryConfig.SuppressInitialValue = true; + + var histogramConfig = NewHistogramConfiguration(); + histogramConfig.SuppressInitialValue = true; - [TestMethod] - public async Task CreatingLabelledMetric_WithInitialValueSuppression_ExportsAfterPublish() + var gauge = factory.CreateGauge("gauge", "", new[] { "foo" }, new GaugeConfiguration { - var registry = Metrics.NewCustomRegistry(); - var factory = Metrics.WithCustomRegistry(registry); - - var sumamryConfig = NewSummaryConfiguration(); - sumamryConfig.SuppressInitialValue = true; - - var histogramConfig = NewHistogramConfiguration(); - histogramConfig.SuppressInitialValue = true; - - var gauge = factory.CreateGauge("gauge", "", new[] { "foo" }, new GaugeConfiguration - { - SuppressInitialValue = true, - }).WithLabels("bar"); - var counter = factory.CreateCounter("counter", "", new[] { "foo" }, new CounterConfiguration - { - SuppressInitialValue = true, - }).WithLabels("bar"); - var summary = factory.CreateSummary("summary", "", new[] { "foo" }, sumamryConfig).WithLabels("bar"); - var histogram = factory.CreateHistogram("histogram", "", new[] { "foo" }, histogramConfig).WithLabels("bar"); - // 4 families with 9 metrics total. - - gauge.Publish(); - counter.Publish(); - summary.Publish(); - histogram.Publish(); - - var serializer = Substitute.For(); - await registry.CollectAndSerializeAsync(serializer, default); - - // Metrics are published because of explicit publish. - await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default, default, default, default, default); - - // gauge + counter + summary.sum + histogram.sum + summary quantile - await serializer.ReceivedWithAnyArgs(5).WriteMetricPointAsync(default, default, default, default, default(double), default); - // summary.count + 2x histogram bucket + histogram count - await serializer.ReceivedWithAnyArgs(4).WriteMetricPointAsync(default, default, default, default, default(long), default); - } + SuppressInitialValue = true, + }).WithLabels("bar"); + var counter = factory.CreateCounter("counter", "", new[] { "foo" }, new CounterConfiguration + { + SuppressInitialValue = true, + }).WithLabels("bar"); + var summary = factory.CreateSummary("summary", "", new[] { "foo" }, sumamryConfig).WithLabels("bar"); + var histogram = factory.CreateHistogram("histogram", "", new[] { "foo" }, histogramConfig).WithLabels("bar"); + // 4 families with 9 metrics total. + + gauge.Set(123); + counter.Inc(); + summary.Observe(123); + histogram.Observe(31); + + var serializer = Substitute.For(); + await registry.CollectAndSerializeAsync(serializer, default); + + // Metrics are published because value was set. + await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default, default, default, default, default); + // gauge + counter + summary.sum + histogram.sum + summary quantile + await serializer.ReceivedWithAnyArgs(5).WriteMetricPointAsync(default, default, default, default(double), default, default, default); + // summary.count + 2x histogram bucket + histogram count + await serializer.ReceivedWithAnyArgs(4).WriteMetricPointAsync(default, default, default, default, default, default, default); + } + + [TestMethod] + public async Task CreatingLabelledMetric_WithInitialValueSuppression_ExportsAfterPublish() + { + var registry = Metrics.NewCustomRegistry(); + var factory = Metrics.WithCustomRegistry(registry); + + var sumamryConfig = NewSummaryConfiguration(); + sumamryConfig.SuppressInitialValue = true; - [TestMethod] - public async Task CreatingLabelledMetric_AndUnpublishingAfterObservingData_DoesNotExport() + var histogramConfig = NewHistogramConfiguration(); + histogramConfig.SuppressInitialValue = true; + + var gauge = factory.CreateGauge("gauge", "", new[] { "foo" }, new GaugeConfiguration + { + SuppressInitialValue = true, + }).WithLabels("bar"); + var counter = factory.CreateCounter("counter", "", new[] { "foo" }, new CounterConfiguration { - var registry = Metrics.NewCustomRegistry(); - var factory = Metrics.WithCustomRegistry(registry); + SuppressInitialValue = true, + }).WithLabels("bar"); + var summary = factory.CreateSummary("summary", "", new[] { "foo" }, sumamryConfig).WithLabels("bar"); + var histogram = factory.CreateHistogram("histogram", "", new[] { "foo" }, histogramConfig).WithLabels("bar"); + // 4 families with 9 metrics total. + + gauge.Publish(); + counter.Publish(); + summary.Publish(); + histogram.Publish(); + + var serializer = Substitute.For(); + await registry.CollectAndSerializeAsync(serializer, default); + + // Metrics are published because of explicit publish. + await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default, default, default, default, default); + + // gauge + counter + summary.sum + histogram.sum + summary quantile + await serializer.ReceivedWithAnyArgs(5).WriteMetricPointAsync(default, default, default, default(double), default, default, default); + // summary.count + 2x histogram bucket + histogram count + await serializer.ReceivedWithAnyArgs(4).WriteMetricPointAsync(default, default, default, default(long), default, default, default); + } - var counter = factory.CreateCounter("counter", "", new[] { "foo" }).WithLabels("bar"); + [TestMethod] + public async Task CreatingLabelledMetric_AndUnpublishingAfterObservingData_DoesNotExport() + { + var registry = Metrics.NewCustomRegistry(); + var factory = Metrics.WithCustomRegistry(registry); - counter.Inc(); - counter.Unpublish(); + var counter = factory.CreateCounter("counter", "", new[] { "foo" }).WithLabels("bar"); - var serializer = Substitute.For(); - await registry.CollectAndSerializeAsync(serializer, default); + counter.Inc(); + counter.Unpublish(); - await serializer.ReceivedWithAnyArgs(1).WriteFamilyDeclarationAsync(default, default, default, default, default, default); - await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default, default); - } - #endregion + var serializer = Substitute.For(); + await registry.CollectAndSerializeAsync(serializer, default); - #region Relation between labelled and unlabelled - [TestMethod] - public async Task CreatingLabelledMetric_WithoutObservingAnyData_DoesNotExportUnlabelled() - { - var registry = Metrics.NewCustomRegistry(); - var factory = Metrics.WithCustomRegistry(registry); + await serializer.ReceivedWithAnyArgs(1).WriteFamilyDeclarationAsync(default, default, default, default, default, default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default, default, default); + } + #endregion - var summaryConfig = NewSummaryConfiguration(); - var histogramConfig = NewHistogramConfiguration(); + #region Relation between labelled and unlabelled + [TestMethod] + public async Task CreatingLabelledMetric_WithoutObservingAnyData_DoesNotExportUnlabelled() + { + var registry = Metrics.NewCustomRegistry(); + var factory = Metrics.WithCustomRegistry(registry); - var gauge = factory.CreateGauge("gauge", "", new[] { "labelname" }); - var counter = factory.CreateCounter("counter", "", new[] { "labelname" }); - var summary = factory.CreateSummary("summary", "", new[] { "labelname" }, summaryConfig); - var histogram = factory.CreateHistogram("histogram", "", new[] { "labelname" }, histogramConfig); - // 4 families with 9 metrics total. + var summaryConfig = NewSummaryConfiguration(); + var histogramConfig = NewHistogramConfiguration(); - var serializer = Substitute.For(); - await registry.CollectAndSerializeAsync(serializer, default); + var gauge = factory.CreateGauge("gauge", "", new[] { "labelname" }); + var counter = factory.CreateCounter("counter", "", new[] { "labelname" }); + var summary = factory.CreateSummary("summary", "", new[] { "labelname" }, summaryConfig); + var histogram = factory.CreateHistogram("histogram", "", new[] { "labelname" }, histogramConfig); + // 4 families with 9 metrics total. - // Family for each of the above, in each is 0 metrics. - await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default, default, default, default, default); - await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default, default); - } + var serializer = Substitute.For(); + await registry.CollectAndSerializeAsync(serializer, default); - [TestMethod] - public async Task CreatingLabelledMetric_AfterObservingLabelledData_DoesNotExportUnlabelled() - { - var registry = Metrics.NewCustomRegistry(); - var factory = Metrics.WithCustomRegistry(registry); - - var summaryConfig = NewSummaryConfiguration(); - var histogramConfig = NewHistogramConfiguration(); - - var gauge = factory.CreateGauge("gauge", "", new[] { "labelname" }); - var counter = factory.CreateCounter("counter", "", new[] { "labelname" }); - var summary = factory.CreateSummary("summary", "", new[] { "labelname" }, summaryConfig); - var histogram = factory.CreateHistogram("histogram", "", new[] { "labelname" }, histogramConfig); - // 4 families with 9 metrics total. - - // Touch some labelled metrics. - gauge.WithLabels("labelvalue").Inc(); - counter.WithLabels("labelvalue").Inc(); - summary.WithLabels("labelvalue").Observe(123); - histogram.WithLabels("labelvalue").Observe(123); - - var serializer = Substitute.For(); - await registry.CollectAndSerializeAsync(serializer, default); - - // Family for each of the above, in each is 4 metrics (labelled only). - await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default, default, default, default, default); - // gauge + counter + summary.sum + histogram.sum + summary quantile - await serializer.ReceivedWithAnyArgs(5).WriteMetricPointAsync(default, default, default, default, default(double), default); - // summary.count + 2x histogram bucket + histogram count - await serializer.ReceivedWithAnyArgs(4).WriteMetricPointAsync(default, default, default, default, default(long), default); - - // Only after touching unlabelled do they get published. - gauge.Inc(); - counter.Inc(); - summary.Observe(123); - histogram.Observe(123); - - serializer.ClearReceivedCalls(); - await registry.CollectAndSerializeAsync(serializer, default); - - // Family for each of the above, in each family the instance count now doubled as unlabelled instances are published. - await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default, default, default, default, default); - await serializer.ReceivedWithAnyArgs(10).WriteMetricPointAsync(default, default, default, default, default(double), default); - await serializer.ReceivedWithAnyArgs(8).WriteMetricPointAsync(default, default, default, default, default(long), default); - } - #endregion + // Family for each of the above, in each is 0 metrics. + await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default, default, default, default, default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default, default, default); + } - [TestMethod] - public void RemovingLabeledInstance_ThenRecreatingIt_CreatesIndependentInstance() - { - var registry = Metrics.NewCustomRegistry(); - var factory = Metrics.WithCustomRegistry(registry); + [TestMethod] + public async Task CreatingLabelledMetric_AfterObservingLabelledData_DoesNotExportUnlabelled() + { + var registry = Metrics.NewCustomRegistry(); + var factory = Metrics.WithCustomRegistry(registry); + + var summaryConfig = NewSummaryConfiguration(); + var histogramConfig = NewHistogramConfiguration(); + + var gauge = factory.CreateGauge("gauge", "", new[] { "labelname" }); + var counter = factory.CreateCounter("counter", "", new[] { "labelname" }); + var summary = factory.CreateSummary("summary", "", new[] { "labelname" }, summaryConfig); + var histogram = factory.CreateHistogram("histogram", "", new[] { "labelname" }, histogramConfig); + // 4 families with 9 metrics total. + + // Touch some labelled metrics. + gauge.WithLabels("labelvalue").Inc(); + counter.WithLabels("labelvalue").Inc(); + summary.WithLabels("labelvalue").Observe(123); + histogram.WithLabels("labelvalue").Observe(123); + + var serializer = Substitute.For(); + await registry.CollectAndSerializeAsync(serializer, default); + + // Family for each of the above, in each is 4 metrics (labelled only). + await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default, default, default, default, default); + // gauge + counter + summary.sum + histogram.sum + summary quantile + await serializer.ReceivedWithAnyArgs(5).WriteMetricPointAsync(default, default, default, default(double), default, default, default); + // summary.count + 2x histogram bucket + histogram count + await serializer.ReceivedWithAnyArgs(4).WriteMetricPointAsync(default, default, default, default, default, default, default); + + // Only after touching unlabelled do they get published. + gauge.Inc(); + counter.Inc(); + summary.Observe(123); + histogram.Observe(123); + + serializer.ClearReceivedCalls(); + await registry.CollectAndSerializeAsync(serializer, default); + + // Family for each of the above, in each family the instance count now doubled as unlabelled instances are published. + await serializer.ReceivedWithAnyArgs(4).WriteFamilyDeclarationAsync(default, default, default, default, default, default); + await serializer.ReceivedWithAnyArgs(10).WriteMetricPointAsync(default, default, default, default(double), default, default, default); + await serializer.ReceivedWithAnyArgs(8).WriteMetricPointAsync(default, default, default, default, default, default, default); + } + #endregion + + [TestMethod] + public void RemovingLabeledInstance_ThenRecreatingIt_CreatesIndependentInstance() + { + var registry = Metrics.NewCustomRegistry(); + var factory = Metrics.WithCustomRegistry(registry); - var counter = factory.CreateCounter("counter", "", new[] { "foo" }); + var counter = factory.CreateCounter("counter", "", new[] { "foo" }); - var bar1 = counter.WithLabels("bar"); - bar1.Inc(); + var bar1 = counter.WithLabels("bar"); + bar1.Inc(); - Assert.AreEqual(1, bar1.Value); - bar1.Remove(); + Assert.AreEqual(1, bar1.Value); + bar1.Remove(); - // The new instance after the old one was removed must be independent. - var bar2 = counter.WithLabels("bar"); - Assert.AreEqual(0, bar2.Value); - } + // The new instance after the old one was removed must be independent. + var bar2 = counter.WithLabels("bar"); + Assert.AreEqual(0, bar2.Value); } } diff --git a/Tests.NetCore/MetricsTests.cs b/Tests.NetCore/MetricsTests.cs index a40584ee..b8d1f9f0 100644 --- a/Tests.NetCore/MetricsTests.cs +++ b/Tests.NetCore/MetricsTests.cs @@ -71,10 +71,10 @@ public async Task CreateCounter_WithDifferentRegistry_CreatesIndependentCounters await registry2.CollectAndSerializeAsync(serializer2, default); await serializer1.ReceivedWithAnyArgs().WriteFamilyDeclarationAsync(default, default, default, default, default, default); - await serializer1.ReceivedWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default(double), default); + await serializer1.ReceivedWithAnyArgs().WriteMetricPointAsync(default, default, default, default(double), default, default, default); await serializer2.ReceivedWithAnyArgs().WriteFamilyDeclarationAsync(default, default, default, default, default, default); - await serializer2.ReceivedWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default(double), default); + await serializer2.ReceivedWithAnyArgs().WriteMetricPointAsync(default, default, default, default(double), default, default, default); } [TestMethod] @@ -90,8 +90,8 @@ public async Task Export_FamilyWithOnlyNonpublishedUnlabeledMetrics_ExportsFamil await _registry.CollectAndSerializeAsync(serializer, default); await serializer.ReceivedWithAnyArgs(1).WriteFamilyDeclarationAsync(default, default, default, default, default, default); - await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default(double), default); - await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default(long), default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default(double), default, default, default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default, default, default); serializer.ClearReceivedCalls(); metric.Inc(); @@ -100,8 +100,8 @@ public async Task Export_FamilyWithOnlyNonpublishedUnlabeledMetrics_ExportsFamil await _registry.CollectAndSerializeAsync(serializer, default); await serializer.ReceivedWithAnyArgs(1).WriteFamilyDeclarationAsync(default, default, default, default, default, default); - await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default(double), default); - await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default(long), default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default(double), default, default, default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default, default, default); } [TestMethod] @@ -119,8 +119,8 @@ public async Task Export_FamilyWithOnlyNonpublishedLabeledMetrics_ExportsFamilyD await _registry.CollectAndSerializeAsync(serializer, default); await serializer.ReceivedWithAnyArgs(1).WriteFamilyDeclarationAsync(default, default, default, default, default, default); - await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default(double), default); - await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default(long), default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default(double), default, default, default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default, default, default); serializer.ClearReceivedCalls(); instance.Inc(); @@ -129,8 +129,8 @@ public async Task Export_FamilyWithOnlyNonpublishedLabeledMetrics_ExportsFamilyD await _registry.CollectAndSerializeAsync(serializer, default); await serializer.ReceivedWithAnyArgs(1).WriteFamilyDeclarationAsync(default, default, default, default, default, default); - await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default(double), default); - await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default(long), default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default(double), default, default, default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default, default, default); } [TestMethod] @@ -148,7 +148,7 @@ public async Task DisposeChild_RemovesMetric() await _registry.CollectAndSerializeAsync(serializer, default); await serializer.ReceivedWithAnyArgs(1).WriteFamilyDeclarationAsync(default, default, default, default, default, default); - await serializer.ReceivedWithAnyArgs(1).WriteMetricPointAsync(default, default, default, default, default(double), default); + await serializer.ReceivedWithAnyArgs(1).WriteMetricPointAsync(default, default, default, default(double), default, default, default); serializer.ClearReceivedCalls(); instance.Dispose(); @@ -156,8 +156,8 @@ public async Task DisposeChild_RemovesMetric() await _registry.CollectAndSerializeAsync(serializer, default); await serializer.ReceivedWithAnyArgs(1).WriteFamilyDeclarationAsync(default, default, default, default, default, default); - await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default(double), default); - await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default(long), default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default(double), default, default, default); + await serializer.DidNotReceiveWithAnyArgs().WriteMetricPointAsync(default, default, default, default, default, default, default); } [TestMethod] From 249620240593fdc00428d6c74e6fb4ba2423d8ee Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Sun, 3 Dec 2023 06:52:43 +0200 Subject: [PATCH 199/230] ArrayPool IS supported on .NET Fx. Why did I think it was not? --- Prometheus/Exemplar.cs | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/Prometheus/Exemplar.cs b/Prometheus/Exemplar.cs index f41616d4..2a3b9e28 100644 --- a/Prometheus/Exemplar.cs +++ b/Prometheus/Exemplar.cs @@ -1,7 +1,5 @@ -#if NET6_0_OR_GREATER -using System.Buffers; +using System.Buffers; using System.Diagnostics; -#endif using System.Text; using Microsoft.Extensions.ObjectPool; @@ -24,7 +22,7 @@ public sealed class Exemplar /// /// Indicates that no exemplar is to be recorded for a given observation. /// - public static readonly Exemplar None = new Exemplar(Array.Empty(), 0); + public static readonly Exemplar None = new([], 0); /// /// An exemplar label key. For optimal performance, create it once and reuse it forever. @@ -79,7 +77,7 @@ internal LabelPair(byte[] keyBytes, byte[] valueBytes, int runeCount) public static LabelKey Key(string key) { if (string.IsNullOrWhiteSpace(key)) - throw new ArgumentException("empty key"); + throw new ArgumentException("empty key", nameof(key)); Collector.ValidateLabelName(key); @@ -184,7 +182,7 @@ public static Exemplar FromTraceContext(in LabelKey traceIdKey, in LabelKey span public Exemplar() { - Buffer = Array.Empty(); + Buffer = []; } private Exemplar(LabelPair[] buffer, int length) @@ -217,12 +215,7 @@ internal static Exemplar AllocateFromPool(int length) if (length < 1) throw new ArgumentOutOfRangeException(nameof(length), $"{nameof(Exemplar)} key-value pair length must be at least 1 when constructing a pool-backed value."); -#if NET var buffer = ArrayPool.Shared.Rent(length); -#else - // .NET Framework does not support ArrayPool, so we just allocate explicit arrays to keep it simple. Migrate to .NET Core to get better performance. - var buffer = new LabelPair[length]; -#endif var instance = ExemplarPool.Get(); instance.Update(buffer, length); @@ -234,12 +227,10 @@ internal void ReturnToPoolIfNotEmpty() if (Length == 0) return; // Only the None instance can have a length of 0. -#if NET ArrayPool.Shared.Return(Buffer); -#endif Length = 0; - Buffer = Array.Empty(); + Buffer = []; ExemplarPool.Return(this); } From c7cf7737b1f6d0936c6557743106a720108b535c Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Sun, 3 Dec 2023 06:55:50 +0200 Subject: [PATCH 200/230] Expose ReadOnlySpan + ReadOnlyMemory WithLabels() + minor code tidy --- Prometheus/Collector.cs | 58 +++++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/Prometheus/Collector.cs b/Prometheus/Collector.cs index 425d27d8..d731d75c 100644 --- a/Prometheus/Collector.cs +++ b/Prometheus/Collector.cs @@ -178,28 +178,35 @@ public abstract class Collector : Collector, ICollector // We need it for the ICollector interface but using this is rarely relevant in client code, so keep it obscured. TChild ICollector.Unlabelled => Unlabelled; - // This servers a slightly silly but useful purpose: by default if you start typing .La... and trigger Intellisense + + // Old naming, deprecated for a silly reason: by default if you start typing .La... and trigger Intellisense // it will often for whatever reason focus on LabelNames instead of Labels, leading to tiny but persistent frustration. // Having WithLabels() instead eliminates the other candidate and allows for a frustration-free typing experience. - public TChild WithLabels(params string[] labelValues) => Labels(labelValues); - - // Discourage it as it can create confusion. But it works fine, so no reason to mark it obsolete, really. + // Discourage this method as it can create confusion. But it works fine, so no reason to mark it obsolete, really. [EditorBrowsable(EditorBrowsableState.Never)] - public TChild Labels(params string[] labelValues) + public TChild Labels(params string[] labelValues) => WithLabels(labelValues); + + public TChild WithLabels(params string[] labelValues) { if (labelValues == null) throw new ArgumentNullException(nameof(labelValues)); + return WithLabels(labelValues.AsMemory()); + } + + public TChild WithLabels(ReadOnlyMemory labelValues) + { var labels = LabelSequence.From(InstanceLabelNames, StringSequence.From(labelValues)); return GetOrAddLabelled(labels); } - internal TChild WithLabels(ReadOnlySpan labelValues) + public TChild WithLabels(ReadOnlySpan labelValues) { - // This is used only by MeterAdapter for now, until we stabilize this feature and have confidence it makes sense in general. - // We first use a pooled buffer to create the LabelSequence in the hopes that an instance with these labels already exists. - // This avoids allocating a string[] for the label values if we can avoid it. We only allocate if we create a new instance. + // We take ReadOnlySpan as a signal that the caller believes we may be able to perform the operation allocation-free because + // the label values are probably already known and a metric instance registered. There is no a guarantee, just a high probability. + // The implementation avoids allocating a long-lived string[] for the label values. We only allocate if we create a new instance. + // We still need to process the label values as a reference type, so we transform the Span into a Memory using a pooled buffer. var buffer = ArrayPool.Shared.Rent(labelValues.Length); try @@ -237,18 +244,18 @@ internal override void RemoveLabelled(LabelSequence labels) try { _children.Remove(labels); + + if (labels.Length == 0) + { + // If we remove the unlabeled instance (technically legitimate, if the caller really desires to do so) then + // we need to also ensure that the special-casing used for it gets properly wired up the next time. + Volatile.Write(ref _lazyUnlabelled, null); + } } finally { _childrenLock.ExitWriteLock(); } - - if (labels.Length == 0) - { - // If we remove the unlabeled instance (technically legitimate, if the caller really desires to do so) then - // we need to also ensure that the special-casing used for it gets properly wired up the next time. - Volatile.Write(ref _lazyUnlabelled, null); - } } internal override int ChildCount @@ -317,19 +324,20 @@ public IEnumerable GetAllLabelValues() private TChild GetOrAddLabelled(LabelSequence instanceLabels) { // NOTE: We do not try to find a metric instance with the same set of label names but in a DIFFERENT order. - // Order of labels matterns in data creation, although does not matter when the exported data set is imported later. + // Order of labels matters in data creation, although does not matter when the exported data set is imported later. // If we somehow end up registering the same metric with the same label names in different order, we will publish it twice, in two orders... // That is not ideal but also not that big of a deal to justify a lookup every time a metric instance is registered. + // First try to find an existing instance. This is the fast path, if we are re-looking-up an existing one. if (TryGetLabelled(instanceLabels, out var existing)) return existing!; + // If no existing one found, grab the write lock and create a new one if needed. return CreateLabelled(instanceLabels); } private bool TryGetLabelled(LabelSequence instanceLabels, out TChild? child) { - // First try to find an existing instance. This is the fast path, if we are re-looking-up an existing one. _childrenLock.EnterReadLock(); try @@ -351,7 +359,6 @@ private bool TryGetLabelled(LabelSequence instanceLabels, out TChild? child) private TChild CreateLabelled(LabelSequence instanceLabels) { - // If no existing one found, grab the write lock and create a new one if needed. _childrenLock.EnterWriteLock(); try @@ -390,11 +397,10 @@ internal Collector(string name, string help, StringSequence instanceLabelNames, : base(name, help, instanceLabelNames, staticLabels) { _createdUnlabelledFunc = CreateUnlabelled; + _createdLabelledChildFunc = CreateLabelledChild; _suppressInitialValue = suppressInitialValue; _exemplarBehavior = exemplarBehavior; - - _createdLabelledChildFunc = CreateLabelledChild; } /// @@ -415,18 +421,18 @@ internal override async ValueTask CollectAndSerializeAsync(IMetricsSerializer se // This could potentially take nontrivial time, as we are serializing to a stream (potentially, a network stream). // Therefore we operate on a defensive copy in a reused buffer. - TChild[] buffer; + TChild[] children; _childrenLock.EnterReadLock(); var childCount = _children.Count; - buffer = ArrayPool.Shared.Rent(childCount); + children = ArrayPool.Shared.Rent(childCount); try { try { - _children.Values.CopyTo(buffer, 0); + _children.Values.CopyTo(children, 0); } finally { @@ -435,13 +441,13 @@ internal override async ValueTask CollectAndSerializeAsync(IMetricsSerializer se for (var i = 0; i < childCount; i++) { - var child = buffer[i]; + var child = children[i]; await child.CollectAndSerializeAsync(serializer, cancel); } } finally { - ArrayPool.Shared.Return(buffer, clearArray: true); + ArrayPool.Shared.Return(children, clearArray: true); } } From 6c4eadde2898d6b4b35f05bb8f4d398a398d8c65 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Sun, 3 Dec 2023 06:56:02 +0200 Subject: [PATCH 201/230] Stabilize benchmark results with more warmup --- Benchmark.NetCore/MetricCreationBenchmarks.cs | 13 +++++++----- .../MetricExpirationBenchmarks.cs | 19 ++++++++++++++--- Prometheus/ManagedLifetimeMetricHandle.cs | 21 ++++++++++++++++++- 3 files changed, 44 insertions(+), 9 deletions(-) diff --git a/Benchmark.NetCore/MetricCreationBenchmarks.cs b/Benchmark.NetCore/MetricCreationBenchmarks.cs index 68d3de66..3e823de3 100644 --- a/Benchmark.NetCore/MetricCreationBenchmarks.cs +++ b/Benchmark.NetCore/MetricCreationBenchmarks.cs @@ -8,6 +8,9 @@ namespace Benchmark.NetCore; /// creating a brand new set of metrics for each scrape. So let's benchmark this scenario. /// [MemoryDiagnoser] +// This seems to need a lot of warmup to stabilize. +[WarmupCount(50)] +//[EventPipeProfiler(BenchmarkDotNet.Diagnosers.EventPipeProfile.CpuSampling)] public class MetricCreationBenchmarks { /// @@ -77,10 +80,10 @@ public void Setup() // We use the same strings both for the names and the values. private static readonly string[] _labels = ["foo", "bar", "baz"]; - private readonly CounterConfiguration _counterConfiguration = CounterConfiguration.Default; - private readonly GaugeConfiguration _gaugeConfiguration = GaugeConfiguration.Default; - private readonly SummaryConfiguration _summaryConfiguration = SummaryConfiguration.Default; - private readonly HistogramConfiguration _histogramConfiguration = HistogramConfiguration.Default; + private static readonly CounterConfiguration _counterConfiguration = CounterConfiguration.Default; + private static readonly GaugeConfiguration _gaugeConfiguration = GaugeConfiguration.Default; + private static readonly SummaryConfiguration _summaryConfiguration = SummaryConfiguration.Default; + private static readonly HistogramConfiguration _histogramConfiguration = HistogramConfiguration.Default; [Benchmark] public void Counter() @@ -104,7 +107,7 @@ public void Gauge() var metric = _factory.CreateGauge(_metricNames[i], _help, _labels, _gaugeConfiguration); for (var repeat = 0; repeat < RepeatCount; repeat++) - metric.WithLabels(_labels).Inc(); + metric.WithLabels(_labels).Set(repeat); } } diff --git a/Benchmark.NetCore/MetricExpirationBenchmarks.cs b/Benchmark.NetCore/MetricExpirationBenchmarks.cs index abc0ade1..accb2f45 100644 --- a/Benchmark.NetCore/MetricExpirationBenchmarks.cs +++ b/Benchmark.NetCore/MetricExpirationBenchmarks.cs @@ -7,7 +7,9 @@ namespace Benchmark.NetCore; /// Here we try to ensure that creating/using expiring metrics does not impose too heavy of a performance burden or create easily identifiable memory leaks. /// [MemoryDiagnoser] -//[EventPipeProfiler(BenchmarkDotNet.Diagnosers.EventPipeProfile.GcVerbose)] +// This seems to need a lot of warmup to stabilize. +[WarmupCount(50)] +//[EventPipeProfiler(BenchmarkDotNet.Diagnosers.EventPipeProfile.CpuSampling)] public class MetricExpirationBenchmarks { /// @@ -48,6 +50,9 @@ static MetricExpirationBenchmarks() private CollectorRegistry _registry; private IManagedLifetimeMetricFactory _factory; + // We use the same strings both for the names and the values. + private static readonly string[] _labels = ["foo", "bar", "baz"]; + [IterationSetup] public void Setup() { @@ -78,8 +83,16 @@ public void Setup() } } - // We use the same strings both for the names and the values. - private static readonly string[] _labels = new[] { "foo", "bar", "baz" }; + [IterationCleanup] + public void Cleanup() + { + for (var i = 0; i < _metricCount; i++) + { + var managedLifetimeCounter = (ManagedLifetimeMetricHandle)_factory.CreateCounter(_metricNames[i], _help, _labels); + // Ensure we do not slow down the next iteration by having the timer keep a bunch of references alive. + managedLifetimeCounter.CancelReaper(); + } + } [Benchmark] public void CreateAndUse_AutoLease() diff --git a/Prometheus/ManagedLifetimeMetricHandle.cs b/Prometheus/ManagedLifetimeMetricHandle.cs index e729cd53..e12d0ce3 100644 --- a/Prometheus/ManagedLifetimeMetricHandle.cs +++ b/Prometheus/ManagedLifetimeMetricHandle.cs @@ -21,6 +21,8 @@ internal ManagedLifetimeMetricHandle(Collector metric, TimeSpan expiresA _metric = metric; _expiresAfter = expiresAfter; + + _reaperCt = _reaperCts.Token; } protected readonly Collector _metric; @@ -293,6 +295,11 @@ private sealed class Lease(ManagedLifetimeMetricHandle private const int ReaperActive = 1; private const int ReaperInactive = 0; + // Only used for testing - in normal usage the reaper only stops when all metric instances have expired. + // That is a potential area for future improvement, as in rare situations this may result in a temporary waste of resources. + private readonly CancellationTokenSource _reaperCts = new(); + private readonly CancellationToken _reaperCt; + /// /// Call this immediately after creating a metric instance that will eventually expire. /// @@ -307,6 +314,15 @@ private void EnsureReaperActive() _ = Task.Run(_reaperFunc); } + /// + /// For testing only. Stops the reaper if it is active, allowing resources to be released after a test + /// without keeping things in memory just because the reaper task is still waiting for a delay of many minutes to elapse. + /// + internal void CancelReaper() + { + _reaperCts.Cancel(); + } + private async Task Reaper() { while (true) @@ -404,7 +420,10 @@ private async Task Reaper() // Work done! Go sleep a bit and come back when something may have expired. // We do not need to be too aggressive here, as expiration is not a hard schedule guarantee. - await Delayer.Delay(_expiresAfter); + await Delayer.Delay(_expiresAfter, _reaperCt); + } + catch (OperationCanceledException) when (_reaperCt.IsCancellationRequested) + { } finally { From dcc971a7309ce2d0d360eb38d0aca0fc81e70424 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Sun, 3 Dec 2023 09:13:42 +0200 Subject: [PATCH 202/230] tidy --- Prometheus/ExemplarBehavior.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Prometheus/ExemplarBehavior.cs b/Prometheus/ExemplarBehavior.cs index 250926cb..5cd584c6 100644 --- a/Prometheus/ExemplarBehavior.cs +++ b/Prometheus/ExemplarBehavior.cs @@ -18,12 +18,12 @@ public sealed class ExemplarBehavior /// public TimeSpan NewExemplarMinInterval { get; set; } = TimeSpan.Zero; - internal static readonly ExemplarBehavior Default = new ExemplarBehavior + internal static readonly ExemplarBehavior Default = new() { DefaultExemplarProvider = (_, _) => Exemplar.FromTraceContext() }; - public static ExemplarBehavior NoExemplars() => new ExemplarBehavior + public static ExemplarBehavior NoExemplars() => new() { DefaultExemplarProvider = (_, _) => Exemplar.None }; From 3eb06c0fb58bfa8dde4f9d3fa7202fcf53374f98 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Sun, 3 Dec 2023 09:30:04 +0200 Subject: [PATCH 203/230] Speed up exemplar processing by moving exemplar storage inline into the Exemplar class --- Benchmark.NetCore/ExemplarBenchmarks.cs | 79 ++++++++++ Prometheus/ChildBase.cs | 7 +- Prometheus/Exemplar.cs | 166 ++++++++++++++------- Prometheus/ObservedExemplar.cs | 5 +- Prometheus/PrometheusConstants.cs | 2 + Prometheus/TextSerializer.Net.cs | 30 ++-- Prometheus/TextSerializer.NetStandardFx.cs | 2 +- 7 files changed, 224 insertions(+), 67 deletions(-) create mode 100644 Benchmark.NetCore/ExemplarBenchmarks.cs diff --git a/Benchmark.NetCore/ExemplarBenchmarks.cs b/Benchmark.NetCore/ExemplarBenchmarks.cs new file mode 100644 index 00000000..35672fc3 --- /dev/null +++ b/Benchmark.NetCore/ExemplarBenchmarks.cs @@ -0,0 +1,79 @@ +using System.Diagnostics; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Diagnosers; +using Prometheus; + +namespace Benchmark.NetCore; + +[MemoryDiagnoser] +//[EventPipeProfiler(EventPipeProfile.CpuSampling)] +public class ExemplarBenchmarks +{ + private readonly CollectorRegistry _registry; + private readonly MetricFactory _factory; + private readonly Counter _counter; + + public ExemplarBenchmarks() + { + _registry = Metrics.NewCustomRegistry(); + _factory = Metrics.WithCustomRegistry(_registry); + + // We provide the exemplars manually, without using the default behavior. + _factory.ExemplarBehavior = ExemplarBehavior.NoExemplars(); + + _counter = _factory.CreateCounter("gauge", "help text"); + } + + // Just establish a baseline - how much time/memory do we spend if not recording an exemplar. + [Benchmark(Baseline = true)] + public void Observe_NoExemplar() + { + _counter.Inc(123); + } + + // Just as a sanity check, this should not cost us anything extra and may even be cheaper as we skip the default behavior lookup. + [Benchmark] + public void Observe_EmptyExemplar() + { + _counter.Inc(123, Exemplar.None); + } + + private static readonly Exemplar.LabelKey CustomLabelKey1 = Exemplar.Key("my_key"); + private static readonly Exemplar.LabelKey CustomLabelKey2 = Exemplar.Key("my_key2"); + + // A manually specified custom exemplar with some arbitrary value. + [Benchmark] + public void Observe_CustomExemplar() + { + _counter.Inc(123, Exemplar.From(CustomLabelKey1.WithValue("my_value"), CustomLabelKey2.WithValue("my_value2"))); + } + + // An exemplar extracted from the current trace context when there is no trace context. + [Benchmark] + public void Observe_ExemplarFromEmptyTraceContext() + { + _counter.Inc(123, Exemplar.FromTraceContext()); + } + + [GlobalSetup(Targets = new[] { nameof(Observe_ExemplarFromTraceContext) })] + public void Setup_ExemplarFromTraceContext() + { + new Activity("test activity").Start(); + + if (Activity.Current == null) + throw new Exception("Sanity check failed."); + } + + // An exemplar extracted from the current trace context when there is a trace context. + [Benchmark] + public void Observe_ExemplarFromTraceContext() + { + _counter.Inc(123, Exemplar.FromTraceContext()); + } + + [GlobalCleanup(Targets = new[] { nameof(Observe_ExemplarFromEmptyTraceContext), nameof(Observe_ExemplarFromTraceContext) })] + public void Cleanup() + { + Activity.Current = null; + } +} diff --git a/Prometheus/ChildBase.cs b/Prometheus/ChildBase.cs index 4cf98db8..cd2fbacb 100644 --- a/Prometheus/ChildBase.cs +++ b/Prometheus/ChildBase.cs @@ -1,3 +1,5 @@ +using System.Runtime.CompilerServices; + namespace Prometheus; /// @@ -145,7 +147,10 @@ internal void RecordExemplar(Exemplar exemplar, ref ObservedExemplar storage, do protected Exemplar GetDefaultExemplar(double value) { - return _exemplarBehavior.DefaultExemplarProvider?.Invoke(Parent, value) ?? Exemplar.None; + if (_exemplarBehavior.DefaultExemplarProvider == null) + return Exemplar.None; + + return _exemplarBehavior.DefaultExemplarProvider(Parent, value); } // May be replaced in test code. diff --git a/Prometheus/Exemplar.cs b/Prometheus/Exemplar.cs index 2a3b9e28..9b864add 100644 --- a/Prometheus/Exemplar.cs +++ b/Prometheus/Exemplar.cs @@ -22,20 +22,20 @@ public sealed class Exemplar /// /// Indicates that no exemplar is to be recorded for a given observation. /// - public static readonly Exemplar None = new([], 0); + public static readonly Exemplar None = new(0); /// /// An exemplar label key. For optimal performance, create it once and reuse it forever. /// public readonly struct LabelKey { - internal LabelKey(byte[] key, int runeCount) + internal LabelKey(byte[] key) { Bytes = key; - RuneCount = runeCount; } - internal int RuneCount { get; } + // We only support ASCII here, so rune count always matches byte count. + internal int RuneCount => Bytes.Length; internal byte[] Bytes { get; } @@ -47,8 +47,26 @@ internal LabelKey(byte[] key, int runeCount) /// public LabelPair WithValue(string value) { - var valueBytes = Encoding.ASCII.GetBytes(value); - return new LabelPair(Bytes, valueBytes, RuneCount + valueBytes.Length); + static bool IsAscii(ReadOnlySpan chars) + { + for (var i = 0; i < chars.Length; i++) + if (chars[i] > 127) + return false; + + return true; + } + + if (!IsAscii(value.AsSpan())) + { + // We believe that approximately 100% of use cases only consist of ASCII characters. + // That being said, we do not want to throw an exception here as the value may be coming from external sources + // that calling code has little control over. Therefore, we just replace such characters with placeholders. + // This matches the default behavior of Encoding.ASCII.GetBytes() - it replaces non-ASCII characters with '?'. + // As this is a highly theoretical case, we do an inefficient conversion here using the built-in encoder. + value = Encoding.ASCII.GetString(Encoding.ASCII.GetBytes(value)); + } + + return new LabelPair(Bytes, value); } } @@ -58,16 +76,19 @@ public LabelPair WithValue(string value) /// public readonly struct LabelPair { - internal LabelPair(byte[] keyBytes, byte[] valueBytes, int runeCount) + internal LabelPair(byte[] keyBytes, string value) { KeyBytes = keyBytes; - ValueBytes = valueBytes; - RuneCount = runeCount; + Value = value; } - internal int RuneCount { get; } + internal int RuneCount => KeyBytes.Length + Value.Length; internal byte[] KeyBytes { get; } - internal byte[] ValueBytes { get; } + + // We keep the value as a string because it typically starts out its life as a string + // and we want to avoid paying the cost of converting it to a byte array until we serialize it. + // If we record many exemplars then we may, in fact, never serialize most of them because they get replaced. + internal string Value { get; } } /// @@ -82,7 +103,7 @@ public static LabelKey Key(string key) Collector.ValidateLabelName(key); var asciiBytes = Encoding.ASCII.GetBytes(key); - return new LabelKey(asciiBytes, asciiBytes.Length); + return new LabelKey(asciiBytes); } /// @@ -97,12 +118,12 @@ public static LabelPair Pair(string key, string value) public static Exemplar From(in LabelPair labelPair1, in LabelPair labelPair2, in LabelPair labelPair3, in LabelPair labelPair4, in LabelPair labelPair5, in LabelPair labelPair6) { var exemplar = Exemplar.AllocateFromPool(length: 6); - exemplar.Buffer[0] = labelPair1; - exemplar.Buffer[1] = labelPair2; - exemplar.Buffer[2] = labelPair3; - exemplar.Buffer[3] = labelPair4; - exemplar.Buffer[4] = labelPair5; - exemplar.Buffer[5] = labelPair6; + exemplar.LabelPair1 = labelPair1; + exemplar.LabelPair2 = labelPair2; + exemplar.LabelPair3 = labelPair3; + exemplar.LabelPair4 = labelPair4; + exemplar.LabelPair5 = labelPair5; + exemplar.LabelPair6 = labelPair6; return exemplar; } @@ -110,22 +131,22 @@ public static Exemplar From(in LabelPair labelPair1, in LabelPair labelPair2, in public static Exemplar From(in LabelPair labelPair1, in LabelPair labelPair2, in LabelPair labelPair3, in LabelPair labelPair4, in LabelPair labelPair5) { var exemplar = Exemplar.AllocateFromPool(length: 5); - exemplar.Buffer[0] = labelPair1; - exemplar.Buffer[1] = labelPair2; - exemplar.Buffer[2] = labelPair3; - exemplar.Buffer[3] = labelPair4; - exemplar.Buffer[4] = labelPair5; + exemplar.LabelPair1 = labelPair1; + exemplar.LabelPair2 = labelPair2; + exemplar.LabelPair3 = labelPair3; + exemplar.LabelPair4 = labelPair4; + exemplar.LabelPair5 = labelPair5; return exemplar; } - + public static Exemplar From(in LabelPair labelPair1, in LabelPair labelPair2, in LabelPair labelPair3, in LabelPair labelPair4) { var exemplar = Exemplar.AllocateFromPool(length: 4); - exemplar.Buffer[0] = labelPair1; - exemplar.Buffer[1] = labelPair2; - exemplar.Buffer[2] = labelPair3; - exemplar.Buffer[3] = labelPair4; + exemplar.LabelPair1 = labelPair1; + exemplar.LabelPair2 = labelPair2; + exemplar.LabelPair3 = labelPair3; + exemplar.LabelPair4 = labelPair4; return exemplar; } @@ -133,9 +154,9 @@ public static Exemplar From(in LabelPair labelPair1, in LabelPair labelPair2, in public static Exemplar From(in LabelPair labelPair1, in LabelPair labelPair2, in LabelPair labelPair3) { var exemplar = Exemplar.AllocateFromPool(length: 3); - exemplar.Buffer[0] = labelPair1; - exemplar.Buffer[1] = labelPair2; - exemplar.Buffer[2] = labelPair3; + exemplar.LabelPair1 = labelPair1; + exemplar.LabelPair2 = labelPair2; + exemplar.LabelPair3 = labelPair3; return exemplar; } @@ -143,8 +164,8 @@ public static Exemplar From(in LabelPair labelPair1, in LabelPair labelPair2, in public static Exemplar From(in LabelPair labelPair1, in LabelPair labelPair2) { var exemplar = Exemplar.AllocateFromPool(length: 2); - exemplar.Buffer[0] = labelPair1; - exemplar.Buffer[1] = labelPair2; + exemplar.LabelPair1 = labelPair1; + exemplar.LabelPair2 = labelPair2; return exemplar; } @@ -152,11 +173,52 @@ public static Exemplar From(in LabelPair labelPair1, in LabelPair labelPair2) public static Exemplar From(in LabelPair labelPair1) { var exemplar = Exemplar.AllocateFromPool(length: 1); - exemplar.Buffer[0] = labelPair1; + exemplar.LabelPair1 = labelPair1; return exemplar; } + public ref struct LabelPairEnumerator(Exemplar exemplar) + { + private readonly Exemplar _exemplar = exemplar; + private int _index = -1; + + public bool MoveNext() => ++_index < _exemplar.Length; + + public LabelPair Current + { + get + { + if (_index == 0) return _exemplar.LabelPair1; + if (_index == 1) return _exemplar.LabelPair2; + if (_index == 2) return _exemplar.LabelPair3; + if (_index == 3) return _exemplar.LabelPair4; + if (_index == 4) return _exemplar.LabelPair5; + if (_index == 5) return _exemplar.LabelPair6; + throw new InvalidOperationException("Invalid index"); + } + } + } + + public LabelPairEnumerator GetEnumerator() => new(this); + + public LabelPair this[int index] + { + get + { + if (index < 0 || index >= Length) + throw new ArgumentOutOfRangeException(nameof(index)); + + if (index == 0) return LabelPair1; + if (index == 1) return LabelPair2; + if (index == 2) return LabelPair3; + if (index == 3) return LabelPair4; + if (index == 4) return LabelPair5; + if (index == 5) return LabelPair6; + throw new ArgumentOutOfRangeException(nameof(index)); + } + } + // Based on https://opentelemetry.io/docs/reference/specification/compatibility/prometheus_and_openmetrics/ private static readonly LabelKey DefaultTraceIdKey = Key("trace_id"); private static readonly LabelKey DefaultSpanIdKey = Key("span_id"); @@ -169,6 +231,7 @@ public static Exemplar FromTraceContext(in LabelKey traceIdKey, in LabelKey span var activity = Activity.Current; if (activity != null) { + // These values already exist as strings inside the Activity logic, so there is no string allocation happening here. var traceIdLabel = traceIdKey.WithValue(activity.TraceId.ToString()); var spanIdLabel = spanIdKey.WithValue(activity.SpanId.ToString()); @@ -182,32 +245,31 @@ public static Exemplar FromTraceContext(in LabelKey traceIdKey, in LabelKey span public Exemplar() { - Buffer = []; } - private Exemplar(LabelPair[] buffer, int length) + private Exemplar(int length) { - Buffer = buffer; Length = length; } - internal void Update(LabelPair[] buffer, int length) + internal void Update(int length) { - Buffer = buffer; Length = length; _consumed = false; } /// - /// The buffer containing the label pairs. Might not be fully filled! - /// - internal LabelPair[] Buffer { get; private set; } - - /// - /// Number of label pairs from the buffer to use. + /// Number of label pairs in use. /// internal int Length { get; private set; } + internal LabelPair LabelPair1; + internal LabelPair LabelPair2; + internal LabelPair LabelPair3; + internal LabelPair LabelPair4; + internal LabelPair LabelPair5; + internal LabelPair LabelPair6; + private static readonly ObjectPool ExemplarPool = ObjectPool.Create(); internal static Exemplar AllocateFromPool(int length) @@ -215,10 +277,8 @@ internal static Exemplar AllocateFromPool(int length) if (length < 1) throw new ArgumentOutOfRangeException(nameof(length), $"{nameof(Exemplar)} key-value pair length must be at least 1 when constructing a pool-backed value."); - var buffer = ArrayPool.Shared.Rent(length); - var instance = ExemplarPool.Get(); - instance.Update(buffer, length); + instance.Update(length); return instance; } @@ -227,10 +287,7 @@ internal void ReturnToPoolIfNotEmpty() if (Length == 0) return; // Only the None instance can have a length of 0. - ArrayPool.Shared.Return(Buffer); - Length = 0; - Buffer = []; ExemplarPool.Return(this); } @@ -254,7 +311,12 @@ public Exemplar Clone() throw new InvalidOperationException($"An instance of {nameof(Exemplar)} cannot be cloned after it has already been used."); var clone = AllocateFromPool(Length); - Array.Copy(Buffer, clone.Buffer, Length); + clone.LabelPair1 = LabelPair1; + clone.LabelPair2 = LabelPair2; + clone.LabelPair3 = LabelPair3; + clone.LabelPair4 = LabelPair4; + clone.LabelPair5 = LabelPair5; + clone.LabelPair6 = LabelPair6; return clone; } } \ No newline at end of file diff --git a/Prometheus/ObservedExemplar.cs b/Prometheus/ObservedExemplar.cs index ddd71047..3d82ef8d 100644 --- a/Prometheus/ObservedExemplar.cs +++ b/Prometheus/ObservedExemplar.cs @@ -42,13 +42,14 @@ private void Update(Exemplar labels, double value) Debug.Assert(this != Empty, "Do not mutate the sentinel"); var totalRuneCount = 0; + for (var i = 0; i < labels.Length; i++) { - totalRuneCount += labels.Buffer[i].RuneCount; + totalRuneCount += labels[i].RuneCount; for (var j = 0; j < labels.Length; j++) { if (i == j) continue; - if (ByteArraysEqual(labels.Buffer[i].KeyBytes, labels.Buffer[j].KeyBytes)) + if (ByteArraysEqual(labels[i].KeyBytes, labels[j].KeyBytes)) throw new ArgumentException("Exemplar contains duplicate keys."); } } diff --git a/Prometheus/PrometheusConstants.cs b/Prometheus/PrometheusConstants.cs index 6df93505..bcff4471 100644 --- a/Prometheus/PrometheusConstants.cs +++ b/Prometheus/PrometheusConstants.cs @@ -17,4 +17,6 @@ public static class PrometheusConstants // Use UTF-8 encoding, but provide the flag to ensure the Unicode Byte Order Mark is never prepended to the output stream. public static readonly Encoding ExportEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); + + internal static readonly Encoding ExemplarEncoding = new ASCIIEncoding(); } \ No newline at end of file diff --git a/Prometheus/TextSerializer.Net.cs b/Prometheus/TextSerializer.Net.cs index 38615b9a..715ba874 100644 --- a/Prometheus/TextSerializer.Net.cs +++ b/Prometheus/TextSerializer.Net.cs @@ -242,12 +242,16 @@ private int WriteExemplar(Span buffer, ObservedExemplar exemplar) var position = 0; AppendToBufferAndIncrementPosition(SpaceHashSpaceLeftBrace, buffer, ref position); - for (var i = 0; i < exemplar.Labels!.Length; i++) + + bool first = true; + foreach (var labelPair in exemplar.Labels!) { - if (i > 0) + if (!first) AppendToBufferAndIncrementPosition(Comma, buffer, ref position); - position += TextSerializer.WriteExemplarLabel(buffer[position..], exemplar.Labels!.Buffer[i].KeyBytes, exemplar.Labels!.Buffer[i].ValueBytes); + first = false; + + position += TextSerializer.WriteExemplarLabel(buffer[position..], labelPair.KeyBytes, labelPair.Value); } AppendToBufferAndIncrementPosition(RightBraceSpace, buffer, ref position); @@ -264,12 +268,16 @@ private int MeasureExemplarMaxLength(ObservedExemplar exemplar) var length = 0; length += SpaceHashSpaceLeftBrace.Length; - for (var i = 0; i < exemplar.Labels!.Length; i++) + + bool first = true; + foreach (var labelPair in exemplar.Labels!) { - if (i > 0) + if (!first) length += Comma.Length; - length += TextSerializer.MeasureExemplarLabelLength(exemplar.Labels!.Buffer[i].KeyBytes, exemplar.Labels!.Buffer[i].ValueBytes); + first = false; + + length += TextSerializer.MeasureExemplarLabelMaxLength(labelPair.KeyBytes, labelPair.Value); } length += RightBraceSpace.Length; @@ -280,20 +288,20 @@ private int MeasureExemplarMaxLength(ObservedExemplar exemplar) return length; } - private static int WriteExemplarLabel(Span buffer, byte[] label, byte[] value) + private static int WriteExemplarLabel(Span buffer, byte[] label, string value) { var position = 0; AppendToBufferAndIncrementPosition(label, buffer, ref position); AppendToBufferAndIncrementPosition(Equal, buffer, ref position); AppendToBufferAndIncrementPosition(Quote, buffer, ref position); - AppendToBufferAndIncrementPosition(value, buffer, ref position); + position += PrometheusConstants.ExemplarEncoding.GetBytes(value.AsSpan(), buffer[position..]); AppendToBufferAndIncrementPosition(Quote, buffer, ref position); return position; } - private static int MeasureExemplarLabelLength(byte[] label, byte[] value) + private static int MeasureExemplarLabelMaxLength(byte[] label, string value) { // We mirror the logic in the Write() call but just measure how many bytes of buffer we need. var length = 0; @@ -301,7 +309,7 @@ private static int MeasureExemplarLabelLength(byte[] label, byte[] value) length += label.Length; length += Equal.Length; length += Quote.Length; - length += value.Length; + length += PrometheusConstants.ExemplarEncoding.GetMaxByteCount(value.Length); length += Quote.Length; return length; @@ -444,7 +452,7 @@ private static void AppendToBufferAndIncrementPosition(ReadOnlySpan from, from.CopyTo(to[position..]); position += from.Length; } - + private static void ValidateBufferLengthAndPosition(int bufferLength, int position) { if (position != bufferLength) diff --git a/Prometheus/TextSerializer.NetStandardFx.cs b/Prometheus/TextSerializer.NetStandardFx.cs index fd3c7ad4..3588c73f 100644 --- a/Prometheus/TextSerializer.NetStandardFx.cs +++ b/Prometheus/TextSerializer.NetStandardFx.cs @@ -153,7 +153,7 @@ private async Task WriteExemplarAsync(CancellationToken cancel, ObservedExemplar { if (i > 0) await _stream.Value.WriteAsync(Comma, 0, Comma.Length, cancel); - await WriteLabel(exemplar.Labels!.Buffer[i].KeyBytes, exemplar.Labels!.Buffer[i].ValueBytes, cancel); + await WriteLabel(exemplar.Labels![i].KeyBytes, PrometheusConstants.ExemplarEncoding.GetBytes(exemplar.Labels![i].Value), cancel); } await _stream.Value.WriteAsync(RightBraceSpace, 0, RightBraceSpace.Length, cancel); From 2a18b8b4126df6c7e7ec1d00b1b90d89498946de Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Sun, 3 Dec 2023 09:45:57 +0200 Subject: [PATCH 204/230] Hide Exemplar new members from public API --- Prometheus/Exemplar.cs | 41 ++++------------------ Prometheus/TextSerializer.Net.cs | 20 +++++------ Prometheus/TextSerializer.NetStandardFx.cs | 1 + 3 files changed, 16 insertions(+), 46 deletions(-) diff --git a/Prometheus/Exemplar.cs b/Prometheus/Exemplar.cs index 9b864add..dab295c1 100644 --- a/Prometheus/Exemplar.cs +++ b/Prometheus/Exemplar.cs @@ -178,43 +178,16 @@ public static Exemplar From(in LabelPair labelPair1) return exemplar; } - public ref struct LabelPairEnumerator(Exemplar exemplar) - { - private readonly Exemplar _exemplar = exemplar; - private int _index = -1; - - public bool MoveNext() => ++_index < _exemplar.Length; - - public LabelPair Current - { - get - { - if (_index == 0) return _exemplar.LabelPair1; - if (_index == 1) return _exemplar.LabelPair2; - if (_index == 2) return _exemplar.LabelPair3; - if (_index == 3) return _exemplar.LabelPair4; - if (_index == 4) return _exemplar.LabelPair5; - if (_index == 5) return _exemplar.LabelPair6; - throw new InvalidOperationException("Invalid index"); - } - } - } - - public LabelPairEnumerator GetEnumerator() => new(this); - - public LabelPair this[int index] + internal ref LabelPair this[int index] { get { - if (index < 0 || index >= Length) - throw new ArgumentOutOfRangeException(nameof(index)); - - if (index == 0) return LabelPair1; - if (index == 1) return LabelPair2; - if (index == 2) return LabelPair3; - if (index == 3) return LabelPair4; - if (index == 4) return LabelPair5; - if (index == 5) return LabelPair6; + if (index == 0) return ref LabelPair1; + if (index == 1) return ref LabelPair2; + if (index == 2) return ref LabelPair3; + if (index == 3) return ref LabelPair4; + if (index == 4) return ref LabelPair5; + if (index == 5) return ref LabelPair6; throw new ArgumentOutOfRangeException(nameof(index)); } } diff --git a/Prometheus/TextSerializer.Net.cs b/Prometheus/TextSerializer.Net.cs index 715ba874..d34ff678 100644 --- a/Prometheus/TextSerializer.Net.cs +++ b/Prometheus/TextSerializer.Net.cs @@ -243,15 +243,13 @@ private int WriteExemplar(Span buffer, ObservedExemplar exemplar) AppendToBufferAndIncrementPosition(SpaceHashSpaceLeftBrace, buffer, ref position); - bool first = true; - foreach (var labelPair in exemplar.Labels!) + for (var i = 0; i < exemplar.Labels!.Length; i++) { - if (!first) + if (i > 0) AppendToBufferAndIncrementPosition(Comma, buffer, ref position); - first = false; - - position += TextSerializer.WriteExemplarLabel(buffer[position..], labelPair.KeyBytes, labelPair.Value); + ref var labelPair = ref exemplar.Labels[i]; + position += WriteExemplarLabel(buffer[position..], labelPair.KeyBytes, labelPair.Value); } AppendToBufferAndIncrementPosition(RightBraceSpace, buffer, ref position); @@ -269,15 +267,13 @@ private int MeasureExemplarMaxLength(ObservedExemplar exemplar) length += SpaceHashSpaceLeftBrace.Length; - bool first = true; - foreach (var labelPair in exemplar.Labels!) + for (var i = 0; i < exemplar.Labels!.Length; i++) { - if (!first) + if (i > 0) length += Comma.Length; - first = false; - - length += TextSerializer.MeasureExemplarLabelMaxLength(labelPair.KeyBytes, labelPair.Value); + ref var labelPair = ref exemplar.Labels[i]; + length += MeasureExemplarLabelMaxLength(labelPair.KeyBytes, labelPair.Value); } length += RightBraceSpace.Length; diff --git a/Prometheus/TextSerializer.NetStandardFx.cs b/Prometheus/TextSerializer.NetStandardFx.cs index 3588c73f..1fd4629f 100644 --- a/Prometheus/TextSerializer.NetStandardFx.cs +++ b/Prometheus/TextSerializer.NetStandardFx.cs @@ -153,6 +153,7 @@ private async Task WriteExemplarAsync(CancellationToken cancel, ObservedExemplar { if (i > 0) await _stream.Value.WriteAsync(Comma, 0, Comma.Length, cancel); + await WriteLabel(exemplar.Labels![i].KeyBytes, PrometheusConstants.ExemplarEncoding.GetBytes(exemplar.Labels![i].Value), cancel); } From 902ee7fd3338821e25fb92eb341971085c14297d Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Sun, 3 Dec 2023 10:03:01 +0200 Subject: [PATCH 205/230] Tiny speedup for exemplar processing --- Prometheus/ChildBase.cs | 3 --- Prometheus/Counter.cs | 2 +- Prometheus/Histogram.cs | 2 +- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/Prometheus/ChildBase.cs b/Prometheus/ChildBase.cs index cd2fbacb..ee9207cc 100644 --- a/Prometheus/ChildBase.cs +++ b/Prometheus/ChildBase.cs @@ -121,9 +121,6 @@ internal void ReturnBorrowedExemplar(ref ObservedExemplar storage, ObservedExemp internal void RecordExemplar(Exemplar exemplar, ref ObservedExemplar storage, double observedValue) { - if (exemplar.Length == 0) - return; - exemplar.MarkAsConsumed(); // We do the "is allowed" check only if we really have an exemplar to record, to minimize the performance impact on users who do not use exemplars. diff --git a/Prometheus/Counter.cs b/Prometheus/Counter.cs index 3fc48e76..bbcfa3ca 100644 --- a/Prometheus/Counter.cs +++ b/Prometheus/Counter.cs @@ -50,7 +50,7 @@ public void Inc(double increment, Exemplar? exemplar) exemplar ??= GetDefaultExemplar(increment); - if (exemplar != null) + if (exemplar?.Length > 0) RecordExemplar(exemplar, ref _observedExemplar, increment); _value.Add(increment); diff --git a/Prometheus/Histogram.cs b/Prometheus/Histogram.cs index 15ffc5a7..6a9ac093 100644 --- a/Prometheus/Histogram.cs +++ b/Prometheus/Histogram.cs @@ -203,7 +203,7 @@ private void ObserveInternal(double val, long count, Exemplar? exemplar) _bucketCounts[bucketIndex].Add(count); - if (exemplar != null) + if (exemplar?.Length > 0) RecordExemplar(exemplar, ref _exemplars[bucketIndex], val); _sum.Add(val * count); From 4e07ece8e656d09ba09f774d83e45d05925351d3 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Sun, 3 Dec 2023 10:17:10 +0200 Subject: [PATCH 206/230] More inlining for more performance in exemplar processing --- Prometheus/Exemplar.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/Prometheus/Exemplar.cs b/Prometheus/Exemplar.cs index dab295c1..b5de5b41 100644 --- a/Prometheus/Exemplar.cs +++ b/Prometheus/Exemplar.cs @@ -1,5 +1,6 @@ using System.Buffers; using System.Diagnostics; +using System.Runtime.CompilerServices; using System.Text; using Microsoft.Extensions.ObjectPool; @@ -45,6 +46,7 @@ internal LabelKey(byte[] key) /// The string is expected to only contain runes in the ASCII range, runes outside the ASCII range will get replaced /// with placeholders. This constraint may be relaxed with future versions. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public LabelPair WithValue(string value) { static bool IsAscii(ReadOnlySpan chars) @@ -115,6 +117,7 @@ public static LabelPair Pair(string key, string value) return Key(key).WithValue(value); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Exemplar From(in LabelPair labelPair1, in LabelPair labelPair2, in LabelPair labelPair3, in LabelPair labelPair4, in LabelPair labelPair5, in LabelPair labelPair6) { var exemplar = Exemplar.AllocateFromPool(length: 6); @@ -128,6 +131,7 @@ public static Exemplar From(in LabelPair labelPair1, in LabelPair labelPair2, in return exemplar; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Exemplar From(in LabelPair labelPair1, in LabelPair labelPair2, in LabelPair labelPair3, in LabelPair labelPair4, in LabelPair labelPair5) { var exemplar = Exemplar.AllocateFromPool(length: 5); @@ -140,6 +144,7 @@ public static Exemplar From(in LabelPair labelPair1, in LabelPair labelPair2, in return exemplar; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Exemplar From(in LabelPair labelPair1, in LabelPair labelPair2, in LabelPair labelPair3, in LabelPair labelPair4) { var exemplar = Exemplar.AllocateFromPool(length: 4); @@ -151,6 +156,7 @@ public static Exemplar From(in LabelPair labelPair1, in LabelPair labelPair2, in return exemplar; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Exemplar From(in LabelPair labelPair1, in LabelPair labelPair2, in LabelPair labelPair3) { var exemplar = Exemplar.AllocateFromPool(length: 3); @@ -161,6 +167,7 @@ public static Exemplar From(in LabelPair labelPair1, in LabelPair labelPair2, in return exemplar; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Exemplar From(in LabelPair labelPair1, in LabelPair labelPair2) { var exemplar = Exemplar.AllocateFromPool(length: 2); @@ -170,6 +177,7 @@ public static Exemplar From(in LabelPair labelPair1, in LabelPair labelPair2) return exemplar; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Exemplar From(in LabelPair labelPair1) { var exemplar = Exemplar.AllocateFromPool(length: 1); @@ -180,6 +188,7 @@ public static Exemplar From(in LabelPair labelPair1) internal ref LabelPair this[int index] { + [MethodImpl(MethodImplOptions.AggressiveInlining)] get { if (index == 0) return ref LabelPair1; @@ -225,6 +234,7 @@ private Exemplar(int length) Length = length; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void Update(int length) { Length = length; @@ -245,16 +255,15 @@ internal void Update(int length) private static readonly ObjectPool ExemplarPool = ObjectPool.Create(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static Exemplar AllocateFromPool(int length) { - if (length < 1) - throw new ArgumentOutOfRangeException(nameof(length), $"{nameof(Exemplar)} key-value pair length must be at least 1 when constructing a pool-backed value."); - var instance = ExemplarPool.Get(); instance.Update(length); return instance; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void ReturnToPoolIfNotEmpty() { if (Length == 0) From 28708906b93bbd3834c747f56a525dce21bc5863 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Sun, 3 Dec 2023 10:30:52 +0200 Subject: [PATCH 207/230] Speed up Gauge.SetToCurrentTimestamp() by using low granularity time source to avoid UtcNow overhead if it can be avoided --- Benchmark.NetCore/GaugeBenchmarks.cs | 25 +++++++++++++++++++++++++ Prometheus/GaugeExtensions.cs | 4 ++-- 2 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 Benchmark.NetCore/GaugeBenchmarks.cs diff --git a/Benchmark.NetCore/GaugeBenchmarks.cs b/Benchmark.NetCore/GaugeBenchmarks.cs new file mode 100644 index 00000000..6de13456 --- /dev/null +++ b/Benchmark.NetCore/GaugeBenchmarks.cs @@ -0,0 +1,25 @@ +using BenchmarkDotNet.Attributes; +using Prometheus; + +namespace Benchmark.NetCore; + +public class GaugeBenchmarks +{ + private readonly CollectorRegistry _registry; + private readonly MetricFactory _factory; + private readonly Gauge _gauge; + + public GaugeBenchmarks() + { + _registry = Metrics.NewCustomRegistry(); + _factory = Metrics.WithCustomRegistry(_registry); + + _gauge = _factory.CreateGauge("gauge", "help text"); + } + + [Benchmark] + public void SetToCurrenTimeUtc() + { + _gauge.SetToCurrentTimeUtc(); + } +} diff --git a/Prometheus/GaugeExtensions.cs b/Prometheus/GaugeExtensions.cs index daa236c9..0a6062b9 100644 --- a/Prometheus/GaugeExtensions.cs +++ b/Prometheus/GaugeExtensions.cs @@ -10,7 +10,7 @@ public static class GaugeExtensions /// public static void SetToCurrentTimeUtc(this IGauge gauge) { - gauge.Set(TimestampHelpers.ToUnixTimeSecondsAsDouble(DateTimeOffset.UtcNow)); + gauge.Set(LowGranularityTimeSource.GetSecondsFromUnixEpoch()); } /// @@ -29,7 +29,7 @@ public static void SetToTimeUtc(this IGauge gauge, DateTimeOffset timestamp) /// public static void IncToCurrentTimeUtc(this IGauge gauge) { - gauge.IncTo(TimestampHelpers.ToUnixTimeSecondsAsDouble(DateTimeOffset.UtcNow)); + gauge.IncTo(LowGranularityTimeSource.GetSecondsFromUnixEpoch()); } /// From 74676319cd9d16ae3b618e9419ec55e28a805c82 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Sun, 3 Dec 2023 10:32:26 +0200 Subject: [PATCH 208/230] Use low granularity time source in counter UtcNow recording --- Benchmark.NetCore/CounterBenchmarks.cs | 25 ++++ Prometheus/CounterExtensions.cs | 189 ++++++++++++------------- 2 files changed, 119 insertions(+), 95 deletions(-) create mode 100644 Benchmark.NetCore/CounterBenchmarks.cs diff --git a/Benchmark.NetCore/CounterBenchmarks.cs b/Benchmark.NetCore/CounterBenchmarks.cs new file mode 100644 index 00000000..505ce984 --- /dev/null +++ b/Benchmark.NetCore/CounterBenchmarks.cs @@ -0,0 +1,25 @@ +using BenchmarkDotNet.Attributes; +using Prometheus; + +namespace Benchmark.NetCore; + +public class CounterBenchmarks +{ + private readonly CollectorRegistry _registry; + private readonly MetricFactory _factory; + private readonly Counter _counter; + + public CounterBenchmarks() + { + _registry = Metrics.NewCustomRegistry(); + _factory = Metrics.WithCustomRegistry(_registry); + + _counter = _factory.CreateCounter("gauge", "help text"); + } + + [Benchmark] + public void IncToCurrentTimeUtc() + { + _counter.IncToCurrentTimeUtc(); + } +} diff --git a/Prometheus/CounterExtensions.cs b/Prometheus/CounterExtensions.cs index ad788458..9f5bbafa 100644 --- a/Prometheus/CounterExtensions.cs +++ b/Prometheus/CounterExtensions.cs @@ -1,117 +1,116 @@ -namespace Prometheus +namespace Prometheus; + +public static class CounterExtensions { - public static class CounterExtensions + /// + /// Increments the value of the counter to the current UTC time as a Unix timestamp in seconds. + /// Value does not include any elapsed leap seconds because Unix timestamps do not include leap seconds. + /// Operation is ignored if the current value is already greater. + /// + public static void IncToCurrentTimeUtc(this ICounter counter) + { + counter.IncTo(LowGranularityTimeSource.GetSecondsFromUnixEpoch()); + } + + /// + /// Increments the value of the counter to a specific moment as the UTC Unix timestamp in seconds. + /// Value does not include any elapsed leap seconds because Unix timestamps do not include leap seconds. + /// Operation is ignored if the current value is already greater. + /// + public static void IncToTimeUtc(this ICounter counter, DateTimeOffset timestamp) { - /// - /// Increments the value of the counter to the current UTC time as a Unix timestamp in seconds. - /// Value does not include any elapsed leap seconds because Unix timestamps do not include leap seconds. - /// Operation is ignored if the current value is already greater. - /// - public static void IncToCurrentTimeUtc(this ICounter counter) + counter.IncTo(TimestampHelpers.ToUnixTimeSecondsAsDouble(timestamp)); + } + + /// + /// Executes the provided operation and increments the counter if an exception occurs. The exception is re-thrown. + /// If an exception filter is specified, only counts exceptions for which the filter returns true. + /// + public static void CountExceptions(this ICounter counter, Action wrapped, Func? exceptionFilter = null) + { + if (counter == null) + throw new ArgumentNullException(nameof(counter)); + + if (wrapped == null) + throw new ArgumentNullException(nameof(wrapped)); + + try { - counter.IncTo(TimestampHelpers.ToUnixTimeSecondsAsDouble(DateTimeOffset.UtcNow)); + wrapped(); } - - /// - /// Increments the value of the counter to a specific moment as the UTC Unix timestamp in seconds. - /// Value does not include any elapsed leap seconds because Unix timestamps do not include leap seconds. - /// Operation is ignored if the current value is already greater. - /// - public static void IncToTimeUtc(this ICounter counter, DateTimeOffset timestamp) + catch (Exception ex) when (exceptionFilter == null || exceptionFilter(ex)) { - counter.IncTo(TimestampHelpers.ToUnixTimeSecondsAsDouble(timestamp)); + counter.Inc(); + throw; } + } - /// - /// Executes the provided operation and increments the counter if an exception occurs. The exception is re-thrown. - /// If an exception filter is specified, only counts exceptions for which the filter returns true. - /// - public static void CountExceptions(this ICounter counter, Action wrapped, Func? exceptionFilter = null) - { - if (counter == null) - throw new ArgumentNullException(nameof(counter)); + /// + /// Executes the provided operation and increments the counter if an exception occurs. The exception is re-thrown. + /// If an exception filter is specified, only counts exceptions for which the filter returns true. + /// + public static TResult CountExceptions(this ICounter counter, Func wrapped, Func? exceptionFilter = null) + { + if (counter == null) + throw new ArgumentNullException(nameof(counter)); - if (wrapped == null) - throw new ArgumentNullException(nameof(wrapped)); + if (wrapped == null) + throw new ArgumentNullException(nameof(wrapped)); - try - { - wrapped(); - } - catch (Exception ex) when (exceptionFilter == null || exceptionFilter(ex)) - { - counter.Inc(); - throw; - } + try + { + return wrapped(); } - - /// - /// Executes the provided operation and increments the counter if an exception occurs. The exception is re-thrown. - /// If an exception filter is specified, only counts exceptions for which the filter returns true. - /// - public static TResult CountExceptions(this ICounter counter, Func wrapped, Func? exceptionFilter = null) + catch (Exception ex) when (exceptionFilter == null || exceptionFilter(ex)) { - if (counter == null) - throw new ArgumentNullException(nameof(counter)); - - if (wrapped == null) - throw new ArgumentNullException(nameof(wrapped)); - - try - { - return wrapped(); - } - catch (Exception ex) when (exceptionFilter == null || exceptionFilter(ex)) - { - counter.Inc(); - throw; - } + counter.Inc(); + throw; } + } - /// - /// Executes the provided async operation and increments the counter if an exception occurs. The exception is re-thrown. - /// If an exception filter is specified, only counts exceptions for which the filter returns true. - /// - public static async Task CountExceptionsAsync(this ICounter counter, Func wrapped, Func? exceptionFilter = null) - { - if (counter == null) - throw new ArgumentNullException(nameof(counter)); + /// + /// Executes the provided async operation and increments the counter if an exception occurs. The exception is re-thrown. + /// If an exception filter is specified, only counts exceptions for which the filter returns true. + /// + public static async Task CountExceptionsAsync(this ICounter counter, Func wrapped, Func? exceptionFilter = null) + { + if (counter == null) + throw new ArgumentNullException(nameof(counter)); - if (wrapped == null) - throw new ArgumentNullException(nameof(wrapped)); + if (wrapped == null) + throw new ArgumentNullException(nameof(wrapped)); - try - { - await wrapped().ConfigureAwait(false); - } - catch (Exception ex) when (exceptionFilter == null || exceptionFilter(ex)) - { - counter.Inc(); - throw; - } + try + { + await wrapped().ConfigureAwait(false); } - - /// - /// Executes the provided async operation and increments the counter if an exception occurs. The exception is re-thrown. - /// If an exception filter is specified, only counts exceptions for which the filter returns true. - /// - public static async Task CountExceptionsAsync(this ICounter counter, Func> wrapped, Func? exceptionFilter = null) + catch (Exception ex) when (exceptionFilter == null || exceptionFilter(ex)) { - if (counter == null) - throw new ArgumentNullException(nameof(counter)); + counter.Inc(); + throw; + } + } + + /// + /// Executes the provided async operation and increments the counter if an exception occurs. The exception is re-thrown. + /// If an exception filter is specified, only counts exceptions for which the filter returns true. + /// + public static async Task CountExceptionsAsync(this ICounter counter, Func> wrapped, Func? exceptionFilter = null) + { + if (counter == null) + throw new ArgumentNullException(nameof(counter)); - if (wrapped == null) - throw new ArgumentNullException(nameof(wrapped)); + if (wrapped == null) + throw new ArgumentNullException(nameof(wrapped)); - try - { - return await wrapped().ConfigureAwait(false); - } - catch (Exception ex) when (exceptionFilter == null || exceptionFilter(ex)) - { - counter.Inc(); - throw; - } + try + { + return await wrapped().ConfigureAwait(false); + } + catch (Exception ex) when (exceptionFilter == null || exceptionFilter(ex)) + { + counter.Inc(); + throw; } } } From acf891d72c80fcb1cdbfdd48e16096b90a1b8748 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Sun, 3 Dec 2023 13:10:32 +0200 Subject: [PATCH 209/230] Tidy --- Prometheus/CanonicalLabel.cs | 18 +++++------------- Prometheus/ChildBase.cs | 13 ++++++------- Prometheus/IMetricsSerializer.cs | 4 ++-- 3 files changed, 13 insertions(+), 22 deletions(-) diff --git a/Prometheus/CanonicalLabel.cs b/Prometheus/CanonicalLabel.cs index 3d88b57f..96fc4ceb 100644 --- a/Prometheus/CanonicalLabel.cs +++ b/Prometheus/CanonicalLabel.cs @@ -1,21 +1,13 @@ namespace Prometheus; -internal readonly struct CanonicalLabel +internal readonly struct CanonicalLabel(byte[] name, byte[] prometheus, byte[] openMetrics) { - public static readonly CanonicalLabel Empty = new( - Array.Empty(), Array.Empty(), Array.Empty()); + public static readonly CanonicalLabel Empty = new([], [], []); - public CanonicalLabel(byte[] name, byte[] prometheus, byte[] openMetrics) - { - Prometheus = prometheus; - OpenMetrics = openMetrics; - Name = name; - } + public byte[] Name { get; } = name; - public byte[] Name { get; } - - public byte[] Prometheus { get; } - public byte[] OpenMetrics { get; } + public byte[] Prometheus { get; } = prometheus; + public byte[] OpenMetrics { get; } = openMetrics; public bool IsNotEmpty => Name.Length > 0; } \ No newline at end of file diff --git a/Prometheus/ChildBase.cs b/Prometheus/ChildBase.cs index ee9207cc..d233301c 100644 --- a/Prometheus/ChildBase.cs +++ b/Prometheus/ChildBase.cs @@ -1,5 +1,3 @@ -using System.Runtime.CompilerServices; - namespace Prometheus; /// @@ -67,7 +65,7 @@ public void Remove() internal byte[] FlattenedLabelsBytes => NonCapturingLazyInitializer.EnsureInitialized(ref _flattenedLabelsBytes, this, _assignFlattenedLabelsBytesFunc)!; private byte[]? _flattenedLabelsBytes; - private static readonly Action _assignFlattenedLabelsBytesFunc; + private static readonly Action _assignFlattenedLabelsBytesFunc = AssignFlattenedLabelsBytes; private static void AssignFlattenedLabelsBytes(ChildBase instance) => instance._flattenedLabelsBytes = instance.FlattenedLabels.Serialize(); internal readonly Collector Parent; @@ -96,7 +94,7 @@ internal ValueTask CollectAndSerializeAsync(IMetricsSerializer serializer, Cance /// Borrowing ensures that no other thread is modifying it (as exemplars are not thread-safe). /// You would typically want to do this while serializing the exemplar. /// - internal ObservedExemplar BorrowExemplar(ref ObservedExemplar storage) + internal static ObservedExemplar BorrowExemplar(ref ObservedExemplar storage) { return Interlocked.Exchange(ref storage, ObservedExemplar.Empty); } @@ -104,7 +102,7 @@ internal ObservedExemplar BorrowExemplar(ref ObservedExemplar storage) /// /// Returns a borrowed exemplar to storage or the object pool, with correct handling for cases where it is Empty. /// - internal void ReturnBorrowedExemplar(ref ObservedExemplar storage, ObservedExemplar borrowed) + internal static void ReturnBorrowedExemplar(ref ObservedExemplar storage, ObservedExemplar borrowed) { if (borrowed == ObservedExemplar.Empty) return; @@ -171,6 +169,9 @@ protected bool IsRecordingNewExemplarAllowed() protected void MarkNewExemplarHasBeenRecorded() { + if (_exemplarBehavior.NewExemplarMinInterval <= TimeSpan.Zero) + return; // No need to record the timestamp if we are not enforcing a minimum interval. + _exemplarLastRecordedTimestamp.Value = ExemplarRecordingTimestampProvider(); } @@ -180,8 +181,6 @@ protected void MarkNewExemplarHasBeenRecorded() static ChildBase() { - _assignFlattenedLabelsBytesFunc = AssignFlattenedLabelsBytes; - Metrics.DefaultRegistry.OnStartCollectingRegistryMetrics(delegate { ExemplarsRecorded = Metrics.CreateCounter("prometheus_net_exemplars_recorded_total", "Number of exemplars that were accepted into in-memory storage in the prometheus-net SDK."); diff --git a/Prometheus/IMetricsSerializer.cs b/Prometheus/IMetricsSerializer.cs index 3a949596..c1701503 100644 --- a/Prometheus/IMetricsSerializer.cs +++ b/Prometheus/IMetricsSerializer.cs @@ -16,13 +16,13 @@ ValueTask WriteFamilyDeclarationAsync(string name, byte[] nameBytes, byte[] help /// /// Writes out a single metric point with a floating point value. /// - ValueTask WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, CanonicalLabel canonicalLabel, + ValueTask WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, CanonicalLabel extraLabel, double value, ObservedExemplar exemplar, byte[]? suffix, CancellationToken cancel); /// /// Writes out a single metric point with an integer value. /// - ValueTask WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, CanonicalLabel canonicalLabel, + ValueTask WriteMetricPointAsync(byte[] name, byte[] flattenedLabels, CanonicalLabel extraLabel, long value, ObservedExemplar exemplar, byte[]? suffix, CancellationToken cancel); /// From ef6d22e5b1cf666f426ca675bcf8cda6b50f3e8a Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Sun, 3 Dec 2023 13:37:49 +0200 Subject: [PATCH 210/230] Add ReadOnlyMemory overload to lifetime.-managed APIs --- Prometheus/IManagedLifetimeMetricHandle.cs | 93 +++++++++++++++++++ .../LabelEnrichingManagedLifetimeCounter.cs | 62 +++++++++++-- .../LabelEnrichingManagedLifetimeGauge.cs | 62 +++++++++++-- .../LabelEnrichingManagedLifetimeHistogram.cs | 62 +++++++++++-- .../LabelEnrichingManagedLifetimeSummary.cs | 62 +++++++++++-- Prometheus/ManagedLifetimeMetricHandle.cs | 52 +++++++++++ 6 files changed, 369 insertions(+), 24 deletions(-) diff --git a/Prometheus/IManagedLifetimeMetricHandle.cs b/Prometheus/IManagedLifetimeMetricHandle.cs index 03b7e27a..0112589e 100644 --- a/Prometheus/IManagedLifetimeMetricHandle.cs +++ b/Prometheus/IManagedLifetimeMetricHandle.cs @@ -4,6 +4,10 @@ /// Handle to a metric with a lease-extended lifetime, enabling the metric to be accessed and its lifetime to be controlled. /// Each label combination is automatically deleted N seconds after the last lease on that label combination expires. /// +/// +/// When creating leases, prefer the overload that takes a ReadOnlySpan because it avoids +/// allocating a string array if the metric instance you are leasing is already alive. +/// public interface IManagedLifetimeMetricHandle where TMetricInterface : ICollectorChild { @@ -95,6 +99,95 @@ public interface IManagedLifetimeMetricHandle Task WithLeaseAsync(Func> action, params string[] labelValues); #endregion + #region Lease(ReadOnlyMemory) + /// + /// Takes a lifetime-extending lease on the metric, scoped to a specific combination of label values. + /// + /// The typical pattern is that the metric value is only modified when the caller is holding a lease on the metric. + /// Automatic removal of the metric will not occur until all leases on the metric are disposed and the expiration duration elapses. + /// + /// + /// Acquiring a new lease after the metric has been removed will re-publish the metric without preserving the old value. + /// Re-publishing may return a new instance of the metric (data collected via expired instances will not be published). + /// + IDisposable AcquireLease(out TMetricInterface metric, ReadOnlyMemory labelValues); + + /// + /// Takes a lifetime-extending lease on the metric, scoped to a specific combination of label values. + /// The lease is returned as a stack-only struct, which is faster than the IDisposable version. + /// + /// The typical pattern is that the metric value is only modified when the caller is holding a lease on the metric. + /// Automatic removal of the metric will not occur until all leases on the metric are disposed and the expiration duration elapses. + /// + /// + /// Acquiring a new lease after the metric has been removed will re-publish the metric without preserving the old value. + /// Re-publishing may return a new instance of the metric (data collected via expired instances will not be published). + /// + RefLease AcquireRefLease(out TMetricInterface metric, ReadOnlyMemory labelValues); + + /// + /// While executing an action, holds a lifetime-extending lease on the metric, scoped to a specific combination of label values. + /// + /// The typical pattern is that the metric value is only modified when the caller is holding a lease on the metric. + /// Automatic removal of the metric will not occur until all leases on the metric are disposed and the expiration duration elapses. + /// + /// + /// Acquiring a new lease after the metric has been removed will re-publish the metric without preserving the old value. + /// Re-publishing may return a new instance of the metric (data collected via expired instances will not be published). + /// + void WithLease(Action action, ReadOnlyMemory labelValues); + + /// + /// While executing an action, holds a lifetime-extending lease on the metric, scoped to a specific combination of label values. + /// Passes a given argument to the callback. + /// + /// The typical pattern is that the metric value is only modified when the caller is holding a lease on the metric. + /// Automatic removal of the metric will not occur until all leases on the metric are disposed and the expiration duration elapses. + /// + /// + /// Acquiring a new lease after the metric has been removed will re-publish the metric without preserving the old value. + /// Re-publishing may return a new instance of the metric (data collected via expired instances will not be published). + /// + void WithLease(Action action, TArg arg, ReadOnlyMemory labelValues); + + /// + /// While executing an action, holds a lifetime-extending lease on the metric, scoped to a specific combination of label values. + /// + /// The typical pattern is that the metric value is only modified when the caller is holding a lease on the metric. + /// Automatic removal of the metric will not occur until all leases on the metric are disposed and the expiration duration elapses. + /// + /// + /// Acquiring a new lease after the metric has been removed will re-publish the metric without preserving the old value. + /// Re-publishing may return a new instance of the metric (data collected via expired instances will not be published). + /// + Task WithLeaseAsync(Func func, ReadOnlyMemory labelValues); + + + /// + /// While executing a function, holds a lifetime-extending lease on the metric, scoped to a specific combination of label values. + /// + /// The typical pattern is that the metric value is only modified when the caller is holding a lease on the metric. + /// Automatic removal of the metric will not occur until all leases on the metric are disposed and the expiration duration elapses. + /// + /// + /// Acquiring a new lease after the metric has been removed will re-publish the metric without preserving the old value. + /// Re-publishing may return a new instance of the metric (data collected via expired instances will not be published). + /// + TResult WithLease(Func func, ReadOnlyMemory labelValues); + + /// + /// While executing a function, holds a lifetime-extending lease on the metric, scoped to a specific combination of label values. + /// + /// The typical pattern is that the metric value is only modified when the caller is holding a lease on the metric. + /// Automatic removal of the metric will not occur until all leases on the metric are disposed and the expiration duration elapses. + /// + /// + /// Acquiring a new lease after the metric has been removed will re-publish the metric without preserving the old value. + /// Re-publishing may return a new instance of the metric (data collected via expired instances will not be published). + /// + Task WithLeaseAsync(Func> action, ReadOnlyMemory labelValues); + #endregion + #region Lease(ReadOnlySpan) /// /// Takes a lifetime-extending lease on the metric, scoped to a specific combination of label values. diff --git a/Prometheus/LabelEnrichingManagedLifetimeCounter.cs b/Prometheus/LabelEnrichingManagedLifetimeCounter.cs index a8214c8b..e3ccad5e 100644 --- a/Prometheus/LabelEnrichingManagedLifetimeCounter.cs +++ b/Prometheus/LabelEnrichingManagedLifetimeCounter.cs @@ -14,6 +14,11 @@ public LabelEnrichingManagedLifetimeCounter(IManagedLifetimeMetricHandle _inner; private readonly string[] _enrichWithLabelValues; + public ICollector WithExtendLifetimeOnUse() + { + return new LabelEnrichingAutoLeasingMetric(_inner.WithExtendLifetimeOnUse(), _enrichWithLabelValues); + } + #region Lease(string[]) public IDisposable AcquireLease(out ICounter metric, params string[] labelValues) { @@ -25,11 +30,6 @@ public RefLease AcquireRefLease(out ICounter metric, params string[] labelValues return _inner.AcquireRefLease(out metric, WithEnrichedLabelValues(labelValues)); } - public ICollector WithExtendLifetimeOnUse() - { - return new LabelEnrichingAutoLeasingMetric(_inner.WithExtendLifetimeOnUse(), _enrichWithLabelValues); - } - public void WithLease(Action action, params string[] labelValues) { _inner.WithLease(action, WithEnrichedLabelValues(labelValues)); @@ -56,9 +56,59 @@ public Task WithLeaseAsync(Func> actio } #endregion + #region Lease(ReadOnlyMemory) + public IDisposable AcquireLease(out ICounter metric, ReadOnlyMemory labelValues) + { + return _inner.AcquireLease(out metric, WithEnrichedLabelValues(labelValues)); + } + + public RefLease AcquireRefLease(out ICounter metric, ReadOnlyMemory labelValues) + { + return _inner.AcquireRefLease(out metric, WithEnrichedLabelValues(labelValues)); + } + + public void WithLease(Action action, ReadOnlyMemory labelValues) + { + _inner.WithLease(action, WithEnrichedLabelValues(labelValues)); + } + + public void WithLease(Action action, TArg arg, ReadOnlyMemory labelValues) + { + _inner.WithLease(action, arg, WithEnrichedLabelValues(labelValues)); + } + + public TResult WithLease(Func func, ReadOnlyMemory labelValues) + { + return _inner.WithLease(func, WithEnrichedLabelValues(labelValues)); + } + + public Task WithLeaseAsync(Func func, ReadOnlyMemory labelValues) + { + return _inner.WithLeaseAsync(func, WithEnrichedLabelValues(labelValues)); + } + + public Task WithLeaseAsync(Func> action, ReadOnlyMemory labelValues) + { + return _inner.WithLeaseAsync(action, WithEnrichedLabelValues(labelValues)); + } + #endregion + private string[] WithEnrichedLabelValues(string[] instanceLabelValues) { - return _enrichWithLabelValues.Concat(instanceLabelValues).ToArray(); + var enriched = new string[_enrichWithLabelValues.Length + instanceLabelValues.Length]; + _enrichWithLabelValues.CopyTo(enriched, 0); + instanceLabelValues.CopyTo(enriched, _enrichWithLabelValues.Length); + + return enriched; + } + + private string[] WithEnrichedLabelValues(ReadOnlyMemory instanceLabelValues) + { + var enriched = new string[_enrichWithLabelValues.Length + instanceLabelValues.Length]; + _enrichWithLabelValues.CopyTo(enriched, 0); + instanceLabelValues.Span.CopyTo(enriched.AsSpan(_enrichWithLabelValues.Length)); + + return enriched; } #region Lease(ReadOnlySpan) diff --git a/Prometheus/LabelEnrichingManagedLifetimeGauge.cs b/Prometheus/LabelEnrichingManagedLifetimeGauge.cs index 5de68b4e..2f272b65 100644 --- a/Prometheus/LabelEnrichingManagedLifetimeGauge.cs +++ b/Prometheus/LabelEnrichingManagedLifetimeGauge.cs @@ -13,6 +13,11 @@ public LabelEnrichingManagedLifetimeGauge(IManagedLifetimeMetricHandle i private readonly IManagedLifetimeMetricHandle _inner; private readonly string[] _enrichWithLabelValues; + public ICollector WithExtendLifetimeOnUse() + { + return new LabelEnrichingAutoLeasingMetric(_inner.WithExtendLifetimeOnUse(), _enrichWithLabelValues); + } + #region Lease(string[]) public IDisposable AcquireLease(out IGauge metric, params string[] labelValues) { @@ -24,11 +29,6 @@ public RefLease AcquireRefLease(out IGauge metric, params string[] labelValues) return _inner.AcquireRefLease(out metric, WithEnrichedLabelValues(labelValues)); } - public ICollector WithExtendLifetimeOnUse() - { - return new LabelEnrichingAutoLeasingMetric(_inner.WithExtendLifetimeOnUse(), _enrichWithLabelValues); - } - public void WithLease(Action action, params string[] labelValues) { _inner.WithLease(action, WithEnrichedLabelValues(labelValues)); @@ -55,9 +55,59 @@ public Task WithLeaseAsync(Func> action, } #endregion + #region Lease(ReadOnlyMemory) + public IDisposable AcquireLease(out IGauge metric, ReadOnlyMemory labelValues) + { + return _inner.AcquireLease(out metric, WithEnrichedLabelValues(labelValues)); + } + + public RefLease AcquireRefLease(out IGauge metric, ReadOnlyMemory labelValues) + { + return _inner.AcquireRefLease(out metric, WithEnrichedLabelValues(labelValues)); + } + + public void WithLease(Action action, ReadOnlyMemory labelValues) + { + _inner.WithLease(action, WithEnrichedLabelValues(labelValues)); + } + + public void WithLease(Action action, TArg arg, ReadOnlyMemory labelValues) + { + _inner.WithLease(action, arg, WithEnrichedLabelValues(labelValues)); + } + + public TResult WithLease(Func func, ReadOnlyMemory labelValues) + { + return _inner.WithLease(func, WithEnrichedLabelValues(labelValues)); + } + + public Task WithLeaseAsync(Func func, ReadOnlyMemory labelValues) + { + return _inner.WithLeaseAsync(func, WithEnrichedLabelValues(labelValues)); + } + + public Task WithLeaseAsync(Func> action, ReadOnlyMemory labelValues) + { + return _inner.WithLeaseAsync(action, WithEnrichedLabelValues(labelValues)); + } + #endregion + private string[] WithEnrichedLabelValues(string[] instanceLabelValues) { - return _enrichWithLabelValues.Concat(instanceLabelValues).ToArray(); + var enriched = new string[_enrichWithLabelValues.Length + instanceLabelValues.Length]; + _enrichWithLabelValues.CopyTo(enriched, 0); + instanceLabelValues.CopyTo(enriched, _enrichWithLabelValues.Length); + + return enriched; + } + + private string[] WithEnrichedLabelValues(ReadOnlyMemory instanceLabelValues) + { + var enriched = new string[_enrichWithLabelValues.Length + instanceLabelValues.Length]; + _enrichWithLabelValues.CopyTo(enriched, 0); + instanceLabelValues.Span.CopyTo(enriched.AsSpan(_enrichWithLabelValues.Length)); + + return enriched; } #region Lease(ReadOnlySpan) diff --git a/Prometheus/LabelEnrichingManagedLifetimeHistogram.cs b/Prometheus/LabelEnrichingManagedLifetimeHistogram.cs index 9199dc3b..6a645cd5 100644 --- a/Prometheus/LabelEnrichingManagedLifetimeHistogram.cs +++ b/Prometheus/LabelEnrichingManagedLifetimeHistogram.cs @@ -13,6 +13,11 @@ public LabelEnrichingManagedLifetimeHistogram(IManagedLifetimeMetricHandle _inner; private readonly string[] _enrichWithLabelValues; + public ICollector WithExtendLifetimeOnUse() + { + return new LabelEnrichingAutoLeasingMetric(_inner.WithExtendLifetimeOnUse(), _enrichWithLabelValues); + } + #region Lease(string[]) public IDisposable AcquireLease(out IHistogram metric, params string[] labelValues) { @@ -24,11 +29,6 @@ public RefLease AcquireRefLease(out IHistogram metric, params string[] labelValu return _inner.AcquireRefLease(out metric, WithEnrichedLabelValues(labelValues)); } - public ICollector WithExtendLifetimeOnUse() - { - return new LabelEnrichingAutoLeasingMetric(_inner.WithExtendLifetimeOnUse(), _enrichWithLabelValues); - } - public void WithLease(Action action, params string[] labelValues) { _inner.WithLease(action, WithEnrichedLabelValues(labelValues)); @@ -55,9 +55,59 @@ public Task WithLeaseAsync(Func> act } #endregion + #region Lease(ReadOnlyMemory) + public IDisposable AcquireLease(out IHistogram metric, ReadOnlyMemory labelValues) + { + return _inner.AcquireLease(out metric, WithEnrichedLabelValues(labelValues)); + } + + public RefLease AcquireRefLease(out IHistogram metric, ReadOnlyMemory labelValues) + { + return _inner.AcquireRefLease(out metric, WithEnrichedLabelValues(labelValues)); + } + + public void WithLease(Action action, ReadOnlyMemory labelValues) + { + _inner.WithLease(action, WithEnrichedLabelValues(labelValues)); + } + + public void WithLease(Action action, TArg arg, ReadOnlyMemory labelValues) + { + _inner.WithLease(action, arg, WithEnrichedLabelValues(labelValues)); + } + + public TResult WithLease(Func func, ReadOnlyMemory labelValues) + { + return _inner.WithLease(func, WithEnrichedLabelValues(labelValues)); + } + + public Task WithLeaseAsync(Func func, ReadOnlyMemory labelValues) + { + return _inner.WithLeaseAsync(func, WithEnrichedLabelValues(labelValues)); + } + + public Task WithLeaseAsync(Func> action, ReadOnlyMemory labelValues) + { + return _inner.WithLeaseAsync(action, WithEnrichedLabelValues(labelValues)); + } + #endregion + private string[] WithEnrichedLabelValues(string[] instanceLabelValues) { - return _enrichWithLabelValues.Concat(instanceLabelValues).ToArray(); + var enriched = new string[_enrichWithLabelValues.Length + instanceLabelValues.Length]; + _enrichWithLabelValues.CopyTo(enriched, 0); + instanceLabelValues.CopyTo(enriched, _enrichWithLabelValues.Length); + + return enriched; + } + + private string[] WithEnrichedLabelValues(ReadOnlyMemory instanceLabelValues) + { + var enriched = new string[_enrichWithLabelValues.Length + instanceLabelValues.Length]; + _enrichWithLabelValues.CopyTo(enriched, 0); + instanceLabelValues.Span.CopyTo(enriched.AsSpan(_enrichWithLabelValues.Length)); + + return enriched; } #region Lease(ReadOnlySpan) diff --git a/Prometheus/LabelEnrichingManagedLifetimeSummary.cs b/Prometheus/LabelEnrichingManagedLifetimeSummary.cs index bca06701..796045fb 100644 --- a/Prometheus/LabelEnrichingManagedLifetimeSummary.cs +++ b/Prometheus/LabelEnrichingManagedLifetimeSummary.cs @@ -13,6 +13,11 @@ public LabelEnrichingManagedLifetimeSummary(IManagedLifetimeMetricHandle _inner; private readonly string[] _enrichWithLabelValues; + public ICollector WithExtendLifetimeOnUse() + { + return new LabelEnrichingAutoLeasingMetric(_inner.WithExtendLifetimeOnUse(), _enrichWithLabelValues); + } + #region Lease(string[]) public IDisposable AcquireLease(out ISummary metric, params string[] labelValues) { @@ -24,11 +29,6 @@ public RefLease AcquireRefLease(out ISummary metric, params string[] labelValues return _inner.AcquireRefLease(out metric, WithEnrichedLabelValues(labelValues)); } - public ICollector WithExtendLifetimeOnUse() - { - return new LabelEnrichingAutoLeasingMetric(_inner.WithExtendLifetimeOnUse(), _enrichWithLabelValues); - } - public void WithLease(Action action, params string[] labelValues) { _inner.WithLease(action, WithEnrichedLabelValues(labelValues)); @@ -55,9 +55,59 @@ public Task WithLeaseAsync(Func> actio } #endregion + #region Lease(ReadOnlyMemory) + public IDisposable AcquireLease(out ISummary metric, ReadOnlyMemory labelValues) + { + return _inner.AcquireLease(out metric, WithEnrichedLabelValues(labelValues)); + } + + public RefLease AcquireRefLease(out ISummary metric, ReadOnlyMemory labelValues) + { + return _inner.AcquireRefLease(out metric, WithEnrichedLabelValues(labelValues)); + } + + public void WithLease(Action action, ReadOnlyMemory labelValues) + { + _inner.WithLease(action, WithEnrichedLabelValues(labelValues)); + } + + public void WithLease(Action action, TArg arg, ReadOnlyMemory labelValues) + { + _inner.WithLease(action, arg, WithEnrichedLabelValues(labelValues)); + } + + public TResult WithLease(Func func, ReadOnlyMemory labelValues) + { + return _inner.WithLease(func, WithEnrichedLabelValues(labelValues)); + } + + public Task WithLeaseAsync(Func func, ReadOnlyMemory labelValues) + { + return _inner.WithLeaseAsync(func, WithEnrichedLabelValues(labelValues)); + } + + public Task WithLeaseAsync(Func> action, ReadOnlyMemory labelValues) + { + return _inner.WithLeaseAsync(action, WithEnrichedLabelValues(labelValues)); + } + #endregion + private string[] WithEnrichedLabelValues(string[] instanceLabelValues) { - return _enrichWithLabelValues.Concat(instanceLabelValues).ToArray(); + var enriched = new string[_enrichWithLabelValues.Length + instanceLabelValues.Length]; + _enrichWithLabelValues.CopyTo(enriched, 0); + instanceLabelValues.CopyTo(enriched, _enrichWithLabelValues.Length); + + return enriched; + } + + private string[] WithEnrichedLabelValues(ReadOnlyMemory instanceLabelValues) + { + var enriched = new string[_enrichWithLabelValues.Length + instanceLabelValues.Length]; + _enrichWithLabelValues.CopyTo(enriched, 0); + instanceLabelValues.Span.CopyTo(enriched.AsSpan(_enrichWithLabelValues.Length)); + + return enriched; } #region Lease(ReadOnlySpan) diff --git a/Prometheus/ManagedLifetimeMetricHandle.cs b/Prometheus/ManagedLifetimeMetricHandle.cs index e12d0ce3..174abdba 100644 --- a/Prometheus/ManagedLifetimeMetricHandle.cs +++ b/Prometheus/ManagedLifetimeMetricHandle.cs @@ -80,6 +80,58 @@ public async Task WithLeaseAsync(Func) + public IDisposable AcquireLease(out TMetricInterface metric, ReadOnlyMemory labelValues) + { + var child = _metric.WithLabels(labelValues); + metric = child; + + return TakeLease(child); + } + + public RefLease AcquireRefLease(out TMetricInterface metric, ReadOnlyMemory labelValues) + { + var child = _metric.WithLabels(labelValues); + metric = child; + + return TakeRefLease(child); + } + + public void WithLease(Action action, ReadOnlyMemory labelValues) + { + var child = _metric.WithLabels(labelValues); + using var lease = TakeRefLease(child); + + action(child); + } + + public void WithLease(Action action, TArg arg, ReadOnlyMemory labelValues) + { + var child = _metric.WithLabels(labelValues); + using var lease = TakeRefLease(child); + + action(arg, child); + } + + public async Task WithLeaseAsync(Func action, ReadOnlyMemory labelValues) + { + using var lease = AcquireLease(out var metric, labelValues); + await action(metric); + } + + public TResult WithLease(Func func, ReadOnlyMemory labelValues) + { + using var lease = AcquireLease(out var metric, labelValues); + return func(metric); + } + + public async Task WithLeaseAsync(Func> func, ReadOnlyMemory labelValues) + { + using var lease = AcquireLease(out var metric, labelValues); + return await func(metric); + } + #endregion + #region Lease(ReadOnlySpan) public IDisposable AcquireLease(out TMetricInterface metric, ReadOnlySpan labelValues) { From ff22ebf1fdaa28aa038b491ff364ed750fcaa7bd Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Sun, 3 Dec 2023 13:44:25 +0200 Subject: [PATCH 211/230] Code tidy --- Prometheus/CollectorIdentity.cs | 21 +- Prometheus/CounterConfiguration.cs | 19 +- Prometheus/DelegatingStreamInternal.cs | 223 +++++++++--------- Prometheus/DiagnosticSourceAdapter.cs | 185 +++++++-------- Prometheus/DiagnosticSourceAdapterOptions.cs | 19 +- .../EventCounterAdapterEventSourceSettings.cs | 25 +- Prometheus/MetricConfiguration.cs | 45 ++-- 7 files changed, 257 insertions(+), 280 deletions(-) diff --git a/Prometheus/CollectorIdentity.cs b/Prometheus/CollectorIdentity.cs index 0f90cebf..c89729bf 100644 --- a/Prometheus/CollectorIdentity.cs +++ b/Prometheus/CollectorIdentity.cs @@ -5,20 +5,12 @@ /// * Any difference in static labels (keys or values) means it is a different collector. /// * Any difference in the names of instance labels means it is a different collector. /// -internal readonly struct CollectorIdentity : IEquatable +internal readonly struct CollectorIdentity(StringSequence instanceLabelNames, LabelSequence staticLabels) : IEquatable { - public readonly StringSequence InstanceLabelNames; - public readonly LabelSequence StaticLabels; + public readonly StringSequence InstanceLabelNames = instanceLabelNames; + public readonly LabelSequence StaticLabels = staticLabels; - private readonly int _hashCode; - - public CollectorIdentity(StringSequence instanceLabelNames, LabelSequence staticLabels) - { - InstanceLabelNames = instanceLabelNames; - StaticLabels = staticLabels; - - _hashCode = CalculateHashCode(instanceLabelNames, staticLabels); - } + private readonly int _hashCode = CalculateHashCode(instanceLabelNames, staticLabels); public bool Equals(CollectorIdentity other) { @@ -59,4 +51,9 @@ public override string ToString() { return $"{_hashCode}{{{InstanceLabelNames.Length} + {StaticLabels.Length}}}"; } + + public override bool Equals(object? obj) + { + return obj is CollectorIdentity identity && Equals(identity); + } } \ No newline at end of file diff --git a/Prometheus/CounterConfiguration.cs b/Prometheus/CounterConfiguration.cs index dbcaebc5..42f713ca 100644 --- a/Prometheus/CounterConfiguration.cs +++ b/Prometheus/CounterConfiguration.cs @@ -1,13 +1,12 @@ -namespace Prometheus +namespace Prometheus; + +public sealed class CounterConfiguration : MetricConfiguration { - public sealed class CounterConfiguration : MetricConfiguration - { - internal static readonly CounterConfiguration Default = new CounterConfiguration(); + internal static readonly CounterConfiguration Default = new(); - /// - /// Allows you to configure how exemplars are applied to the published metric. - /// If null, inherits the exemplar behavior from the metric factory. - /// - public ExemplarBehavior? ExemplarBehavior { get; set; } - } + /// + /// Allows you to configure how exemplars are applied to the published metric. + /// If null, inherits the exemplar behavior from the metric factory. + /// + public ExemplarBehavior? ExemplarBehavior { get; set; } } diff --git a/Prometheus/DelegatingStreamInternal.cs b/Prometheus/DelegatingStreamInternal.cs index f61759bc..1b8a6121 100644 --- a/Prometheus/DelegatingStreamInternal.cs +++ b/Prometheus/DelegatingStreamInternal.cs @@ -5,148 +5,147 @@ #nullable enable -namespace Prometheus +namespace Prometheus; + +// Forwards all calls to an inner stream except where overridden in a derived class. +internal abstract class DelegatingStreamInternal : Stream { - // Forwards all calls to an inner stream except where overridden in a derived class. - internal abstract class DelegatingStreamInternal : Stream - { - private readonly Stream _innerStream; + private readonly Stream _innerStream; - #region Properties + #region Properties - public override bool CanRead - { - get { return _innerStream.CanRead; } - } + public override bool CanRead + { + get { return _innerStream.CanRead; } + } - public override bool CanSeek - { - get { return _innerStream.CanSeek; } - } + public override bool CanSeek + { + get { return _innerStream.CanSeek; } + } - public override bool CanWrite - { - get { return _innerStream.CanWrite; } - } + public override bool CanWrite + { + get { return _innerStream.CanWrite; } + } - public override long Length - { - get { return _innerStream.Length; } - } + public override long Length + { + get { return _innerStream.Length; } + } - public override long Position - { - get { return _innerStream.Position; } - set { _innerStream.Position = value; } - } + public override long Position + { + get { return _innerStream.Position; } + set { _innerStream.Position = value; } + } - public override int ReadTimeout - { - get { return _innerStream.ReadTimeout; } - set { _innerStream.ReadTimeout = value; } - } + public override int ReadTimeout + { + get { return _innerStream.ReadTimeout; } + set { _innerStream.ReadTimeout = value; } + } - public override bool CanTimeout - { - get { return _innerStream.CanTimeout; } - } + public override bool CanTimeout + { + get { return _innerStream.CanTimeout; } + } - public override int WriteTimeout - { - get { return _innerStream.WriteTimeout; } - set { _innerStream.WriteTimeout = value; } - } + public override int WriteTimeout + { + get { return _innerStream.WriteTimeout; } + set { _innerStream.WriteTimeout = value; } + } - #endregion Properties + #endregion Properties - protected DelegatingStreamInternal(Stream innerStream) - { - _innerStream = innerStream; - } + protected DelegatingStreamInternal(Stream innerStream) + { + _innerStream = innerStream; + } - protected override void Dispose(bool disposing) + protected override void Dispose(bool disposing) + { + if (disposing) { - if (disposing) - { - _innerStream.Dispose(); - } - base.Dispose(disposing); + _innerStream.Dispose(); } + base.Dispose(disposing); + } - public override long Seek(long offset, SeekOrigin origin) - { - return _innerStream.Seek(offset, origin); - } + public override long Seek(long offset, SeekOrigin origin) + { + return _innerStream.Seek(offset, origin); + } - public override int Read(byte[] buffer, int offset, int count) - { - return _innerStream.Read(buffer, offset, count); - } + public override int Read(byte[] buffer, int offset, int count) + { + return _innerStream.Read(buffer, offset, count); + } - public override int ReadByte() - { - return _innerStream.ReadByte(); - } + public override int ReadByte() + { + return _innerStream.ReadByte(); + } - public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - return _innerStream.ReadAsync(buffer, offset, count, cancellationToken); - } - public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) - { - return _innerStream.BeginRead(buffer, offset, count, callback, state); - } + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _innerStream.ReadAsync(buffer, offset, count, cancellationToken); + } + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) + { + return _innerStream.BeginRead(buffer, offset, count, callback, state); + } - public override int EndRead(IAsyncResult asyncResult) - { - return _innerStream.EndRead(asyncResult); - } + public override int EndRead(IAsyncResult asyncResult) + { + return _innerStream.EndRead(asyncResult); + } - public override void Flush() - { - _innerStream.Flush(); - } + public override void Flush() + { + _innerStream.Flush(); + } - public override Task FlushAsync(CancellationToken cancellationToken) - { - return _innerStream.FlushAsync(cancellationToken); - } + public override Task FlushAsync(CancellationToken cancellationToken) + { + return _innerStream.FlushAsync(cancellationToken); + } - public override void SetLength(long value) - { - _innerStream.SetLength(value); - } + public override void SetLength(long value) + { + _innerStream.SetLength(value); + } - public override void Write(byte[] buffer, int offset, int count) - { - _innerStream.Write(buffer, offset, count); - } + public override void Write(byte[] buffer, int offset, int count) + { + _innerStream.Write(buffer, offset, count); + } - public override void WriteByte(byte value) - { - _innerStream.WriteByte(value); - } + public override void WriteByte(byte value) + { + _innerStream.WriteByte(value); + } - public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - return _innerStream.WriteAsync(buffer, offset, count, cancellationToken); - } + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return _innerStream.WriteAsync(buffer, offset, count, cancellationToken); + } - public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) - { - return _innerStream.BeginWrite(buffer, offset, count, callback, state); - } + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) + { + return _innerStream.BeginWrite(buffer, offset, count, callback, state); + } - public override void EndWrite(IAsyncResult asyncResult) - { - _innerStream.EndWrite(asyncResult); - } + public override void EndWrite(IAsyncResult asyncResult) + { + _innerStream.EndWrite(asyncResult); + } - public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) - { - return _innerStream.CopyToAsync(destination, bufferSize, cancellationToken); - } + public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + return _innerStream.CopyToAsync(destination, bufferSize, cancellationToken); } } diff --git a/Prometheus/DiagnosticSourceAdapter.cs b/Prometheus/DiagnosticSourceAdapter.cs index 8a407e20..483482e4 100644 --- a/Prometheus/DiagnosticSourceAdapter.cs +++ b/Prometheus/DiagnosticSourceAdapter.cs @@ -1,132 +1,117 @@ #if NET using System.Diagnostics; -namespace Prometheus +namespace Prometheus; + +/// +/// Monitors all DiagnosticSource events and exposes them as Prometheus counters. +/// The event data is discarded, only the number of occurrences is measured. +/// +/// +/// This is a very coarse data set due to lacking any intelligence on the payload. +/// Users are recommended to make custom adapters with more detail for specific use cases. +/// +public sealed class DiagnosticSourceAdapter : IDisposable { /// - /// Monitors all DiagnosticSource events and exposes them as Prometheus counters. - /// The event data is discarded, only the number of occurrences is measured. + /// Starts listening for DiagnosticSource events and reporting them as Prometheus metrics. + /// Dispose of the return value to stop listening. /// - /// - /// This is a very coarse data set due to lacking any intelligence on the payload. - /// Users are recommended to make custom adapters with more detail for specific use cases. - /// - public sealed class DiagnosticSourceAdapter : IDisposable + public static IDisposable StartListening() => StartListening(DiagnosticSourceAdapterOptions.Default); + + /// + /// Starts listening for DiagnosticSource events and reporting them as Prometheus metrics. + /// Dispose of the return value to stop listening. + /// + public static IDisposable StartListening(DiagnosticSourceAdapterOptions options) => new DiagnosticSourceAdapter(options); + + private DiagnosticSourceAdapter(DiagnosticSourceAdapterOptions options) { - /// - /// Starts listening for DiagnosticSource events and reporting them as Prometheus metrics. - /// Dispose of the return value to stop listening. - /// - public static IDisposable StartListening() => StartListening(DiagnosticSourceAdapterOptions.Default); - - /// - /// Starts listening for DiagnosticSource events and reporting them as Prometheus metrics. - /// Dispose of the return value to stop listening. - /// - public static IDisposable StartListening(DiagnosticSourceAdapterOptions options) => new DiagnosticSourceAdapter(options); - - private DiagnosticSourceAdapter(DiagnosticSourceAdapterOptions options) - { - _options = options; - _metric = Metrics.WithCustomRegistry(options.Registry) - .CreateCounter("diagnostic_events_total", "Total count of events received via the DiagnosticSource infrastructure.", labelNames: new[] - { - "source", // Name of the DiagnosticSource - "event" // Name of the event - }); - - var newListenerObserver = new NewListenerObserver(OnNewListener); - _newListenerSubscription = DiagnosticListener.AllListeners.Subscribe(newListenerObserver); - } + _options = options; + _metric = Metrics.WithCustomRegistry(options.Registry) + .CreateCounter("diagnostic_events_total", "Total count of events received via the DiagnosticSource infrastructure.", labelNames: new[] + { + "source", // Name of the DiagnosticSource + "event" // Name of the event + }); - private readonly DiagnosticSourceAdapterOptions _options; - private readonly Counter _metric; + var newListenerObserver = new NewListenerObserver(OnNewListener); + _newListenerSubscription = DiagnosticListener.AllListeners.Subscribe(newListenerObserver); + } + + private readonly DiagnosticSourceAdapterOptions _options; + private readonly Counter _metric; - private readonly IDisposable _newListenerSubscription; + private readonly IDisposable _newListenerSubscription; - // listener name -> subscription - private readonly Dictionary _newEventSubscription = new Dictionary(); - private readonly object _newEventSubscriptionLock = new object(); + // listener name -> subscription + private readonly Dictionary _newEventSubscription = new Dictionary(); + private readonly object _newEventSubscriptionLock = new object(); - private void OnNewListener(DiagnosticListener listener) + private void OnNewListener(DiagnosticListener listener) + { + lock (_newEventSubscriptionLock) { - lock (_newEventSubscriptionLock) + if (_newEventSubscription.TryGetValue(listener.Name, out var oldSubscription)) { - if (_newEventSubscription.TryGetValue(listener.Name, out var oldSubscription)) - { - oldSubscription.Dispose(); - _newEventSubscription.Remove(listener.Name); - } - - if (!_options.ListenerFilterPredicate(listener)) - return; - - var listenerName = listener.Name; - var newEventObserver = new NewEventObserver(kvp => OnEvent(listenerName, kvp.Key, kvp.Value)); - _newEventSubscription[listenerName] = listener.Subscribe(newEventObserver); + oldSubscription.Dispose(); + _newEventSubscription.Remove(listener.Name); } + + if (!_options.ListenerFilterPredicate(listener)) + return; + + var listenerName = listener.Name; + var newEventObserver = new NewEventObserver(kvp => OnEvent(listenerName, kvp.Key, kvp.Value)); + _newEventSubscription[listenerName] = listener.Subscribe(newEventObserver); } + } - private void OnEvent(string listenerName, string eventName, object? payload) + private void OnEvent(string listenerName, string eventName, object? payload) + { + _metric.WithLabels(listenerName, eventName).Inc(); + } + + private sealed class NewListenerObserver(Action onNewListener) : IObserver + { + public void OnCompleted() { - _metric.WithLabels(listenerName, eventName).Inc(); } - private sealed class NewListenerObserver : IObserver + public void OnError(Exception error) { - private readonly Action _onNewListener; - - public NewListenerObserver(Action onNewListener) - { - _onNewListener = onNewListener; - } - - public void OnCompleted() - { - } - - public void OnError(Exception error) - { - } - - public void OnNext(DiagnosticListener listener) - { - _onNewListener(listener); - } } - private sealed class NewEventObserver : IObserver> + public void OnNext(DiagnosticListener listener) { - private readonly Action> _onEvent; - - public NewEventObserver(Action> onEvent) - { - _onEvent = onEvent; - } - - public void OnCompleted() - { - } + onNewListener(listener); + } + } - public void OnError(Exception error) - { - } + private sealed class NewEventObserver(Action> onEvent) : IObserver> + { + public void OnCompleted() + { + } - public void OnNext(KeyValuePair receivedEvent) - { - _onEvent(receivedEvent); - } + public void OnError(Exception error) + { } - public void Dispose() + public void OnNext(KeyValuePair receivedEvent) { - _newListenerSubscription.Dispose(); + onEvent(receivedEvent); + } + } - lock (_newEventSubscriptionLock) - { - foreach (var subscription in _newEventSubscription.Values) - subscription.Dispose(); - } + public void Dispose() + { + _newListenerSubscription.Dispose(); + + lock (_newEventSubscriptionLock) + { + foreach (var subscription in _newEventSubscription.Values) + subscription.Dispose(); } } } diff --git a/Prometheus/DiagnosticSourceAdapterOptions.cs b/Prometheus/DiagnosticSourceAdapterOptions.cs index 2bf9cf47..b86ecda8 100644 --- a/Prometheus/DiagnosticSourceAdapterOptions.cs +++ b/Prometheus/DiagnosticSourceAdapterOptions.cs @@ -1,18 +1,17 @@ #if NET using System.Diagnostics; -namespace Prometheus +namespace Prometheus; + +public sealed class DiagnosticSourceAdapterOptions { - public sealed class DiagnosticSourceAdapterOptions - { - internal static readonly DiagnosticSourceAdapterOptions Default = new DiagnosticSourceAdapterOptions(); + internal static readonly DiagnosticSourceAdapterOptions Default = new(); - /// - /// By default we subscribe to all listeners but this allows you to filter by listener. - /// - public Func ListenerFilterPredicate = _ => true; + /// + /// By default we subscribe to all listeners but this allows you to filter by listener. + /// + public Func ListenerFilterPredicate = _ => true; - public CollectorRegistry Registry = Metrics.DefaultRegistry; - } + public CollectorRegistry Registry = Metrics.DefaultRegistry; } #endif \ No newline at end of file diff --git a/Prometheus/EventCounterAdapterEventSourceSettings.cs b/Prometheus/EventCounterAdapterEventSourceSettings.cs index b380229b..ab08dd2f 100644 --- a/Prometheus/EventCounterAdapterEventSourceSettings.cs +++ b/Prometheus/EventCounterAdapterEventSourceSettings.cs @@ -1,20 +1,19 @@ using System.Diagnostics.Tracing; -namespace Prometheus +namespace Prometheus; + +/// +/// Defines how the EventCounterAdapter will subscribe to an event source. +/// +public sealed class EventCounterAdapterEventSourceSettings { /// - /// Defines how the EventCounterAdapter will subscribe to an event source. + /// Minimum level of events to receive. /// - public sealed class EventCounterAdapterEventSourceSettings - { - /// - /// Minimum level of events to receive. - /// - public EventLevel MinimumLevel { get; set; } = EventLevel.Informational; + public EventLevel MinimumLevel { get; set; } = EventLevel.Informational; - /// - /// Event keywords, of which at least one must match for an event to be received. - /// - public EventKeywords MatchKeywords { get; set; } = EventKeywords.None; - } + /// + /// Event keywords, of which at least one must match for an event to be received. + /// + public EventKeywords MatchKeywords { get; set; } = EventKeywords.None; } diff --git a/Prometheus/MetricConfiguration.cs b/Prometheus/MetricConfiguration.cs index 3f66e2d0..07cc7fbc 100644 --- a/Prometheus/MetricConfiguration.cs +++ b/Prometheus/MetricConfiguration.cs @@ -1,28 +1,27 @@ -namespace Prometheus +namespace Prometheus; + +/// +/// This class packages the options for creating metrics into a single class (with subclasses per metric type) +/// for easy extensibility of the API without adding numerous method overloads whenever new options are added. +/// +public abstract class MetricConfiguration { /// - /// This class packages the options for creating metrics into a single class (with subclasses per metric type) - /// for easy extensibility of the API without adding numerous method overloads whenever new options are added. + /// NOTE: Only used by APIs that do not take an explicit labelNames value as input. + /// + /// Names of all the label fields that are defined for each instance of the metric. + /// If null, the metric will be created without any instance-specific labels. + /// + /// Before using a metric that uses instance-specific labels, .WithLabels() must be called to provide values for the labels. /// - public abstract class MetricConfiguration - { - /// - /// NOTE: Only used by APIs that do not take an explicit labelNames value as input. - /// - /// Names of all the label fields that are defined for each instance of the metric. - /// If null, the metric will be created without any instance-specific labels. - /// - /// Before using a metric that uses instance-specific labels, .WithLabels() must be called to provide values for the labels. - /// - public string[]? LabelNames { get; set; } + public string[]? LabelNames { get; set; } - /// - /// If true, the metric will not be published until its value is first modified (regardless of the specific value). - /// This is useful to delay publishing gauges that get their initial values delay-loaded. - /// - /// By default, metrics are published as soon as possible - if they do not use labels then they are published on - /// creation and if they use labels then as soon as the label values are assigned. - /// - public bool SuppressInitialValue { get; set; } - } + /// + /// If true, the metric will not be published until its value is first modified (regardless of the specific value). + /// This is useful to delay publishing gauges that get their initial values delay-loaded. + /// + /// By default, metrics are published as soon as possible - if they do not use labels then they are published on + /// creation and if they use labels then as soon as the label values are assigned. + /// + public bool SuppressInitialValue { get; set; } } From b3a58d278812d547a28e9d47e7ba91f16c90d933 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Sun, 3 Dec 2023 19:42:12 +0200 Subject: [PATCH 212/230] Replace some runtime-encoded string constants with compile-time-encoded constants --- Prometheus/Histogram.cs | 8 ++--- Prometheus/Summary.cs | 6 ++-- Prometheus/TextSerializer.Net.cs | 10 +++--- Prometheus/TextSerializer.NetStandardFx.cs | 42 +++++++++++----------- 4 files changed, 32 insertions(+), 34 deletions(-) diff --git a/Prometheus/Histogram.cs b/Prometheus/Histogram.cs index 6a9ac093..c2755c2e 100644 --- a/Prometheus/Histogram.cs +++ b/Prometheus/Histogram.cs @@ -31,7 +31,7 @@ public sealed class Histogram : Collector, IHistogram // These labels go together with the buckets, so we do not need to allocate them for every child. private readonly CanonicalLabel[] _leLabels; - private static readonly byte[] LeLabelName = PrometheusConstants.ExportEncoding.GetBytes("le"); + private static readonly byte[] LeLabelName = "le"u8.ToArray(); internal Histogram(string name, string help, StringSequence instanceLabelNames, LabelSequence staticLabels, bool suppressInitialValue, double[]? buckets, ExemplarBehavior exemplarBehavior) : base(name, help, instanceLabelNames, staticLabels, suppressInitialValue, exemplarBehavior) @@ -118,9 +118,9 @@ internal Child(Histogram parent, LabelSequence instanceLabels, LabelSequence fla private ThreadSafeDouble _sum = new(0.0D); private readonly ThreadSafeLong[] _bucketCounts; - private static readonly byte[] SumSuffix = PrometheusConstants.ExportEncoding.GetBytes("sum"); - private static readonly byte[] CountSuffix = PrometheusConstants.ExportEncoding.GetBytes("count"); - private static readonly byte[] BucketSuffix = PrometheusConstants.ExportEncoding.GetBytes("bucket"); + private static readonly byte[] SumSuffix = "sum"u8.ToArray(); + private static readonly byte[] CountSuffix = "count"u8.ToArray(); + private static readonly byte[] BucketSuffix = "bucket"u8.ToArray(); private readonly ObservedExemplar[] _exemplars; #if NET diff --git a/Prometheus/Summary.cs b/Prometheus/Summary.cs index a04b37cd..1b027c72 100644 --- a/Prometheus/Summary.cs +++ b/Prometheus/Summary.cs @@ -55,7 +55,7 @@ public sealed class Summary : Collector, ISummary // These labels go together with the objectives, so we do not need to allocate them for every child. private readonly CanonicalLabel[] _quantileLabels; - private static readonly byte[] QuantileLabelName = PrometheusConstants.ExportEncoding.GetBytes("quantile"); + private static readonly byte[] QuantileLabelName = "quantile"u8.ToArray(); internal Summary( string name, @@ -141,8 +141,8 @@ internal Child(Summary parent, LabelSequence instanceLabels, LabelSequence flatt private readonly Summary _parent; - private static readonly byte[] SumSuffix = PrometheusConstants.ExportEncoding.GetBytes("sum"); - private static readonly byte[] CountSuffix = PrometheusConstants.ExportEncoding.GetBytes("count"); + private static readonly byte[] SumSuffix = "sum"u8.ToArray(); + private static readonly byte[] CountSuffix = "count"u8.ToArray(); #if NET [AsyncMethodBuilder(typeof(PoolingAsyncValueTaskMethodBuilder))] diff --git a/Prometheus/TextSerializer.Net.cs b/Prometheus/TextSerializer.Net.cs index d34ff678..f93fee80 100644 --- a/Prometheus/TextSerializer.Net.cs +++ b/Prometheus/TextSerializer.Net.cs @@ -33,16 +33,16 @@ internal sealed class TextSerializer : IMetricsSerializer internal static ReadOnlySpan HashHelpSpace => [(byte)'#', (byte)' ', (byte)'H', (byte)'E', (byte)'L', (byte)'P', (byte)' ']; internal static ReadOnlySpan NewlineHashTypeSpace => [(byte)'\n', (byte)'#', (byte)' ', (byte)'T', (byte)'Y', (byte)'P', (byte)'E', (byte)' ']; - internal static readonly byte[] UnknownBytes = PrometheusConstants.ExportEncoding.GetBytes("unknown"); + internal static readonly byte[] UnknownBytes = "unknown"u8.ToArray(); internal static readonly byte[] EofNewLineBytes = [(byte)'#', (byte)' ', (byte)'E', (byte)'O', (byte)'F', (byte)'\n']; internal static readonly byte[] PositiveInfinityBytes = [(byte)'+', (byte)'I', (byte)'n', (byte)'f']; internal static readonly Dictionary MetricTypeToBytes = new() { - { MetricType.Gauge, PrometheusConstants.ExportEncoding.GetBytes("gauge") }, - { MetricType.Counter, PrometheusConstants.ExportEncoding.GetBytes("counter") }, - { MetricType.Histogram, PrometheusConstants.ExportEncoding.GetBytes("histogram") }, - { MetricType.Summary, PrometheusConstants.ExportEncoding.GetBytes("summary") }, + { MetricType.Gauge, "gauge"u8.ToArray() }, + { MetricType.Counter, "counter"u8.ToArray() }, + { MetricType.Histogram, "histogram"u8.ToArray() }, + { MetricType.Summary, "summary"u8.ToArray() }, }; private static readonly char[] DotEChar = ['.', 'e']; diff --git a/Prometheus/TextSerializer.NetStandardFx.cs b/Prometheus/TextSerializer.NetStandardFx.cs index 1fd4629f..9b012c21 100644 --- a/Prometheus/TextSerializer.NetStandardFx.cs +++ b/Prometheus/TextSerializer.NetStandardFx.cs @@ -17,30 +17,28 @@ internal sealed class TextSerializer : IMetricsSerializer internal static readonly byte[] RightBraceSpace = [(byte)'}', (byte)' ']; internal static readonly byte[] Space = [(byte)' ']; internal static readonly byte[] SpaceHashSpaceLeftBrace = [(byte)' ', (byte)'#', (byte)' ', (byte)'{']; - internal static readonly byte[] PositiveInfinity = PrometheusConstants.ExportEncoding.GetBytes("+Inf"); - internal static readonly byte[] NegativeInfinity = PrometheusConstants.ExportEncoding.GetBytes("-Inf"); - internal static readonly byte[] NotANumber = PrometheusConstants.ExportEncoding.GetBytes("NaN"); - internal static readonly byte[] DotZero = PrometheusConstants.ExportEncoding.GetBytes(".0"); - internal static readonly byte[] FloatPositiveOne = PrometheusConstants.ExportEncoding.GetBytes("1.0"); - internal static readonly byte[] FloatZero = PrometheusConstants.ExportEncoding.GetBytes("0.0"); - internal static readonly byte[] FloatNegativeOne = PrometheusConstants.ExportEncoding.GetBytes("-1.0"); - internal static readonly byte[] IntPositiveOne = PrometheusConstants.ExportEncoding.GetBytes("1"); - internal static readonly byte[] IntZero = PrometheusConstants.ExportEncoding.GetBytes("0"); - internal static readonly byte[] IntNegativeOne = PrometheusConstants.ExportEncoding.GetBytes("-1"); - internal static readonly byte[] EofNewLine = PrometheusConstants.ExportEncoding.GetBytes("# EOF\n"); - internal static readonly byte[] HashHelpSpace = PrometheusConstants.ExportEncoding.GetBytes("# HELP "); - internal static readonly byte[] NewlineHashTypeSpace = PrometheusConstants.ExportEncoding.GetBytes("\n# TYPE "); - - internal static readonly byte[] Unknown = PrometheusConstants.ExportEncoding.GetBytes("unknown"); - - internal static readonly byte[] PositiveInfinityBytes = PrometheusConstants.ExportEncoding.GetBytes("+Inf"); + internal static readonly byte[] PositiveInfinity = "+Inf"u8.ToArray(); + internal static readonly byte[] NegativeInfinity = "-Inf"u8.ToArray(); + internal static readonly byte[] NotANumber = "NaN"u8.ToArray(); + internal static readonly byte[] DotZero = ".0"u8.ToArray(); + internal static readonly byte[] FloatPositiveOne = "1.0"u8.ToArray(); + internal static readonly byte[] FloatZero = "0.0"u8.ToArray(); + internal static readonly byte[] FloatNegativeOne = "-1.0"u8.ToArray(); + internal static readonly byte[] IntPositiveOne = "1"u8.ToArray(); + internal static readonly byte[] IntZero = "0"u8.ToArray(); + internal static readonly byte[] IntNegativeOne = "-1"u8.ToArray(); + internal static readonly byte[] EofNewLine = "# EOF\n"u8.ToArray(); + internal static readonly byte[] HashHelpSpace = "# HELP "u8.ToArray(); + internal static readonly byte[] NewlineHashTypeSpace = "\n# TYPE "u8.ToArray(); + + internal static readonly byte[] Unknown = "unknown"u8.ToArray(); internal static readonly Dictionary MetricTypeToBytes = new() { - { MetricType.Gauge, PrometheusConstants.ExportEncoding.GetBytes("gauge") }, - { MetricType.Counter, PrometheusConstants.ExportEncoding.GetBytes("counter") }, - { MetricType.Histogram, PrometheusConstants.ExportEncoding.GetBytes("histogram") }, - { MetricType.Summary, PrometheusConstants.ExportEncoding.GetBytes("summary") }, + { MetricType.Gauge, "gauge"u8.ToArray() }, + { MetricType.Counter, "counter"u8.ToArray() }, + { MetricType.Histogram, "histogram"u8.ToArray() }, + { MetricType.Summary, "summary"u8.ToArray() }, }; private static readonly char[] DotEChar = ['.', 'e']; @@ -298,7 +296,7 @@ await _stream.Value.WriteAsync( internal static CanonicalLabel EncodeValueAsCanonicalLabel(byte[] name, double value) { if (double.IsPositiveInfinity(value)) - return new CanonicalLabel(name, PositiveInfinityBytes, PositiveInfinityBytes); + return new CanonicalLabel(name, PositiveInfinity, PositiveInfinity); var valueAsString = value.ToString("g", CultureInfo.InvariantCulture); var prometheusBytes = PrometheusConstants.ExportEncoding.GetBytes(valueAsString); From ea5222b0d48d4fd14d3bc9a1fc3917f73f45e0bc Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Sun, 3 Dec 2023 22:11:10 +0200 Subject: [PATCH 213/230] History file update --- History | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/History b/History index e78233d8..02aa6be1 100644 --- a/History +++ b/History @@ -1,5 +1,8 @@ * 8.2.0 -- Various optimizations to reduce spent CPU time and allocated memory. +- .WithLabels() & similar now accept ReadOnlyMemory as alternative to string[]. Same behavior, just easier to use if you already have a ReadOnlyMemory. +- .WithLabels() & similar now accept ReadOnlySpan as alternative to string[]. This enables allocation-free metric instance creation if a metric instance with these labels is already known. +- Incorporated various optimizations to reduce the required CPU time and allocated memory, including #410, #443 and other contributions. +- Observation of large histograms is now 10-30% faster on machines that support AVX2 instructions. * 8.1.1 - Fix bug in .NET Meters API adapter for UpDownCounter, which was incorrectly transformed to Prometheus metrics. #452 and #453 * 8.1.0 From 2e9d9265d431141dda7a25e1cffb89a23a8b6b0c Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Mon, 4 Dec 2023 10:10:39 +0200 Subject: [PATCH 214/230] StringSequence --- Benchmark.NetCore/LabelSequenceBenchmarks.cs | 17 +++++++++++++++++ Benchmark.NetCore/StringSequenceBenchmarks.cs | 16 ++++++++++++++++ Prometheus/StringSequence.cs | 18 ++++++++++-------- 3 files changed, 43 insertions(+), 8 deletions(-) create mode 100644 Benchmark.NetCore/LabelSequenceBenchmarks.cs create mode 100644 Benchmark.NetCore/StringSequenceBenchmarks.cs diff --git a/Benchmark.NetCore/LabelSequenceBenchmarks.cs b/Benchmark.NetCore/LabelSequenceBenchmarks.cs new file mode 100644 index 00000000..c879ed6c --- /dev/null +++ b/Benchmark.NetCore/LabelSequenceBenchmarks.cs @@ -0,0 +1,17 @@ +using BenchmarkDotNet.Attributes; +using Prometheus; + +namespace Benchmark.NetCore; + +[MemoryDiagnoser] +public class LabelSequenceBenchmarks +{ + private static readonly StringSequence Names3Array = StringSequence.From(new[] { "aaaaaaaaaaaaaaaaa", "bbbbbbbbbbbbbb", "cccccccccccccc" }); + private static readonly StringSequence Values3Array = StringSequence.From(new[] { "valueaaaaaaaaaaaaaaaaa", "valuebbbbbbbbbbbbbb", "valuecccccccccccccc" }); + + [Benchmark] + public void Create_From3Array() + { + LabelSequence.From(Names3Array, Values3Array); + } +} diff --git a/Benchmark.NetCore/StringSequenceBenchmarks.cs b/Benchmark.NetCore/StringSequenceBenchmarks.cs new file mode 100644 index 00000000..294b1b2d --- /dev/null +++ b/Benchmark.NetCore/StringSequenceBenchmarks.cs @@ -0,0 +1,16 @@ +using BenchmarkDotNet.Attributes; +using Prometheus; + +namespace Benchmark.NetCore; + +[MemoryDiagnoser] +public class StringSequenceBenchmarks +{ + private static readonly string[] Values3Array = ["aaaaaaaaaaaaaaaaa", "bbbbbbbbbbbbbb", "cccccccccccccc"]; + + [Benchmark] + public void Create_From3Array() + { + StringSequence.From(Values3Array); + } +} diff --git a/Prometheus/StringSequence.cs b/Prometheus/StringSequence.cs index 00066fe3..5ac874f0 100644 --- a/Prometheus/StringSequence.cs +++ b/Prometheus/StringSequence.cs @@ -18,7 +18,7 @@ namespace Prometheus; public Enumerator GetEnumerator() { - return new Enumerator(_values.Span, _inheritedValues ?? []); + return new Enumerator(_values.Span, _inheritedValueArrays ?? []); } public ref struct Enumerator @@ -152,7 +152,9 @@ private StringSequence(StringSequence inheritFrom, StringSequence thenFrom, Read } _values = andFinallyPrepend; - _inheritedValues = InheritFrom(inheritFrom, thenFrom); + + if (!inheritFrom.IsEmpty || !thenFrom.IsEmpty) + _inheritedValueArrays = InheritFrom(inheritFrom, thenFrom); Length = _values.Length + inheritFrom.Length + thenFrom.Length; @@ -167,7 +169,7 @@ public static StringSequence From(params string[] values) return new StringSequence(Empty, Empty, values); } - public static StringSequence From(ReadOnlyMemory values) + public static StringSequence From(in ReadOnlyMemory values) { if (values.Length == 0) return Empty; @@ -198,7 +200,7 @@ public StringSequence Concat(StringSequence concatenatedValues) // Inherited values from one or more parent instances. // It may be null because structs have a default ctor that zero-initializes them, so watch out. - private readonly ReadOnlyMemory[]? _inheritedValues; + private readonly ReadOnlyMemory[]? _inheritedValueArrays; private readonly int _hashCode; @@ -215,13 +217,13 @@ public StringSequence Concat(StringSequence concatenatedValues) if (!first.IsEmpty) { firstOwnArrayCount = first._values.Length > 0 ? 1 : 0; - firstInheritedArrayCount = first._inheritedValues?.Length ?? 0; + firstInheritedArrayCount = first._inheritedValueArrays?.Length ?? 0; } if (!second.IsEmpty) { secondOwnArrayCount = second._values.Length > 0 ? 1 : 0; - secondInheritedArrayCount = second._inheritedValues?.Length ?? 0; + secondInheritedArrayCount = second._inheritedValueArrays?.Length ?? 0; } var totalSegmentCount = firstOwnArrayCount + firstInheritedArrayCount + secondOwnArrayCount + secondInheritedArrayCount; @@ -240,7 +242,7 @@ public StringSequence Concat(StringSequence concatenatedValues) if (secondInheritedArrayCount != 0) { - Array.Copy(second._inheritedValues!, 0, result, targetIndex, secondInheritedArrayCount); + Array.Copy(second._inheritedValueArrays!, 0, result, targetIndex, secondInheritedArrayCount); targetIndex += secondInheritedArrayCount; } @@ -251,7 +253,7 @@ public StringSequence Concat(StringSequence concatenatedValues) if (firstInheritedArrayCount != 0) { - Array.Copy(first._inheritedValues!, 0, result, targetIndex, firstInheritedArrayCount); + Array.Copy(first._inheritedValueArrays!, 0, result, targetIndex, firstInheritedArrayCount); targetIndex += firstInheritedArrayCount; } From d32045b1a3813959900297c3783751a95d9c7ed3 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Mon, 4 Dec 2023 10:16:30 +0200 Subject: [PATCH 215/230] "in" --- Prometheus/StringSequence.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Prometheus/StringSequence.cs b/Prometheus/StringSequence.cs index 5ac874f0..7af5a093 100644 --- a/Prometheus/StringSequence.cs +++ b/Prometheus/StringSequence.cs @@ -119,7 +119,7 @@ public override bool Equals(object? obj) // There are various ways we can make a StringSequence, comining one or two parents and maybe adding some extra to the start. // This ctor tries to account for all these options. - private StringSequence(StringSequence inheritFrom, StringSequence thenFrom, ReadOnlyMemory andFinallyPrepend) + private StringSequence(in StringSequence inheritFrom, in StringSequence thenFrom, in ReadOnlyMemory andFinallyPrepend) { // Simplify construction if we just need to match one of the cloneable inputs. if (!inheritFrom.IsEmpty && thenFrom.IsEmpty && andFinallyPrepend.Length == 0) From d92cce106bf3e3085cad779650d159248689aad3 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Mon, 4 Dec 2023 10:45:56 +0200 Subject: [PATCH 216/230] Fix broken logic in MetricExpirationBenchmarks --- Benchmark.NetCore/Benchmark.NetCore.csproj | 1 + .../MetricExpirationBenchmarks.cs | 54 +++++++++++++----- Prometheus/ManagedLifetimeMetricHandle.cs | 56 +++++++------------ 3 files changed, 59 insertions(+), 52 deletions(-) diff --git a/Benchmark.NetCore/Benchmark.NetCore.csproj b/Benchmark.NetCore/Benchmark.NetCore.csproj index 00958443..183b1bd4 100644 --- a/Benchmark.NetCore/Benchmark.NetCore.csproj +++ b/Benchmark.NetCore/Benchmark.NetCore.csproj @@ -21,6 +21,7 @@ + diff --git a/Benchmark.NetCore/MetricExpirationBenchmarks.cs b/Benchmark.NetCore/MetricExpirationBenchmarks.cs index accb2f45..4bac6ccd 100644 --- a/Benchmark.NetCore/MetricExpirationBenchmarks.cs +++ b/Benchmark.NetCore/MetricExpirationBenchmarks.cs @@ -1,5 +1,6 @@ using BenchmarkDotNet.Attributes; using Prometheus; +using Prometheus.Tests; namespace Benchmark.NetCore; @@ -8,8 +9,8 @@ namespace Benchmark.NetCore; /// [MemoryDiagnoser] // This seems to need a lot of warmup to stabilize. -[WarmupCount(50)] -//[EventPipeProfiler(BenchmarkDotNet.Diagnosers.EventPipeProfile.CpuSampling)] +[WarmupCount(80)] +//[EventPipeProfiler(BenchmarkDotNet.Diagnosers.EventPipeProfile.GcVerbose)] public class MetricExpirationBenchmarks { /// @@ -53,6 +54,8 @@ static MetricExpirationBenchmarks() // We use the same strings both for the names and the values. private static readonly string[] _labels = ["foo", "bar", "baz"]; + private BreakableDelayer _delayer; + [IterationSetup] public void Setup() { @@ -63,6 +66,8 @@ public void Setup() var regularFactory = Metrics.WithCustomRegistry(_registry); + _delayer = new BreakableDelayer(); + // We create non-expiring versions of the metrics to pre-warm the metrics registry. // While not a realistic use case, it does help narrow down the benchmark workload to the actual part that is special about expiring metrics, // which means we get more useful results from this (rather than with some metric allocation overhead mixed in). @@ -75,7 +80,7 @@ public void Setup() // Both the usage and the lifetime manager allocation matter but we want to bring them out separately in the benchmarks. if (PreallocateLifetimeManager) { - var managedLifetimeCounter = _factory.CreateCounter(_metricNames[i], _help, _labels); + var managedLifetimeCounter = CreateCounter(_metricNames[i], _help, _labels); // And also take the first lease to pre-warm the lifetime manager. managedLifetimeCounter.AcquireLease(out _, _labels).Dispose(); @@ -86,12 +91,31 @@ public void Setup() [IterationCleanup] public void Cleanup() { + // Ensure that all metrics are marked as expired, so the expiration processing logic destroys them all. + // This causes some extra work during cleanup but on the other hand, it ensures good isolation between iterations, so fine. for (var i = 0; i < _metricCount; i++) { - var managedLifetimeCounter = (ManagedLifetimeMetricHandle)_factory.CreateCounter(_metricNames[i], _help, _labels); - // Ensure we do not slow down the next iteration by having the timer keep a bunch of references alive. - managedLifetimeCounter.CancelReaper(); + var counter = CreateCounter(_metricNames[i], _help, _labels); + counter.SetAllKeepaliveTimestampsToDistantPast(); } + + // Twice and with some sleep time, just for good measure. + // BenchmarkDotNet today does not support async here, so we do a sync sleep or two. + _delayer.BreakAllDelays(); + Thread.Sleep(millisecondsTimeout: 5); + _delayer.BreakAllDelays(); + Thread.Sleep(millisecondsTimeout: 5); + } + + private ManagedLifetimeMetricHandle CreateCounter(string name, string help, string[] labels) + { + var counter = (ManagedLifetimeMetricHandle)_factory.CreateCounter(name, help, labels); + + // We use a breakable delayer to ensure that we can control when the metric expiration logic runs, so one iteration + // of the benchmark does not start to interfere with another iteration just because some timers are left running. + counter.Delayer = _delayer; + + return counter; } [Benchmark] @@ -99,7 +123,7 @@ public void CreateAndUse_AutoLease() { for (var i = 0; i < _metricCount; i++) { - var metric = _factory.CreateCounter(_metricNames[i], _help, _labels).WithExtendLifetimeOnUse(); + var metric = CreateCounter(_metricNames[i], _help, _labels).WithExtendLifetimeOnUse(); for (var repeat = 0; repeat < RepeatCount; repeat++) metric.WithLabels(_labels).Inc(); @@ -112,7 +136,7 @@ public void CreateAndUse_AutoLease_WithDuplicates() for (var dupe = 0; dupe < _duplicateCount; dupe++) for (var i = 0; i < _metricCount; i++) { - var metric = _factory.CreateCounter(_metricNames[i], _help, _labels).WithExtendLifetimeOnUse(); + var metric = CreateCounter(_metricNames[i], _help, _labels).WithExtendLifetimeOnUse(); for (var repeat = 0; repeat < RepeatCount; repeat++) metric.WithLabels(_labels).Inc(); @@ -124,7 +148,7 @@ public void CreateAndUse_ManualLease() { for (var i = 0; i < _metricCount; i++) { - var counter = _factory.CreateCounter(_metricNames[i], _help, _labels); + var counter = CreateCounter(_metricNames[i], _help, _labels); for (var repeat = 0; repeat < RepeatCount; repeat++) { @@ -140,7 +164,7 @@ public void CreateAndUse_ManualLease_WithDuplicates() for (var dupe = 0; dupe < _duplicateCount; dupe++) for (var i = 0; i < _metricCount; i++) { - var counter = _factory.CreateCounter(_metricNames[i], _help, _labels); + var counter = CreateCounter(_metricNames[i], _help, _labels); for (var repeat = 0; repeat < RepeatCount; repeat++) { @@ -150,12 +174,12 @@ public void CreateAndUse_ManualLease_WithDuplicates() } } - [Benchmark] + //[Benchmark] public void CreateAndUse_ManualRefLease() { for (var i = 0; i < _metricCount; i++) { - var counter = _factory.CreateCounter(_metricNames[i], _help, _labels); + var counter = CreateCounter(_metricNames[i], _help, _labels); for (var repeat = 0; repeat < RepeatCount; repeat++) { @@ -171,7 +195,7 @@ public void CreateAndUse_ManualRefLease_WithDuplicates() for (var dupe = 0; dupe < _duplicateCount; dupe++) for (var i = 0; i < _metricCount; i++) { - var counter = _factory.CreateCounter(_metricNames[i], _help, _labels); + var counter = CreateCounter(_metricNames[i], _help, _labels); for (var repeat = 0; repeat < RepeatCount; repeat++) { @@ -194,7 +218,7 @@ public void CreateAndUse_WithLease() for (var i = 0; i < _metricCount; i++) { - var counter = _factory.CreateCounter(_metricNames[i], _help, _labels); + var counter = CreateCounter(_metricNames[i], _help, _labels); for (var repeat = 0; repeat < RepeatCount; repeat++) { @@ -212,7 +236,7 @@ public void CreateAndUse_WithLease_WithDuplicates() for (var dupe = 0; dupe < _duplicateCount; dupe++) for (var i = 0; i < _metricCount; i++) { - var counter = _factory.CreateCounter(_metricNames[i], _help, _labels); + var counter = CreateCounter(_metricNames[i], _help, _labels); for (var repeat = 0; repeat < RepeatCount; repeat++) counter.WithLease(incrementCounterAction, _labels); diff --git a/Prometheus/ManagedLifetimeMetricHandle.cs b/Prometheus/ManagedLifetimeMetricHandle.cs index 174abdba..e7c6b244 100644 --- a/Prometheus/ManagedLifetimeMetricHandle.cs +++ b/Prometheus/ManagedLifetimeMetricHandle.cs @@ -21,8 +21,6 @@ internal ManagedLifetimeMetricHandle(Collector metric, TimeSpan expiresA _metric = metric; _expiresAfter = expiresAfter; - - _reaperCt = _reaperCts.Token; } protected readonly Collector _metric; @@ -347,11 +345,6 @@ private sealed class Lease(ManagedLifetimeMetricHandle private const int ReaperActive = 1; private const int ReaperInactive = 0; - // Only used for testing - in normal usage the reaper only stops when all metric instances have expired. - // That is a potential area for future improvement, as in rare situations this may result in a temporary waste of resources. - private readonly CancellationTokenSource _reaperCts = new(); - private readonly CancellationToken _reaperCt; - /// /// Call this immediately after creating a metric instance that will eventually expire. /// @@ -366,15 +359,6 @@ private void EnsureReaperActive() _ = Task.Run(_reaperFunc); } - /// - /// For testing only. Stops the reaper if it is active, allowing resources to be released after a test - /// without keeping things in memory just because the reaper task is still waiting for a delay of many minutes to elapse. - /// - internal void CancelReaper() - { - _reaperCts.Cancel(); - } - private async Task Reaper() { while (true) @@ -453,34 +437,32 @@ private async Task Reaper() _lifetimesLock.ExitWriteLock(); } } + } + finally + { + ArrayPool.Shared.Return(expiredInstancesBuffer); + } - // Check if we need to shut down the reaper or keep going. - _lifetimesLock.EnterReadLock(); - - try - { - if (_lifetimes.Count == 0) - { - CleanupReaper(); - return; - } - } - finally - { - _lifetimesLock.ExitReadLock(); - } + // Check if we need to shut down the reaper or keep going. + _lifetimesLock.EnterReadLock(); - // Work done! Go sleep a bit and come back when something may have expired. - // We do not need to be too aggressive here, as expiration is not a hard schedule guarantee. - await Delayer.Delay(_expiresAfter, _reaperCt); - } - catch (OperationCanceledException) when (_reaperCt.IsCancellationRequested) + try { + if (_lifetimes.Count != 0) + goto has_more_work; } finally { - ArrayPool.Shared.Return(expiredInstancesBuffer); + _lifetimesLock.ExitReadLock(); } + + CleanupReaper(); + return; + + has_more_work: + // Work done! Go sleep a bit and come back when something may have expired. + // We do not need to be too aggressive here, as expiration is not a hard schedule guarantee. + await Delayer.Delay(_expiresAfter); } } From 4538e37f406a70fb8d1ce3dcc933a3cfd33a8e3f Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Mon, 4 Dec 2023 11:10:29 +0200 Subject: [PATCH 217/230] Sample app tidy --- Sample.Console.DotNetMeters/CustomDotNetMeters.cs | 1 + Sample.Console.DotNetMeters/Program.cs | 5 +++-- Sample.Console/Program.cs | 1 + Sample.Grpc/Program.cs | 1 + 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Sample.Console.DotNetMeters/CustomDotNetMeters.cs b/Sample.Console.DotNetMeters/CustomDotNetMeters.cs index ed620a18..9ec01552 100644 --- a/Sample.Console.DotNetMeters/CustomDotNetMeters.cs +++ b/Sample.Console.DotNetMeters/CustomDotNetMeters.cs @@ -75,6 +75,7 @@ int MeasureSandLevel() highCardinalityCounter1.Add(Random.Shared.Next(10_000_000), new KeyValuePair("connection-id", connection)); // Maybe some connection went away, maybe some was added. + // Timeseries that stop receiving updates will disappear from prometheus-net output after a short delay (up to 10 minutes by default). if (Random.Shared.Next(100) == 0) { activeConnections.RemoveAt(Random.Shared.Next(activeConnections.Count)); diff --git a/Sample.Console.DotNetMeters/Program.cs b/Sample.Console.DotNetMeters/Program.cs index 67ed74e6..1137ca6c 100644 --- a/Sample.Console.DotNetMeters/Program.cs +++ b/Sample.Console.DotNetMeters/Program.cs @@ -9,7 +9,8 @@ Metrics.SuppressDefaultMetrics(new SuppressDefaultMetricOptions { SuppressProcessMetrics = true, - SuppressEventCounters = true + SuppressEventCounters = true, + SuppressDebugMetrics = true }); // Example of static labels that conflict with .NET Meters API labels ("Bytes considered" histogram). @@ -23,7 +24,7 @@ using var server = new KestrelMetricServer(port: 1234); server.Start(); -// Start publishing sample data via .NET Meters API. Data from this API is published by default by prometheus-net. +// Start publishing sample data via .NET Meters API. All data from the .NET Meters API is published by default. CustomDotNetMeters.PublishSampleData(); // Metrics published in this sample: diff --git a/Sample.Console/Program.cs b/Sample.Console/Program.cs index 1ea76f0a..cf30871a 100644 --- a/Sample.Console/Program.cs +++ b/Sample.Console/Program.cs @@ -27,6 +27,7 @@ // * built-in process metrics giving basic information about the .NET runtime (enabled by default) // * metrics from .NET Event Counters (enabled by default, updated every 10 seconds) // * metrics from .NET Meters (enabled by default) +// * prometheus-net self-inspection metrics that indicate number of registered metrics/timeseries (enabled by default) // * the custom sample counter defined above Console.WriteLine("Open http://localhost:1234/metrics in a web browser."); Console.WriteLine("Press enter to exit."); diff --git a/Sample.Grpc/Program.cs b/Sample.Grpc/Program.cs index 7a2b94c1..d78de82f 100644 --- a/Sample.Grpc/Program.cs +++ b/Sample.Grpc/Program.cs @@ -30,6 +30,7 @@ // * built-in process metrics giving basic information about the .NET runtime (enabled by default) // * metrics from .NET Event Counters (enabled by default, updated every 10 seconds) // * metrics from .NET Meters (enabled by default) + // * prometheus-net self-inspection metrics that indicate number of registered metrics/timeseries (enabled by default) // * metrics about HTTP requests handled by the web app (configured above) // * metrics about gRPC requests handled by the web app (configured above) app.MapMetrics(); From 3393f76e6ec0ccf04b55634489116ff2ffb1bf3b Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Mon, 4 Dec 2023 11:34:38 +0200 Subject: [PATCH 218/230] Update benchmark results in readme --- Docs/MeasurementsBenchmarks.xlsx | Bin 10331 -> 10088 bytes README.md | 42 +++++++++++-------------------- 2 files changed, 14 insertions(+), 28 deletions(-) diff --git a/Docs/MeasurementsBenchmarks.xlsx b/Docs/MeasurementsBenchmarks.xlsx index aeefba7a901bd6ed7ae65b0b8789acb8095c3d75..52647c125dbd5f92a04702a8765715d436eacd45 100644 GIT binary patch delta 2800 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 YxQ_%50&B`fY&2N_ts=)|qwqWVADHU~CjbBd delta 3052 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 diff --git a/README.md b/README.md index ea324dcb..ae823ea2 100644 --- a/README.md +++ b/README.md @@ -875,37 +875,23 @@ A suite of benchmarks is included if you wish to explore the performance charact As an example of the performance of measuring data using prometheus-net, we have the results of the MeasurementBenchmarks here, converted into measurements per second: -| Metric type | Concurrency | Measurements per second | -|-------------|------------:|------------------------:| -| Gauge | 1 thread | 519 million | -| Counter | 1 thread | 134 million | -| Histogram | 1 thread | 69 million | -| Summary | 1 thread | 2 million | -| Gauge | 16 threads | 53 million | -| Counter | 16 threads | 9 million | -| Histogram | 16 threads | 5 million | -| Summary | 16 threads | 2 million | - -> **Note** -> All measurements on all threads are recorded by the same metric instance, for maximum stress and concurrent load. In real-world apps with the load spread across multiple metrics, you can expect even better performance. +| Metric type | Measurements per second | +|-------------------------|------------------------:| +| Counter | 259 million | +| Gauge | 593 million | +| Histogram (16 buckets) | 106 million | +| Histogram (128 buckets) | 58 million | Another popular .NET SDK with Prometheus support is the OpenTelemetry SDK. To help you choose, we have [SdkComparisonBenchmarks.cs](Benchmark.NetCore/SdkComparisonBenchmarks.cs) to compare the two SDKs and give some idea of how they differer in the performance tradeoffs made. Both SDKs are evaluated in single-threaded mode under a comparable workload and enabled feature set. A representative result is here: -```text -BenchmarkDotNet v0.13.10, Windows 11 (10.0.22621.2715/22H2/2022Update/SunValley2) -AMD Ryzen 9 5950X, 1 CPU, 32 logical and 16 physical cores - [Host] : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 - - -| Method | Mean | Gen0 | Gen1 | Allocated | -|------------------------------ |------------:|--------:|--------:|----------:| -| PromNetCounter | 237.1 us | - | - | - | -| PromNetHistogram | 1,236.2 us | - | - | 2 B | -| OTelCounter | 10,981.5 us | - | - | 11 B | -| OTelHistogram | 12,078.9 us | - | - | 24 B | -| PromNetHistogramForAdHocLabel | 1,877.7 us | 50.7813 | 48.8281 | 872701 B | -| OTelHistogramForAdHocLabel | 354.0 us | 5.3711 | - | 96000 B | -``` +| SDK | Benchmark scenario | CPU time spent | Memory allocated | +|----------------|---------------------------------------------------|---------------:|-----------------:| +| prometheus-net | Counter measurement (existing timeseries) x100K | 233 µs | None | +| OpenTelemetry | Counter measurement (existing timeseries) x100K | 10770 µs | None | +| prometheus-net | Histogram measurement (existing timeseries) x100K | 958 µs | None | +| OpenTelemetry | Histogram measurement (existing timeseries) x100K | 11997 µs | None | +| prometheus-net | Histogram measurement (new timeseries) x1K | 992 µs | 664 KB | +| OpenTelemetry | Histogram measurement (new timeseries) x1K | 386 µs | 96 KB | # Community projects From 885f52cb9b16fc7f9e7d8c17e2f8fec8129f4e81 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Mon, 4 Dec 2023 11:36:30 +0200 Subject: [PATCH 219/230] Smaller table in readme for better rendering --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index ae823ea2..da9b6d07 100644 --- a/README.md +++ b/README.md @@ -884,14 +884,14 @@ As an example of the performance of measuring data using prometheus-net, we have Another popular .NET SDK with Prometheus support is the OpenTelemetry SDK. To help you choose, we have [SdkComparisonBenchmarks.cs](Benchmark.NetCore/SdkComparisonBenchmarks.cs) to compare the two SDKs and give some idea of how they differer in the performance tradeoffs made. Both SDKs are evaluated in single-threaded mode under a comparable workload and enabled feature set. A representative result is here: -| SDK | Benchmark scenario | CPU time spent | Memory allocated | -|----------------|---------------------------------------------------|---------------:|-----------------:| -| prometheus-net | Counter measurement (existing timeseries) x100K | 233 µs | None | -| OpenTelemetry | Counter measurement (existing timeseries) x100K | 10770 µs | None | -| prometheus-net | Histogram measurement (existing timeseries) x100K | 958 µs | None | -| OpenTelemetry | Histogram measurement (existing timeseries) x100K | 11997 µs | None | -| prometheus-net | Histogram measurement (new timeseries) x1K | 992 µs | 664 KB | -| OpenTelemetry | Histogram measurement (new timeseries) x1K | 386 µs | 96 KB | +| SDK | Benchmark scenario | CPU time | Memory | +|----------------|---------------------------------------|---------:|-------:| +| prometheus-net | Counter (existing timeseries) x100K | 233 µs | None | +| OpenTelemetry | Counter (existing timeseries) x100K | 10770 µs | None | +| prometheus-net | Histogram (existing timeseries) x100K | 958 µs | None | +| OpenTelemetry | Histogram (existing timeseries) x100K | 11997 µs | None | +| prometheus-net | Histogram (new timeseries) x1K | 992 µs | 664 KB | +| OpenTelemetry | Histogram (new timeseries) x1K | 386 µs | 96 KB | # Community projects From 97e72067be8ece1794cc7c91b1cef2331e2a84c7 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Mon, 4 Dec 2023 16:20:37 +0200 Subject: [PATCH 220/230] More benchmarks are fine-tuning for StringSequence --- Benchmark.NetCore/LabelSequenceBenchmarks.cs | 4 +- Benchmark.NetCore/StringSequenceBenchmarks.cs | 48 +++++++++++++++++++ Prometheus/StringSequence.cs | 38 ++++----------- 3 files changed, 59 insertions(+), 31 deletions(-) diff --git a/Benchmark.NetCore/LabelSequenceBenchmarks.cs b/Benchmark.NetCore/LabelSequenceBenchmarks.cs index c879ed6c..5affa893 100644 --- a/Benchmark.NetCore/LabelSequenceBenchmarks.cs +++ b/Benchmark.NetCore/LabelSequenceBenchmarks.cs @@ -12,6 +12,8 @@ public class LabelSequenceBenchmarks [Benchmark] public void Create_From3Array() { - LabelSequence.From(Names3Array, Values3Array); + // This is too fast for the benchmark engine, so let's create some additional work by looping through it many times. + for (var i = 0; i < 10_000; i++) + LabelSequence.From(Names3Array, Values3Array); } } diff --git a/Benchmark.NetCore/StringSequenceBenchmarks.cs b/Benchmark.NetCore/StringSequenceBenchmarks.cs index 294b1b2d..0a052efe 100644 --- a/Benchmark.NetCore/StringSequenceBenchmarks.cs +++ b/Benchmark.NetCore/StringSequenceBenchmarks.cs @@ -7,10 +7,58 @@ namespace Benchmark.NetCore; public class StringSequenceBenchmarks { private static readonly string[] Values3Array = ["aaaaaaaaaaaaaaaaa", "bbbbbbbbbbbbbb", "cccccccccccccc"]; + private static readonly ReadOnlyMemory Values3Memory = new(Values3Array); + + private static readonly string[] Values3ArrayPart1 = ["aaaaaaaaaaaaaaaaa"]; + private static readonly string[] Values3ArrayPart2 = ["bbbbbbbbbbbbbb"]; + private static readonly string[] Values3ArrayPart3 = ["cccccccccccccc"]; [Benchmark] public void Create_From3Array() { StringSequence.From(Values3Array); } + + [Benchmark] + public void Create_From3Memory() + { + StringSequence.From(Values3Memory); + } + + [Benchmark] + public void Create_From3ArrayConcat() + { + var part1 = StringSequence.From(Values3ArrayPart1); + var part2 = StringSequence.From(Values3ArrayPart2); + var part3 = StringSequence.From(Values3ArrayPart3); + + part1.Concat(part2).Concat(part3); + } + + private static readonly StringSequence FromValues3 = StringSequence.From(Values3Array); + private static readonly StringSequence Other = StringSequence.From(new[] { "fooooooooooooooo", "baaaaaaaaaaaaar", "baaaaaaaaaaz" }); + + [Benchmark] + public void Contains_Positive() + { + FromValues3.Contains(Values3Array[2]); + } + + [Benchmark] + public void Contains_Negative() + { + FromValues3.Contains("a string that is not in there"); + } + + [Benchmark] + public void Equals_Positive() + { + FromValues3.Equals(FromValues3); + } + + [Benchmark] + public void Equals_Negative() + { + FromValues3.Equals(Other); + } } diff --git a/Prometheus/StringSequence.cs b/Prometheus/StringSequence.cs index 7af5a093..e68f3fee 100644 --- a/Prometheus/StringSequence.cs +++ b/Prometheus/StringSequence.cs @@ -16,10 +16,7 @@ namespace Prometheus; { public static readonly StringSequence Empty = new(); - public Enumerator GetEnumerator() - { - return new Enumerator(_values.Span, _inheritedValueArrays ?? []); - } + public Enumerator GetEnumerator() => new(_values.Span, _inheritedValueArrays ?? []); public ref struct Enumerator { @@ -119,24 +116,8 @@ public override bool Equals(object? obj) // There are various ways we can make a StringSequence, comining one or two parents and maybe adding some extra to the start. // This ctor tries to account for all these options. - private StringSequence(in StringSequence inheritFrom, in StringSequence thenFrom, in ReadOnlyMemory andFinallyPrepend) + private StringSequence(StringSequence inheritFrom, StringSequence thenFrom, in ReadOnlyMemory andFinallyPrepend) { - // Simplify construction if we just need to match one of the cloneable inputs. - if (!inheritFrom.IsEmpty && thenFrom.IsEmpty && andFinallyPrepend.Length == 0) - { - this = inheritFrom; - return; - } - else if (!thenFrom.IsEmpty && inheritFrom.IsEmpty && andFinallyPrepend.Length == 0) - { - this = thenFrom; - return; - } - - // Simplify construction if we have nothing at all. - if (inheritFrom.IsEmpty && thenFrom.IsEmpty && andFinallyPrepend.Length == 0) - return; - // Anything inherited is already validated. Perform a sanity check on anything new. if (andFinallyPrepend.Length != 0) { @@ -144,14 +125,12 @@ private StringSequence(in StringSequence inheritFrom, in StringSequence thenFrom for (var i = 0; i < span.Length; i++) { - var ownValue = span[i]; - - if (ownValue == null) + if (span[i] == null) throw new NotSupportedException("Null values are not supported for metric label names and values."); } - } - _values = andFinallyPrepend; + _values = andFinallyPrepend; + } if (!inheritFrom.IsEmpty || !thenFrom.IsEmpty) _inheritedValueArrays = InheritFrom(inheritFrom, thenFrom); @@ -169,7 +148,7 @@ public static StringSequence From(params string[] values) return new StringSequence(Empty, Empty, values); } - public static StringSequence From(in ReadOnlyMemory values) + public static StringSequence From(ReadOnlyMemory values) { if (values.Length == 0) return Empty; @@ -205,7 +184,7 @@ public StringSequence Concat(StringSequence concatenatedValues) private readonly int _hashCode; // We can inherit from one or two parent sequences. Order is "first at the end, second prefixed to it" as is typical (ancestors at the end). - private static ReadOnlyMemory[]? InheritFrom(StringSequence first, StringSequence second) + private static ReadOnlyMemory[] InheritFrom(StringSequence first, StringSequence second) { // Expected output: second._values, second._inheritedValues, first._values, first._inheritedValues @@ -229,7 +208,7 @@ public StringSequence Concat(StringSequence concatenatedValues) var totalSegmentCount = firstOwnArrayCount + firstInheritedArrayCount + secondOwnArrayCount + secondInheritedArrayCount; if (totalSegmentCount == 0) - return null; + throw new Exception("Unreachable code reached: InheritFrom() should not even be called if there is nothing to inherit."); var result = new ReadOnlyMemory[totalSegmentCount]; @@ -254,7 +233,6 @@ public StringSequence Concat(StringSequence concatenatedValues) if (firstInheritedArrayCount != 0) { Array.Copy(first._inheritedValueArrays!, 0, result, targetIndex, firstInheritedArrayCount); - targetIndex += firstInheritedArrayCount; } return result; From a5955f1de09cd43a16d1a9773cf08e380d8246d3 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Mon, 4 Dec 2023 17:59:48 +0200 Subject: [PATCH 221/230] Code tidy --- .../GrpcMetricsMiddlewareExtensions.cs | 51 +- .../GrpcMetricsOptionsBase.cs | 19 +- .../GrpcMiddlewareExporterOptions.cs | 9 +- .../GrpcRequestCountMiddleware.cs | 45 +- .../GrpcRequestCountOptions.cs | 15 +- .../GrpcRequestLabelNames.cs | 27 +- .../GrpcRequestMiddlewareBase.cs | 149 +++-- .../HealthCheckBuilderExtensions.cs | 13 +- .../PrometheusHealthCheckPublisher.cs | 57 +- .../PrometheusHealthCheckPublisherOptions.cs | 14 +- .../HttpMetrics/CaptureRouteDataMiddleware.cs | 63 +- .../HttpMetrics/CapturedRouteDataFeature.cs | 9 +- .../HttpMetrics/HttpCustomLabel.cs | 31 +- .../HttpMetrics/HttpInProgressOptions.cs | 15 +- .../HttpMetrics/HttpRequestCountOptions.cs | 15 +- .../HttpMetrics/HttpRequestDurationOptions.cs | 15 +- .../HttpMetrics/HttpRequestLabelNames.cs | 93 ++- .../HttpMetrics/HttpRequestMiddlewareBase.cs | 539 +++++++++--------- .../HttpMetrics/HttpRouteParameterMapping.cs | 59 +- .../HttpMetrics/ICapturedRouteDataFeature.cs | 9 +- .../KestrelMetricServerExtensions.cs | 57 +- .../MetricServerMiddlewareExtensions.cs | 219 ++++--- Prometheus/GaugeConfiguration.cs | 9 +- Prometheus/HistogramConfiguration.cs | 27 +- .../HttpClientDelegatingHandlerBase.cs | 169 +++--- .../HttpClientExporterOptions.cs | 15 +- .../HttpClientMetrics/HttpClientIdentity.cs | 17 +- .../HttpClientInProgressHandler.cs | 35 +- .../HttpClientInProgressOptions.cs | 15 +- .../HttpClientMetricsOptionsBase.cs | 19 +- .../HttpClientRequestCountHandler.cs | 45 +- .../HttpClientRequestCountOptions.cs | 15 +- .../HttpClientRequestDurationHandler.cs | 63 +- .../HttpClientRequestDurationOptions.cs | 15 +- .../HttpClientRequestLabelNames.cs | 51 +- .../HttpClientResponseDurationHandler.cs | 191 +++---- .../HttpClientResponseDurationOptions.cs | 15 +- Prometheus/HttpClientMetricsExtensions.cs | 141 +++-- Prometheus/ICollector.cs | 37 +- Prometheus/ICollectorChild.cs | 13 +- Prometheus/ICollectorRegistry.cs | 25 +- Prometheus/IDelayer.cs | 17 +- Prometheus/IGauge.cs | 19 +- Prometheus/IMetricFactory.cs | 81 ++- Prometheus/IMetricServer.cs | 39 +- Prometheus/IObserver.cs | 17 +- Prometheus/ISummary.cs | 7 +- Prometheus/ITimer.cs | 21 +- Prometheus/MetricPusher.cs | 7 +- Prometheus/MetricPusherOptions.cs | 53 +- Prometheus/MetricServer.cs | 2 +- Prometheus/MetricType.cs | 15 +- Prometheus/Metrics.cs | 310 +++++----- Prometheus/PrometheusNameHelpers.cs | 81 ++- Prometheus/PushStreamContentInternal.cs | 119 ++-- Prometheus/QuantileEpsilonPair.cs | 17 +- Prometheus/RealDelayer.cs | 23 +- Prometheus/SummaryConfiguration.cs | 51 +- Prometheus/SuppressDefaultMetricOptions.cs | 117 ++-- Prometheus/ThreadSafeLong.cs | 4 +- Prometheus/TimerExtensions.cs | 101 ++-- Prometheus/TimestampHelpers.cs | 41 +- Prometheus/ValueStopwatch.cs | 46 +- 63 files changed, 1778 insertions(+), 1850 deletions(-) diff --git a/Prometheus.AspNetCore.Grpc/GrpcMetricsMiddlewareExtensions.cs b/Prometheus.AspNetCore.Grpc/GrpcMetricsMiddlewareExtensions.cs index d2309b90..e9cbcfea 100644 --- a/Prometheus.AspNetCore.Grpc/GrpcMetricsMiddlewareExtensions.cs +++ b/Prometheus.AspNetCore.Grpc/GrpcMetricsMiddlewareExtensions.cs @@ -1,35 +1,34 @@ using Microsoft.AspNetCore.Builder; -namespace Prometheus +namespace Prometheus; + +public static class GrpcMetricsMiddlewareExtensions { - public static class GrpcMetricsMiddlewareExtensions + /// + /// Configures the ASP.NET Core request pipeline to collect Prometheus metrics on processed gRPC requests. + /// + public static IApplicationBuilder UseGrpcMetrics(this IApplicationBuilder app, + Action configure) { - /// - /// Configures the ASP.NET Core request pipeline to collect Prometheus metrics on processed gRPC requests. - /// - public static IApplicationBuilder UseGrpcMetrics(this IApplicationBuilder app, - Action configure) - { - var options = new GrpcMiddlewareExporterOptions(); - configure?.Invoke(options); - app.UseGrpcMetrics(options); - return app; - } - - /// - /// Configures the ASP.NET Core request pipeline to collect Prometheus metrics on processed gRPC requests. - /// - public static IApplicationBuilder UseGrpcMetrics(this IApplicationBuilder app, - GrpcMiddlewareExporterOptions? options = null) - { - options ??= new GrpcMiddlewareExporterOptions(); + var options = new GrpcMiddlewareExporterOptions(); + configure?.Invoke(options); + app.UseGrpcMetrics(options); + return app; + } - if (options.RequestCount.Enabled) - { - app.UseMiddleware(options.RequestCount); - } + /// + /// Configures the ASP.NET Core request pipeline to collect Prometheus metrics on processed gRPC requests. + /// + public static IApplicationBuilder UseGrpcMetrics(this IApplicationBuilder app, + GrpcMiddlewareExporterOptions? options = null) + { + options ??= new GrpcMiddlewareExporterOptions(); - return app; + if (options.RequestCount.Enabled) + { + app.UseMiddleware(options.RequestCount); } + + return app; } } diff --git a/Prometheus.AspNetCore.Grpc/GrpcMetricsOptionsBase.cs b/Prometheus.AspNetCore.Grpc/GrpcMetricsOptionsBase.cs index 63f6056f..034478ea 100644 --- a/Prometheus.AspNetCore.Grpc/GrpcMetricsOptionsBase.cs +++ b/Prometheus.AspNetCore.Grpc/GrpcMetricsOptionsBase.cs @@ -1,13 +1,12 @@ -namespace Prometheus +namespace Prometheus; + +public abstract class GrpcMetricsOptionsBase { - public abstract class GrpcMetricsOptionsBase - { - public bool Enabled { get; set; } = true; + public bool Enabled { get; set; } = true; - /// - /// Allows you to override the registry used to create the default metric instance. - /// Value is ignored if you specify a custom metric instance in the options. - /// - public CollectorRegistry? Registry { get; set; } - } + /// + /// Allows you to override the registry used to create the default metric instance. + /// Value is ignored if you specify a custom metric instance in the options. + /// + public CollectorRegistry? Registry { get; set; } } diff --git a/Prometheus.AspNetCore.Grpc/GrpcMiddlewareExporterOptions.cs b/Prometheus.AspNetCore.Grpc/GrpcMiddlewareExporterOptions.cs index f7d65578..8254fc19 100644 --- a/Prometheus.AspNetCore.Grpc/GrpcMiddlewareExporterOptions.cs +++ b/Prometheus.AspNetCore.Grpc/GrpcMiddlewareExporterOptions.cs @@ -1,7 +1,6 @@ -namespace Prometheus +namespace Prometheus; + +public sealed class GrpcMiddlewareExporterOptions { - public sealed class GrpcMiddlewareExporterOptions - { - public GrpcRequestCountOptions RequestCount { get; set; } = new GrpcRequestCountOptions(); - } + public GrpcRequestCountOptions RequestCount { get; set; } = new GrpcRequestCountOptions(); } diff --git a/Prometheus.AspNetCore.Grpc/GrpcRequestCountMiddleware.cs b/Prometheus.AspNetCore.Grpc/GrpcRequestCountMiddleware.cs index 2a154699..9eba9ec1 100644 --- a/Prometheus.AspNetCore.Grpc/GrpcRequestCountMiddleware.cs +++ b/Prometheus.AspNetCore.Grpc/GrpcRequestCountMiddleware.cs @@ -1,32 +1,31 @@ using Microsoft.AspNetCore.Http; -namespace Prometheus +namespace Prometheus; + +/// +/// Counts the number of requests to gRPC services. +/// +internal sealed class GrpcRequestCountMiddleware : GrpcRequestMiddlewareBase, ICounter> { - /// - /// Counts the number of requests to gRPC services. - /// - internal sealed class GrpcRequestCountMiddleware : GrpcRequestMiddlewareBase, ICounter> - { - private readonly RequestDelegate _next; + private readonly RequestDelegate _next; - public GrpcRequestCountMiddleware(RequestDelegate next, GrpcRequestCountOptions? options) - : base(options, options?.Counter) - { - _next = next ?? throw new ArgumentNullException(nameof(next)); - } + public GrpcRequestCountMiddleware(RequestDelegate next, GrpcRequestCountOptions? options) + : base(options, options?.Counter) + { + _next = next ?? throw new ArgumentNullException(nameof(next)); + } - public async Task Invoke(HttpContext context) - { - CreateChild(context)?.Inc(); + public async Task Invoke(HttpContext context) + { + CreateChild(context)?.Inc(); - await _next(context); - } + await _next(context); + } - protected override string[] DefaultLabels => GrpcRequestLabelNames.All; + protected override string[] DefaultLabels => GrpcRequestLabelNames.All; - protected override ICollector CreateMetricInstance(string[] labelNames) => MetricFactory.CreateCounter( - "grpc_requests_received_total", - "Number of gRPC requests received (including those currently being processed).", - labelNames); - } + protected override ICollector CreateMetricInstance(string[] labelNames) => MetricFactory.CreateCounter( + "grpc_requests_received_total", + "Number of gRPC requests received (including those currently being processed).", + labelNames); } \ No newline at end of file diff --git a/Prometheus.AspNetCore.Grpc/GrpcRequestCountOptions.cs b/Prometheus.AspNetCore.Grpc/GrpcRequestCountOptions.cs index d0fcc291..dd986ef2 100644 --- a/Prometheus.AspNetCore.Grpc/GrpcRequestCountOptions.cs +++ b/Prometheus.AspNetCore.Grpc/GrpcRequestCountOptions.cs @@ -1,10 +1,9 @@ -namespace Prometheus +namespace Prometheus; + +public sealed class GrpcRequestCountOptions : GrpcMetricsOptionsBase { - public sealed class GrpcRequestCountOptions : GrpcMetricsOptionsBase - { - /// - /// Set this to use a custom metric instead of the default. - /// - public ICollector? Counter { get; set; } - } + /// + /// Set this to use a custom metric instead of the default. + /// + public ICollector? Counter { get; set; } } diff --git a/Prometheus.AspNetCore.Grpc/GrpcRequestLabelNames.cs b/Prometheus.AspNetCore.Grpc/GrpcRequestLabelNames.cs index ac17708e..95a79886 100644 --- a/Prometheus.AspNetCore.Grpc/GrpcRequestLabelNames.cs +++ b/Prometheus.AspNetCore.Grpc/GrpcRequestLabelNames.cs @@ -1,17 +1,16 @@ -namespace Prometheus +namespace Prometheus; + +/// +/// Reserved label names used in gRPC metrics. +/// +public static class GrpcRequestLabelNames { - /// - /// Reserved label names used in gRPC metrics. - /// - public static class GrpcRequestLabelNames - { - public const string Service = "service"; - public const string Method = "method"; + public const string Service = "service"; + public const string Method = "method"; - public static readonly string[] All = - { - Service, - Method, - }; - } + public static readonly string[] All = + { + Service, + Method, + }; } diff --git a/Prometheus.AspNetCore.Grpc/GrpcRequestMiddlewareBase.cs b/Prometheus.AspNetCore.Grpc/GrpcRequestMiddlewareBase.cs index ceae81a0..8f6c80d2 100644 --- a/Prometheus.AspNetCore.Grpc/GrpcRequestMiddlewareBase.cs +++ b/Prometheus.AspNetCore.Grpc/GrpcRequestMiddlewareBase.cs @@ -1,99 +1,98 @@ using Grpc.AspNetCore.Server; using Microsoft.AspNetCore.Http; -namespace Prometheus +namespace Prometheus; + +// Modeled after HttpRequestMiddlewareBase, just with gRPC specific functionality. +internal abstract class GrpcRequestMiddlewareBase + where TCollector : class, ICollector + where TChild : class, ICollectorChild { - // Modeled after HttpRequestMiddlewareBase, just with gRPC specific functionality. - internal abstract class GrpcRequestMiddlewareBase - where TCollector : class, ICollector - where TChild : class, ICollectorChild - { - /// - /// The set of labels from among the defaults that this metric supports. - /// - /// This set will be automatically extended with labels for additional - /// route parameters when creating the default metric instance. - /// - protected abstract string[] DefaultLabels { get; } + /// + /// The set of labels from among the defaults that this metric supports. + /// + /// This set will be automatically extended with labels for additional + /// route parameters when creating the default metric instance. + /// + protected abstract string[] DefaultLabels { get; } - /// - /// Creates the default metric instance with the specified set of labels. - /// Only used if the caller does not provide a custom metric instance in the options. - /// - protected abstract TCollector CreateMetricInstance(string[] labelNames); + /// + /// Creates the default metric instance with the specified set of labels. + /// Only used if the caller does not provide a custom metric instance in the options. + /// + protected abstract TCollector CreateMetricInstance(string[] labelNames); - /// - /// The factory to use for creating the default metric for this middleware. - /// Not used if a custom metric is alreaedy provided in options. - /// - protected MetricFactory MetricFactory { get; } + /// + /// The factory to use for creating the default metric for this middleware. + /// Not used if a custom metric is alreaedy provided in options. + /// + protected MetricFactory MetricFactory { get; } - private readonly TCollector _metric; + private readonly TCollector _metric; + + protected GrpcRequestMiddlewareBase(GrpcMetricsOptionsBase? options, TCollector? customMetric) + { + MetricFactory = Metrics.WithCustomRegistry(options?.Registry ?? Metrics.DefaultRegistry); - protected GrpcRequestMiddlewareBase(GrpcMetricsOptionsBase? options, TCollector? customMetric) + if (customMetric != null) { - MetricFactory = Metrics.WithCustomRegistry(options?.Registry ?? Metrics.DefaultRegistry); + _metric = customMetric; + ValidateNoUnexpectedLabelNames(); + } + else + { + _metric = CreateMetricInstance(DefaultLabels); + } + } - if (customMetric != null) - { - _metric = customMetric; - ValidateNoUnexpectedLabelNames(); - } - else - { - _metric = CreateMetricInstance(DefaultLabels); - } + protected TChild? CreateChild(HttpContext context) + { + var metadata = context.GetEndpoint()?.Metadata?.GetMetadata(); + if (metadata == null) + { + // Not a gRPC request + return null; } - protected TChild? CreateChild(HttpContext context) + if (!_metric.LabelNames.Any()) { - var metadata = context.GetEndpoint()?.Metadata?.GetMetadata(); - if (metadata == null) - { - // Not a gRPC request - return null; - } + return _metric.Unlabelled; + } - if (!_metric.LabelNames.Any()) - { - return _metric.Unlabelled; - } + return CreateChild(context, metadata); + } - return CreateChild(context, metadata); - } + protected TChild CreateChild(HttpContext context, GrpcMethodMetadata metadata) + { + var labelValues = new string[_metric.LabelNames.Length]; - protected TChild CreateChild(HttpContext context, GrpcMethodMetadata metadata) + for (var i = 0; i < labelValues.Length; i++) { - var labelValues = new string[_metric.LabelNames.Length]; - - for (var i = 0; i < labelValues.Length; i++) + switch (_metric.LabelNames[i]) { - switch (_metric.LabelNames[i]) - { - case GrpcRequestLabelNames.Service: - labelValues[i] = metadata.Method.ServiceName; - break; - case GrpcRequestLabelNames.Method: - labelValues[i] = metadata.Method.Name; - break; - default: - // Should never reach this point because we validate in ctor. - throw new NotSupportedException($"Unexpected label name on {_metric.Name}: {_metric.LabelNames[i]}"); - } + case GrpcRequestLabelNames.Service: + labelValues[i] = metadata.Method.ServiceName; + break; + case GrpcRequestLabelNames.Method: + labelValues[i] = metadata.Method.Name; + break; + default: + // Should never reach this point because we validate in ctor. + throw new NotSupportedException($"Unexpected label name on {_metric.Name}: {_metric.LabelNames[i]}"); } - - return _metric.WithLabels(labelValues); } - /// - /// If we use a custom metric, it should not have labels that are neither defaults nor additional route parameters. - /// - private void ValidateNoUnexpectedLabelNames() - { - var unexpected = _metric.LabelNames.Except(DefaultLabels); + return _metric.WithLabels(labelValues); + } - if (unexpected.Any()) - throw new ArgumentException($"Provided custom gRPC request metric instance for {GetType().Name} has some unexpected labels: {string.Join(", ", unexpected)}."); - } + /// + /// If we use a custom metric, it should not have labels that are neither defaults nor additional route parameters. + /// + private void ValidateNoUnexpectedLabelNames() + { + var unexpected = _metric.LabelNames.Except(DefaultLabels); + + if (unexpected.Any()) + throw new ArgumentException($"Provided custom gRPC request metric instance for {GetType().Name} has some unexpected labels: {string.Join(", ", unexpected)}."); } } \ No newline at end of file diff --git a/Prometheus.AspNetCore.HealthChecks/HealthCheckBuilderExtensions.cs b/Prometheus.AspNetCore.HealthChecks/HealthCheckBuilderExtensions.cs index c8afbf92..187fa7aa 100644 --- a/Prometheus.AspNetCore.HealthChecks/HealthCheckBuilderExtensions.cs +++ b/Prometheus.AspNetCore.HealthChecks/HealthCheckBuilderExtensions.cs @@ -1,15 +1,14 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; -namespace Prometheus +namespace Prometheus; + +public static class HealthCheckBuilderExtensions { - public static class HealthCheckBuilderExtensions + public static IHealthChecksBuilder ForwardToPrometheus(this IHealthChecksBuilder builder, PrometheusHealthCheckPublisherOptions? options = null) { - public static IHealthChecksBuilder ForwardToPrometheus(this IHealthChecksBuilder builder, PrometheusHealthCheckPublisherOptions? options = null) - { - builder.Services.AddSingleton(provider => new PrometheusHealthCheckPublisher(options)); + builder.Services.AddSingleton(provider => new PrometheusHealthCheckPublisher(options)); - return builder; - } + return builder; } } diff --git a/Prometheus.AspNetCore.HealthChecks/PrometheusHealthCheckPublisher.cs b/Prometheus.AspNetCore.HealthChecks/PrometheusHealthCheckPublisher.cs index 609c6483..04d87c46 100644 --- a/Prometheus.AspNetCore.HealthChecks/PrometheusHealthCheckPublisher.cs +++ b/Prometheus.AspNetCore.HealthChecks/PrometheusHealthCheckPublisher.cs @@ -1,40 +1,39 @@ using Microsoft.Extensions.Diagnostics.HealthChecks; -namespace Prometheus +namespace Prometheus; + +/// +/// Publishes ASP.NET Core Health Check statuses as Prometheus metrics. +/// +internal sealed class PrometheusHealthCheckPublisher : IHealthCheckPublisher { - /// - /// Publishes ASP.NET Core Health Check statuses as Prometheus metrics. - /// - internal sealed class PrometheusHealthCheckPublisher : IHealthCheckPublisher - { - private readonly Gauge _checkStatus; + private readonly Gauge _checkStatus; - public PrometheusHealthCheckPublisher(PrometheusHealthCheckPublisherOptions? options) - { - _checkStatus = options?.Gauge ?? new PrometheusHealthCheckPublisherOptions().Gauge; - } + public PrometheusHealthCheckPublisher(PrometheusHealthCheckPublisherOptions? options) + { + _checkStatus = options?.Gauge ?? new PrometheusHealthCheckPublisherOptions().Gauge; + } - public Task PublishAsync(HealthReport report, CancellationToken cancellationToken) - { - foreach (var reportEntry in report.Entries) - _checkStatus.WithLabels(reportEntry.Key).Set(HealthStatusToMetricValue(reportEntry.Value.Status)); + public Task PublishAsync(HealthReport report, CancellationToken cancellationToken) + { + foreach (var reportEntry in report.Entries) + _checkStatus.WithLabels(reportEntry.Key).Set(HealthStatusToMetricValue(reportEntry.Value.Status)); - return Task.CompletedTask; - } + return Task.CompletedTask; + } - private static double HealthStatusToMetricValue(HealthStatus status) + private static double HealthStatusToMetricValue(HealthStatus status) + { + switch (status) { - switch (status) - { - case HealthStatus.Unhealthy: - return 0; - case HealthStatus.Degraded: - return 0.5; - case HealthStatus.Healthy: - return 1; - default: - throw new NotSupportedException($"Unexpected HealthStatus value: {status}"); - } + case HealthStatus.Unhealthy: + return 0; + case HealthStatus.Degraded: + return 0.5; + case HealthStatus.Healthy: + return 1; + default: + throw new NotSupportedException($"Unexpected HealthStatus value: {status}"); } } } diff --git a/Prometheus.AspNetCore.HealthChecks/PrometheusHealthCheckPublisherOptions.cs b/Prometheus.AspNetCore.HealthChecks/PrometheusHealthCheckPublisherOptions.cs index 2157bfe2..0f640ee4 100644 --- a/Prometheus.AspNetCore.HealthChecks/PrometheusHealthCheckPublisherOptions.cs +++ b/Prometheus.AspNetCore.HealthChecks/PrometheusHealthCheckPublisherOptions.cs @@ -1,11 +1,9 @@ -namespace Prometheus +namespace Prometheus; + +public sealed class PrometheusHealthCheckPublisherOptions { - public sealed class PrometheusHealthCheckPublisherOptions - { - private const string DefaultName = "aspnetcore_healthcheck_status"; - private const string DefaultHelp = "ASP.NET Core health check status (0 == Unhealthy, 0.5 == Degraded, 1 == Healthy)"; + private const string DefaultName = "aspnetcore_healthcheck_status"; + private const string DefaultHelp = "ASP.NET Core health check status (0 == Unhealthy, 0.5 == Degraded, 1 == Healthy)"; - public Gauge Gauge { get; set; } = - Metrics.CreateGauge(DefaultName, DefaultHelp, labelNames: new[] { "name" }); - } + public Gauge Gauge { get; set; } = Metrics.CreateGauge(DefaultName, DefaultHelp, labelNames: ["name"]); } diff --git a/Prometheus.AspNetCore/HttpMetrics/CaptureRouteDataMiddleware.cs b/Prometheus.AspNetCore/HttpMetrics/CaptureRouteDataMiddleware.cs index 04dcecb3..af2416ff 100644 --- a/Prometheus.AspNetCore/HttpMetrics/CaptureRouteDataMiddleware.cs +++ b/Prometheus.AspNetCore/HttpMetrics/CaptureRouteDataMiddleware.cs @@ -1,46 +1,45 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; -namespace Prometheus.HttpMetrics +namespace Prometheus.HttpMetrics; + +/// +/// If routing data is available before executing the inner handler, this routing data is captured +/// and can be used later by other middlewares that wish not to be affected by runtime changes to routing data. +/// +/// +/// This is intended to be executed after the .UseRouting() middleware that performs ASP.NET Core 3 endpoint routing. +/// +/// The captured route data is stored in the context via ICapturedRouteDataFeature. +/// +internal sealed class CaptureRouteDataMiddleware { - /// - /// If routing data is available before executing the inner handler, this routing data is captured - /// and can be used later by other middlewares that wish not to be affected by runtime changes to routing data. - /// - /// - /// This is intended to be executed after the .UseRouting() middleware that performs ASP.NET Core 3 endpoint routing. - /// - /// The captured route data is stored in the context via ICapturedRouteDataFeature. - /// - internal sealed class CaptureRouteDataMiddleware - { - private readonly RequestDelegate _next; + private readonly RequestDelegate _next; - public CaptureRouteDataMiddleware(RequestDelegate next) - { - _next = next; - } + public CaptureRouteDataMiddleware(RequestDelegate next) + { + _next = next; + } - public Task Invoke(HttpContext context) - { - TryCaptureRouteData(context); + public Task Invoke(HttpContext context) + { + TryCaptureRouteData(context); - return _next(context); - } + return _next(context); + } - private static void TryCaptureRouteData(HttpContext context) - { - var routeData = context.GetRouteData(); + private static void TryCaptureRouteData(HttpContext context) + { + var routeData = context.GetRouteData(); - if (routeData == null || routeData.Values.Count <= 0) - return; + if (routeData == null || routeData.Values.Count <= 0) + return; - var capturedRouteData = new CapturedRouteDataFeature(); + var capturedRouteData = new CapturedRouteDataFeature(); - foreach (var pair in routeData.Values) - capturedRouteData.Values.Add(pair.Key, pair.Value); + foreach (var pair in routeData.Values) + capturedRouteData.Values.Add(pair.Key, pair.Value); - context.Features.Set(capturedRouteData); - } + context.Features.Set(capturedRouteData); } } diff --git a/Prometheus.AspNetCore/HttpMetrics/CapturedRouteDataFeature.cs b/Prometheus.AspNetCore/HttpMetrics/CapturedRouteDataFeature.cs index 847ff518..f2562e2b 100644 --- a/Prometheus.AspNetCore/HttpMetrics/CapturedRouteDataFeature.cs +++ b/Prometheus.AspNetCore/HttpMetrics/CapturedRouteDataFeature.cs @@ -1,9 +1,8 @@ using Microsoft.AspNetCore.Routing; -namespace Prometheus.HttpMetrics +namespace Prometheus.HttpMetrics; + +sealed class CapturedRouteDataFeature : ICapturedRouteDataFeature { - sealed class CapturedRouteDataFeature : ICapturedRouteDataFeature - { - public RouteValueDictionary Values { get; } = new RouteValueDictionary(); - } + public RouteValueDictionary Values { get; } = new RouteValueDictionary(); } diff --git a/Prometheus.AspNetCore/HttpMetrics/HttpCustomLabel.cs b/Prometheus.AspNetCore/HttpMetrics/HttpCustomLabel.cs index cedd3809..830781c9 100644 --- a/Prometheus.AspNetCore/HttpMetrics/HttpCustomLabel.cs +++ b/Prometheus.AspNetCore/HttpMetrics/HttpCustomLabel.cs @@ -1,23 +1,22 @@ using Microsoft.AspNetCore.Http; -namespace Prometheus.HttpMetrics +namespace Prometheus.HttpMetrics; + +public sealed class HttpCustomLabel { - public sealed class HttpCustomLabel - { - /// - /// Name of the Prometheus label. - /// - public string LabelName { get; } + /// + /// Name of the Prometheus label. + /// + public string LabelName { get; } - /// - /// A method that extracts the label value from the HttpContext of the request being handled. - /// - public Func LabelValueProvider { get; } + /// + /// A method that extracts the label value from the HttpContext of the request being handled. + /// + public Func LabelValueProvider { get; } - public HttpCustomLabel(string labelName, Func labelValueProvider) - { - LabelName = labelName; - LabelValueProvider = labelValueProvider; - } + public HttpCustomLabel(string labelName, Func labelValueProvider) + { + LabelName = labelName; + LabelValueProvider = labelValueProvider; } } diff --git a/Prometheus.AspNetCore/HttpMetrics/HttpInProgressOptions.cs b/Prometheus.AspNetCore/HttpMetrics/HttpInProgressOptions.cs index 7e3d30b2..df09ea3d 100644 --- a/Prometheus.AspNetCore/HttpMetrics/HttpInProgressOptions.cs +++ b/Prometheus.AspNetCore/HttpMetrics/HttpInProgressOptions.cs @@ -1,10 +1,9 @@ -namespace Prometheus.HttpMetrics +namespace Prometheus.HttpMetrics; + +public sealed class HttpInProgressOptions : HttpMetricsOptionsBase { - public sealed class HttpInProgressOptions : HttpMetricsOptionsBase - { - /// - /// Set this to use a custom metric instead of the default. - /// - public ICollector? Gauge { get; set; } - } + /// + /// Set this to use a custom metric instead of the default. + /// + public ICollector? Gauge { get; set; } } \ No newline at end of file diff --git a/Prometheus.AspNetCore/HttpMetrics/HttpRequestCountOptions.cs b/Prometheus.AspNetCore/HttpMetrics/HttpRequestCountOptions.cs index d395cea2..e09e0d98 100644 --- a/Prometheus.AspNetCore/HttpMetrics/HttpRequestCountOptions.cs +++ b/Prometheus.AspNetCore/HttpMetrics/HttpRequestCountOptions.cs @@ -1,10 +1,9 @@ -namespace Prometheus.HttpMetrics +namespace Prometheus.HttpMetrics; + +public sealed class HttpRequestCountOptions : HttpMetricsOptionsBase { - public sealed class HttpRequestCountOptions : HttpMetricsOptionsBase - { - /// - /// Set this to use a custom metric instead of the default. - /// - public ICollector? Counter { get; set; } - } + /// + /// Set this to use a custom metric instead of the default. + /// + public ICollector? Counter { get; set; } } \ No newline at end of file diff --git a/Prometheus.AspNetCore/HttpMetrics/HttpRequestDurationOptions.cs b/Prometheus.AspNetCore/HttpMetrics/HttpRequestDurationOptions.cs index ccdd1272..ddda75aa 100644 --- a/Prometheus.AspNetCore/HttpMetrics/HttpRequestDurationOptions.cs +++ b/Prometheus.AspNetCore/HttpMetrics/HttpRequestDurationOptions.cs @@ -1,10 +1,9 @@ -namespace Prometheus.HttpMetrics +namespace Prometheus.HttpMetrics; + +public sealed class HttpRequestDurationOptions : HttpMetricsOptionsBase { - public sealed class HttpRequestDurationOptions : HttpMetricsOptionsBase - { - /// - /// Set this to use a custom metric instead of the default. - /// - public ICollector? Histogram { get; set; } - } + /// + /// Set this to use a custom metric instead of the default. + /// + public ICollector? Histogram { get; set; } } \ No newline at end of file diff --git a/Prometheus.AspNetCore/HttpMetrics/HttpRequestLabelNames.cs b/Prometheus.AspNetCore/HttpMetrics/HttpRequestLabelNames.cs index dfcc6009..d2fbcfdd 100644 --- a/Prometheus.AspNetCore/HttpMetrics/HttpRequestLabelNames.cs +++ b/Prometheus.AspNetCore/HttpMetrics/HttpRequestLabelNames.cs @@ -1,55 +1,54 @@ -namespace Prometheus.HttpMetrics +namespace Prometheus.HttpMetrics; + +/// +/// Label names used by the HTTP request handler metrics system. +/// +public static class HttpRequestLabelNames { - /// - /// Label names used by the HTTP request handler metrics system. - /// - public static class HttpRequestLabelNames - { - public const string Code = "code"; - public const string Method = "method"; - public const string Controller = "controller"; - public const string Action = "action"; + public const string Code = "code"; + public const string Method = "method"; + public const string Controller = "controller"; + public const string Action = "action"; - // Not reserved for background-compatibility, as it used to be optional and user-supplied. - // Conditionally, it may also be automatically added to metrics. - public const string Page = "page"; + // Not reserved for background-compatibility, as it used to be optional and user-supplied. + // Conditionally, it may also be automatically added to metrics. + public const string Page = "page"; - // Not reserved for background-compatibility, as it used to be optional and user-supplied. - public const string Endpoint = "endpoint"; + // Not reserved for background-compatibility, as it used to be optional and user-supplied. + public const string Endpoint = "endpoint"; - // All labels that are supported by prometheus-net default logic. - // Useful if you want to define a custom metric that extends the default logic, without hardcoding the built-in label names. - public static readonly string[] All = - { - Code, - Method, - Controller, - Action, - Page, - Endpoint - }; + // All labels that are supported by prometheus-net default logic. + // Useful if you want to define a custom metric that extends the default logic, without hardcoding the built-in label names. + public static readonly string[] All = + { + Code, + Method, + Controller, + Action, + Page, + Endpoint + }; - // These are reserved and may only be used with the default logic. - internal static readonly string[] Default = - { - Code, - Method, - Controller, - Action - }; + // These are reserved and may only be used with the default logic. + internal static readonly string[] Default = + { + Code, + Method, + Controller, + Action + }; - internal static readonly string[] DefaultsAvailableBeforeExecutingFinalHandler = - { - Method, - Controller, - Action - }; + internal static readonly string[] DefaultsAvailableBeforeExecutingFinalHandler = + { + Method, + Controller, + Action + }; - // Labels that do not need routing information to be collected. - internal static readonly string[] NonRouteSpecific = - { - Code, - Method - }; - } + // Labels that do not need routing information to be collected. + internal static readonly string[] NonRouteSpecific = + { + Code, + Method + }; } \ No newline at end of file diff --git a/Prometheus.AspNetCore/HttpMetrics/HttpRequestMiddlewareBase.cs b/Prometheus.AspNetCore/HttpMetrics/HttpRequestMiddlewareBase.cs index dc15f45e..0a5cea70 100644 --- a/Prometheus.AspNetCore/HttpMetrics/HttpRequestMiddlewareBase.cs +++ b/Prometheus.AspNetCore/HttpMetrics/HttpRequestMiddlewareBase.cs @@ -2,329 +2,328 @@ using Microsoft.AspNetCore.Routing; using System.Globalization; -namespace Prometheus.HttpMetrics +namespace Prometheus.HttpMetrics; + +/// +/// This base class performs the data management necessary to associate the correct labels and values +/// with HTTP request metrics, depending on the options the user has provided for the HTTP metric middleware. +/// +/// The following labels are supported: +/// 'code' (HTTP status code) +/// 'method' (HTTP request method) +/// 'controller' (The Controller used to fulfill the HTTP request) +/// 'action' (The Action used to fulfill the HTTP request) +/// Any other label - from one of: +/// * HTTP route parameter (if name/mapping specified in options; name need not match). +/// * custom logic (callback decides value for each request) +/// +/// The 'code' and 'method' data are taken from the current HTTP context. +/// 'controller', 'action' and route parameter labels will be taken from the request routing information. +/// +/// If a custom metric is provided in the options, it must not be missing any labels for explicitly defined +/// custom route parameters. However, it is permitted to lack any of the default labels (code/method/...). +/// +internal abstract class HttpRequestMiddlewareBase + where TCollector : class, ICollector + where TChild : class, ICollectorChild { /// - /// This base class performs the data management necessary to associate the correct labels and values - /// with HTTP request metrics, depending on the options the user has provided for the HTTP metric middleware. + /// The set of labels from among the defaults that this metric supports. /// - /// The following labels are supported: - /// 'code' (HTTP status code) - /// 'method' (HTTP request method) - /// 'controller' (The Controller used to fulfill the HTTP request) - /// 'action' (The Action used to fulfill the HTTP request) - /// Any other label - from one of: - /// * HTTP route parameter (if name/mapping specified in options; name need not match). - /// * custom logic (callback decides value for each request) + /// This set will be automatically extended with labels for additional + /// route parameters and custom labels when creating the default metric instance. /// - /// The 'code' and 'method' data are taken from the current HTTP context. - /// 'controller', 'action' and route parameter labels will be taken from the request routing information. - /// - /// If a custom metric is provided in the options, it must not be missing any labels for explicitly defined - /// custom route parameters. However, it is permitted to lack any of the default labels (code/method/...). + /// It will also be extended by additional built-in logic (page, endpoint). /// - internal abstract class HttpRequestMiddlewareBase - where TCollector : class, ICollector - where TChild : class, ICollectorChild - { - /// - /// The set of labels from among the defaults that this metric supports. - /// - /// This set will be automatically extended with labels for additional - /// route parameters and custom labels when creating the default metric instance. - /// - /// It will also be extended by additional built-in logic (page, endpoint). - /// - protected abstract string[] BaselineLabels { get; } - - /// - /// Creates the default metric instance with the specified set of labels. - /// Only used if the caller does not provide a custom metric instance in the options. - /// - protected abstract TCollector CreateMetricInstance(string[] labelNames); - - /// - /// The factory to use for creating the default metric for this middleware. - /// Not used if a custom metric is already provided in options. - /// - protected IMetricFactory MetricFactory { get; } - - private readonly List _additionalRouteParameters; - private readonly List _customLabels; - private readonly TCollector _metric; - - // For labels that are route parameter mappings. - private readonly Dictionary _labelToRouteParameterMap; - - // For labels that use a custom value provider. - private readonly Dictionary> _labelToValueProviderMap; - - private readonly bool _labelsRequireRouteData; - private readonly bool _reduceStatusCodeCardinality; - - protected HttpRequestMiddlewareBase(HttpMetricsOptionsBase options, TCollector? customMetric) - { - MetricFactory = options.MetricFactory ?? Metrics.WithCustomRegistry(options.Registry ?? Metrics.DefaultRegistry); + protected abstract string[] BaselineLabels { get; } - _additionalRouteParameters = options.AdditionalRouteParameters ?? new List(0); - _customLabels = options.CustomLabels ?? new List(0); + /// + /// Creates the default metric instance with the specified set of labels. + /// Only used if the caller does not provide a custom metric instance in the options. + /// + protected abstract TCollector CreateMetricInstance(string[] labelNames); - if (options.IncludePageLabelInDefaultsInternal) - AddPageLabelIfNoConflict(customMetric); + /// + /// The factory to use for creating the default metric for this middleware. + /// Not used if a custom metric is already provided in options. + /// + protected IMetricFactory MetricFactory { get; } - AddEndpointLabelIfNoConflict(customMetric); + private readonly List _additionalRouteParameters; + private readonly List _customLabels; + private readonly TCollector _metric; - ValidateMappings(); - _labelToRouteParameterMap = CreateLabelToRouteParameterMap(); - _reduceStatusCodeCardinality = options?.ReduceStatusCodeCardinality ?? false; - _labelToValueProviderMap = CreateLabelToValueProviderMap(); + // For labels that are route parameter mappings. + private readonly Dictionary _labelToRouteParameterMap; - if (customMetric != null) - { - _metric = customMetric; + // For labels that use a custom value provider. + private readonly Dictionary> _labelToValueProviderMap; - ValidateNoUnexpectedLabelNames(); - ValidateAdditionalRouteParametersPresentInMetricLabelNames(); - ValidateCustomLabelsPresentInMetricLabelNames(); - } - else - { - _metric = CreateMetricInstance(CreateDefaultLabelSet()); - } + private readonly bool _labelsRequireRouteData; + private readonly bool _reduceStatusCodeCardinality; - _labelsRequireRouteData = _metric.LabelNames.Except(HttpRequestLabelNames.NonRouteSpecific).Any(); - } + protected HttpRequestMiddlewareBase(HttpMetricsOptionsBase options, TCollector? customMetric) + { + MetricFactory = options.MetricFactory ?? Metrics.WithCustomRegistry(options.Registry ?? Metrics.DefaultRegistry); - private void AddPageLabelIfNoConflict(TCollector? customMetric) - { - // We were asked to add the "page" label because Razor Pages was detected. - // We will only do this if nothing else has already occupied the "page" label. - // If a custom metric is used, we also skip this if it has no "page" label name defined. - // - // The possible conflicts are: - // * an existing route parameter mapping (which works out the same as our logic, so fine) - // * custom logic that defines a "page" label (in which case we allow it to win, for backward compatibility). + _additionalRouteParameters = options.AdditionalRouteParameters ?? new List(0); + _customLabels = options.CustomLabels ?? new List(0); - if (_additionalRouteParameters.Any(x => x.LabelName == HttpRequestLabelNames.Page)) - return; + if (options.IncludePageLabelInDefaultsInternal) + AddPageLabelIfNoConflict(customMetric); - if (_customLabels.Any(x => x.LabelName == HttpRequestLabelNames.Page)) - return; + AddEndpointLabelIfNoConflict(customMetric); - if (customMetric != null && !customMetric.LabelNames.Contains(HttpRequestLabelNames.Page)) - return; + ValidateMappings(); + _labelToRouteParameterMap = CreateLabelToRouteParameterMap(); + _reduceStatusCodeCardinality = options?.ReduceStatusCodeCardinality ?? false; + _labelToValueProviderMap = CreateLabelToValueProviderMap(); - // If we got so far, we are good - all preconditions for adding "page" label exist. - _additionalRouteParameters.Add(new HttpRouteParameterMapping("page")); - } + if (customMetric != null) + { + _metric = customMetric; - private void AddEndpointLabelIfNoConflict(TCollector? customMetric) + ValidateNoUnexpectedLabelNames(); + ValidateAdditionalRouteParametersPresentInMetricLabelNames(); + ValidateCustomLabelsPresentInMetricLabelNames(); + } + else { - // We always try to add an "endpoint" label with the endpoint routing route pattern. - // We will only do this if nothing else has already occupied the "endpoint" label. - // If a custom metric is used, we also skip this if it has no "endpoint" label name defined. - // - // The possible conflicts are: - // * an existing route parameter mapping - // * custom logic that defines an "endpoint" label - // - // In case of conflict, we let the user-defined item win. - - if (_additionalRouteParameters.Any(x => x.LabelName == HttpRequestLabelNames.Endpoint)) - return; - - if (_customLabels.Any(x => x.LabelName == HttpRequestLabelNames.Endpoint)) - return; - - if (customMetric != null && !customMetric.LabelNames.Contains(HttpRequestLabelNames.Endpoint)) - return; - - _customLabels.Add(new HttpCustomLabel(HttpRequestLabelNames.Endpoint, context => - { - var endpoint = context.GetEndpoint() as RouteEndpoint; - return endpoint?.RoutePattern.RawText ?? ""; - })); + _metric = CreateMetricInstance(CreateDefaultLabelSet()); } - /// - /// Creates the metric child instance to use for measurements. - /// - /// - /// Internal for testing purposes. - /// - protected internal TChild CreateChild(HttpContext context) - { - if (!_metric.LabelNames.Any()) - return _metric.Unlabelled; + _labelsRequireRouteData = _metric.LabelNames.Except(HttpRequestLabelNames.NonRouteSpecific).Any(); + } - if (!_labelsRequireRouteData) - return CreateChild(context, null); + private void AddPageLabelIfNoConflict(TCollector? customMetric) + { + // We were asked to add the "page" label because Razor Pages was detected. + // We will only do this if nothing else has already occupied the "page" label. + // If a custom metric is used, we also skip this if it has no "page" label name defined. + // + // The possible conflicts are: + // * an existing route parameter mapping (which works out the same as our logic, so fine) + // * custom logic that defines a "page" label (in which case we allow it to win, for backward compatibility). - var routeData = context.Features.Get()?.Values; + if (_additionalRouteParameters.Any(x => x.LabelName == HttpRequestLabelNames.Page)) + return; - // If we have captured route data, we always prefer it. - // Otherwise, we extract new route data right now. - if (routeData == null) - routeData = context.GetRouteData()?.Values; + if (_customLabels.Any(x => x.LabelName == HttpRequestLabelNames.Page)) + return; - return CreateChild(context, routeData); - } + if (customMetric != null && !customMetric.LabelNames.Contains(HttpRequestLabelNames.Page)) + return; + + // If we got so far, we are good - all preconditions for adding "page" label exist. + _additionalRouteParameters.Add(new HttpRouteParameterMapping("page")); + } - protected TChild CreateChild(HttpContext context, RouteValueDictionary? routeData) + private void AddEndpointLabelIfNoConflict(TCollector? customMetric) + { + // We always try to add an "endpoint" label with the endpoint routing route pattern. + // We will only do this if nothing else has already occupied the "endpoint" label. + // If a custom metric is used, we also skip this if it has no "endpoint" label name defined. + // + // The possible conflicts are: + // * an existing route parameter mapping + // * custom logic that defines an "endpoint" label + // + // In case of conflict, we let the user-defined item win. + + if (_additionalRouteParameters.Any(x => x.LabelName == HttpRequestLabelNames.Endpoint)) + return; + + if (_customLabels.Any(x => x.LabelName == HttpRequestLabelNames.Endpoint)) + return; + + if (customMetric != null && !customMetric.LabelNames.Contains(HttpRequestLabelNames.Endpoint)) + return; + + _customLabels.Add(new HttpCustomLabel(HttpRequestLabelNames.Endpoint, context => { - var labelValues = new string[_metric.LabelNames.Length]; + var endpoint = context.GetEndpoint() as RouteEndpoint; + return endpoint?.RoutePattern.RawText ?? ""; + })); + } - for (var i = 0; i < labelValues.Length; i++) - { - switch (_metric.LabelNames[i]) - { - case HttpRequestLabelNames.Method: - labelValues[i] = context.Request.Method; - break; - case HttpRequestLabelNames.Code: - labelValues[i] = _reduceStatusCodeCardinality ? Math.Floor(context.Response.StatusCode / 100.0).ToString("#xx") : context.Response.StatusCode.ToString(CultureInfo.InvariantCulture); - break; - default: - // If we get to this point it must be either: - if (_labelToRouteParameterMap.TryGetValue(_metric.LabelNames[i], out var parameterName)) - { - // A mapped route parameter. - labelValues[i] = routeData?[parameterName] as string ?? string.Empty; - } - else if (_labelToValueProviderMap.TryGetValue(_metric.LabelNames[i], out var valueProvider)) - { - // A custom label value provider. - labelValues[i] = valueProvider(context) ?? string.Empty; - } - else - { - // Something we do not have data for. - // This can happen if, for example, a custom metric inherits "all" the labels without reimplementing the "when do we add which label" - // logic that prometheus-net implements (which is an entirely reasonable design). So it might just add a "page" label when we have no - // page information. Instead of rejecting such custom metrics, we just leave the label value empty and carry on. - labelValues[i] = ""; - } - break; - } - } + /// + /// Creates the metric child instance to use for measurements. + /// + /// + /// Internal for testing purposes. + /// + protected internal TChild CreateChild(HttpContext context) + { + if (!_metric.LabelNames.Any()) + return _metric.Unlabelled; - return _metric.WithLabels(labelValues); - } + if (!_labelsRequireRouteData) + return CreateChild(context, null); + var routeData = context.Features.Get()?.Values; - /// - /// Creates the set of labels defined on the automatically created metric. - /// - private string[] CreateDefaultLabelSet() - { - return BaselineLabels - .Concat(_additionalRouteParameters.Select(x => x.LabelName)) - .Concat(_customLabels.Select(x => x.LabelName)) - .ToArray(); - } + // If we have captured route data, we always prefer it. + // Otherwise, we extract new route data right now. + if (routeData == null) + routeData = context.GetRouteData()?.Values; + + return CreateChild(context, routeData); + } - /// - /// Creates the full set of labels ALLOWED for the current metric. - /// This may be a greater set than the labels automatically added to the default metric. - /// - private string[] CreateAllowedLabelSet() + protected TChild CreateChild(HttpContext context, RouteValueDictionary? routeData) + { + var labelValues = new string[_metric.LabelNames.Length]; + + for (var i = 0; i < labelValues.Length; i++) { - return HttpRequestLabelNames.All - .Concat(_additionalRouteParameters.Select(x => x.LabelName)) - .Concat(_customLabels.Select(x => x.LabelName)) - .Distinct() // Some builtins may also exist in the customs, with customs overwriting. That's fine. - .ToArray(); + switch (_metric.LabelNames[i]) + { + case HttpRequestLabelNames.Method: + labelValues[i] = context.Request.Method; + break; + case HttpRequestLabelNames.Code: + labelValues[i] = _reduceStatusCodeCardinality ? Math.Floor(context.Response.StatusCode / 100.0).ToString("#xx") : context.Response.StatusCode.ToString(CultureInfo.InvariantCulture); + break; + default: + // If we get to this point it must be either: + if (_labelToRouteParameterMap.TryGetValue(_metric.LabelNames[i], out var parameterName)) + { + // A mapped route parameter. + labelValues[i] = routeData?[parameterName] as string ?? string.Empty; + } + else if (_labelToValueProviderMap.TryGetValue(_metric.LabelNames[i], out var valueProvider)) + { + // A custom label value provider. + labelValues[i] = valueProvider(context) ?? string.Empty; + } + else + { + // Something we do not have data for. + // This can happen if, for example, a custom metric inherits "all" the labels without reimplementing the "when do we add which label" + // logic that prometheus-net implements (which is an entirely reasonable design). So it might just add a "page" label when we have no + // page information. Instead of rejecting such custom metrics, we just leave the label value empty and carry on. + labelValues[i] = ""; + } + break; + } } - private void ValidateMappings() - { - var routeParameterLabelNames = _additionalRouteParameters.Select(x => x.LabelName).ToList(); + return _metric.WithLabels(labelValues); + } - if (routeParameterLabelNames.Distinct(StringComparer.InvariantCultureIgnoreCase).Count() != routeParameterLabelNames.Count) - throw new ArgumentException("The set of additional route parameters to track contains multiple entries with the same label name.", nameof(HttpMetricsOptionsBase.AdditionalRouteParameters)); - if (HttpRequestLabelNames.Default.Except(routeParameterLabelNames, StringComparer.InvariantCultureIgnoreCase).Count() != HttpRequestLabelNames.Default.Length) - throw new ArgumentException($"The set of additional route parameters to track contains an entry with a reserved label name. Reserved label names are: {string.Join(", ", HttpRequestLabelNames.Default)}"); + /// + /// Creates the set of labels defined on the automatically created metric. + /// + private string[] CreateDefaultLabelSet() + { + return BaselineLabels + .Concat(_additionalRouteParameters.Select(x => x.LabelName)) + .Concat(_customLabels.Select(x => x.LabelName)) + .ToArray(); + } - var customLabelNames = _customLabels.Select(x => x.LabelName).ToList(); + /// + /// Creates the full set of labels ALLOWED for the current metric. + /// This may be a greater set than the labels automatically added to the default metric. + /// + private string[] CreateAllowedLabelSet() + { + return HttpRequestLabelNames.All + .Concat(_additionalRouteParameters.Select(x => x.LabelName)) + .Concat(_customLabels.Select(x => x.LabelName)) + .Distinct() // Some builtins may also exist in the customs, with customs overwriting. That's fine. + .ToArray(); + } - if (customLabelNames.Distinct(StringComparer.InvariantCultureIgnoreCase).Count() != customLabelNames.Count) - throw new ArgumentException("The set of custom labels contains multiple entries with the same label name.", nameof(HttpMetricsOptionsBase.CustomLabels)); + private void ValidateMappings() + { + var routeParameterLabelNames = _additionalRouteParameters.Select(x => x.LabelName).ToList(); - if (HttpRequestLabelNames.Default.Except(customLabelNames, StringComparer.InvariantCultureIgnoreCase).Count() != HttpRequestLabelNames.Default.Length) - throw new ArgumentException($"The set of custom labels contains an entry with a reserved label name. Reserved label names are: {string.Join(", ", HttpRequestLabelNames.Default)}"); + if (routeParameterLabelNames.Distinct(StringComparer.InvariantCultureIgnoreCase).Count() != routeParameterLabelNames.Count) + throw new ArgumentException("The set of additional route parameters to track contains multiple entries with the same label name.", nameof(HttpMetricsOptionsBase.AdditionalRouteParameters)); - if (customLabelNames.Intersect(routeParameterLabelNames).Any()) - throw new ArgumentException("The set of custom labels and the set of additional route parameters contain conflicting label names.", nameof(HttpMetricsOptionsBase.CustomLabels)); - } + if (HttpRequestLabelNames.Default.Except(routeParameterLabelNames, StringComparer.InvariantCultureIgnoreCase).Count() != HttpRequestLabelNames.Default.Length) + throw new ArgumentException($"The set of additional route parameters to track contains an entry with a reserved label name. Reserved label names are: {string.Join(", ", HttpRequestLabelNames.Default)}"); - private Dictionary CreateLabelToRouteParameterMap() - { - var map = new Dictionary(_additionalRouteParameters.Count + 2); + var customLabelNames = _customLabels.Select(x => x.LabelName).ToList(); - // Defaults are hardcoded. - map["action"] = "action"; - map["controller"] = "controller"; + if (customLabelNames.Distinct(StringComparer.InvariantCultureIgnoreCase).Count() != customLabelNames.Count) + throw new ArgumentException("The set of custom labels contains multiple entries with the same label name.", nameof(HttpMetricsOptionsBase.CustomLabels)); - // Any additional ones are merged. - foreach (var entry in _additionalRouteParameters) - map[entry.LabelName] = entry.ParameterName; + if (HttpRequestLabelNames.Default.Except(customLabelNames, StringComparer.InvariantCultureIgnoreCase).Count() != HttpRequestLabelNames.Default.Length) + throw new ArgumentException($"The set of custom labels contains an entry with a reserved label name. Reserved label names are: {string.Join(", ", HttpRequestLabelNames.Default)}"); - return map; - } + if (customLabelNames.Intersect(routeParameterLabelNames).Any()) + throw new ArgumentException("The set of custom labels and the set of additional route parameters contain conflicting label names.", nameof(HttpMetricsOptionsBase.CustomLabels)); + } - private Dictionary> CreateLabelToValueProviderMap() - { - var map = new Dictionary>(_customLabels.Count); + private Dictionary CreateLabelToRouteParameterMap() + { + var map = new Dictionary(_additionalRouteParameters.Count + 2); - foreach (var entry in _customLabels) - map[entry.LabelName] = entry.LabelValueProvider; + // Defaults are hardcoded. + map["action"] = "action"; + map["controller"] = "controller"; - return map; - } + // Any additional ones are merged. + foreach (var entry in _additionalRouteParameters) + map[entry.LabelName] = entry.ParameterName; - /// - /// Inspects the metric instance to ensure that all required labels are present. - /// - /// - /// If there are mappings to include route parameters in the labels, there must be labels defined for each such parameter. - /// We do this automatically if we use the default metric instance but if a custom one is provided, this must be done by the caller. - /// - private void ValidateAdditionalRouteParametersPresentInMetricLabelNames() - { - var labelNames = _additionalRouteParameters.Select(x => x.LabelName).ToList(); - var missing = labelNames.Except(_metric.LabelNames); + return map; + } - if (missing.Any()) - throw new ArgumentException($"Provided custom HTTP request metric instance for {GetType().Name} is missing required labels: {string.Join(", ", missing)}."); - } + private Dictionary> CreateLabelToValueProviderMap() + { + var map = new Dictionary>(_customLabels.Count); - /// - /// Inspects the metric instance to ensure that all required labels are present. - /// - /// - /// If there are mappings to include custom labels, there must be label names defined for each such parameter. - /// We do this automatically if we use the default metric instance but if a custom one is provided, this must be done by the caller. - /// - private void ValidateCustomLabelsPresentInMetricLabelNames() - { - var labelNames = _customLabels.Select(x => x.LabelName).ToList(); - var missing = labelNames.Except(_metric.LabelNames); + foreach (var entry in _customLabels) + map[entry.LabelName] = entry.LabelValueProvider; - if (missing.Any()) - throw new ArgumentException($"Provided custom HTTP request metric instance for {GetType().Name} is missing required labels: {string.Join(", ", missing)}."); - } + return map; + } - /// - /// If we use a custom metric, it should not have labels that are neither defaults nor additional route parameters. - /// - private void ValidateNoUnexpectedLabelNames() - { - var allowedLabels = CreateAllowedLabelSet(); - var unexpected = _metric.LabelNames.Except(allowedLabels); + /// + /// Inspects the metric instance to ensure that all required labels are present. + /// + /// + /// If there are mappings to include route parameters in the labels, there must be labels defined for each such parameter. + /// We do this automatically if we use the default metric instance but if a custom one is provided, this must be done by the caller. + /// + private void ValidateAdditionalRouteParametersPresentInMetricLabelNames() + { + var labelNames = _additionalRouteParameters.Select(x => x.LabelName).ToList(); + var missing = labelNames.Except(_metric.LabelNames); - if (unexpected.Any()) - throw new ArgumentException($"Provided custom HTTP request metric instance for {GetType().Name} has some unexpected labels: {string.Join(", ", unexpected)}."); - } + if (missing.Any()) + throw new ArgumentException($"Provided custom HTTP request metric instance for {GetType().Name} is missing required labels: {string.Join(", ", missing)}."); + } + + /// + /// Inspects the metric instance to ensure that all required labels are present. + /// + /// + /// If there are mappings to include custom labels, there must be label names defined for each such parameter. + /// We do this automatically if we use the default metric instance but if a custom one is provided, this must be done by the caller. + /// + private void ValidateCustomLabelsPresentInMetricLabelNames() + { + var labelNames = _customLabels.Select(x => x.LabelName).ToList(); + var missing = labelNames.Except(_metric.LabelNames); + + if (missing.Any()) + throw new ArgumentException($"Provided custom HTTP request metric instance for {GetType().Name} is missing required labels: {string.Join(", ", missing)}."); + } + + /// + /// If we use a custom metric, it should not have labels that are neither defaults nor additional route parameters. + /// + private void ValidateNoUnexpectedLabelNames() + { + var allowedLabels = CreateAllowedLabelSet(); + var unexpected = _metric.LabelNames.Except(allowedLabels); + + if (unexpected.Any()) + throw new ArgumentException($"Provided custom HTTP request metric instance for {GetType().Name} has some unexpected labels: {string.Join(", ", unexpected)}."); } } diff --git a/Prometheus.AspNetCore/HttpMetrics/HttpRouteParameterMapping.cs b/Prometheus.AspNetCore/HttpMetrics/HttpRouteParameterMapping.cs index c1977984..fafab89a 100644 --- a/Prometheus.AspNetCore/HttpMetrics/HttpRouteParameterMapping.cs +++ b/Prometheus.AspNetCore/HttpMetrics/HttpRouteParameterMapping.cs @@ -1,40 +1,39 @@ -namespace Prometheus.HttpMetrics +namespace Prometheus.HttpMetrics; + +/// +/// Maps an HTTP route parameter name to a Prometheus label name. +/// +/// +/// Typically, the parameter name and the label name will be equal. +/// The purpose of this is to enable capture of route parameters that conflict with built-in label names like "method" (HTTP method). +/// +public sealed class HttpRouteParameterMapping { /// - /// Maps an HTTP route parameter name to a Prometheus label name. + /// Name of the HTTP route parameter. /// - /// - /// Typically, the parameter name and the label name will be equal. - /// The purpose of this is to enable capture of route parameters that conflict with built-in label names like "method" (HTTP method). - /// - public sealed class HttpRouteParameterMapping - { - /// - /// Name of the HTTP route parameter. - /// - public string ParameterName { get; } - - /// - /// Name of the Prometheus label. - /// - public string LabelName { get; } + public string ParameterName { get; } - public HttpRouteParameterMapping(string name) - { - Collector.ValidateLabelName(name); + /// + /// Name of the Prometheus label. + /// + public string LabelName { get; } - ParameterName = name; - LabelName = name; - } + public HttpRouteParameterMapping(string name) + { + Collector.ValidateLabelName(name); - public HttpRouteParameterMapping(string parameterName, string labelName) - { - Collector.ValidateLabelName(labelName); + ParameterName = name; + LabelName = name; + } - ParameterName = parameterName; - LabelName = labelName; - } + public HttpRouteParameterMapping(string parameterName, string labelName) + { + Collector.ValidateLabelName(labelName); - public static implicit operator HttpRouteParameterMapping(string name) => new HttpRouteParameterMapping(name); + ParameterName = parameterName; + LabelName = labelName; } + + public static implicit operator HttpRouteParameterMapping(string name) => new HttpRouteParameterMapping(name); } diff --git a/Prometheus.AspNetCore/HttpMetrics/ICapturedRouteDataFeature.cs b/Prometheus.AspNetCore/HttpMetrics/ICapturedRouteDataFeature.cs index dc99e518..d712c9d3 100644 --- a/Prometheus.AspNetCore/HttpMetrics/ICapturedRouteDataFeature.cs +++ b/Prometheus.AspNetCore/HttpMetrics/ICapturedRouteDataFeature.cs @@ -1,9 +1,8 @@ using Microsoft.AspNetCore.Routing; -namespace Prometheus.HttpMetrics +namespace Prometheus.HttpMetrics; + +interface ICapturedRouteDataFeature { - interface ICapturedRouteDataFeature - { - RouteValueDictionary Values { get; } - } + RouteValueDictionary Values { get; } } diff --git a/Prometheus.AspNetCore/KestrelMetricServerExtensions.cs b/Prometheus.AspNetCore/KestrelMetricServerExtensions.cs index 757b78a4..b5215ca2 100644 --- a/Prometheus.AspNetCore/KestrelMetricServerExtensions.cs +++ b/Prometheus.AspNetCore/KestrelMetricServerExtensions.cs @@ -1,45 +1,44 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -namespace Prometheus +namespace Prometheus; + +public static class KestrelMetricServerExtensions { - public static class KestrelMetricServerExtensions + public static IServiceCollection AddMetricServer(this IServiceCollection services, Action optionsCallback) { - public static IServiceCollection AddMetricServer(this IServiceCollection services, Action optionsCallback) + return services.AddHostedService(sp => { - return services.AddHostedService(sp => - { - var options = new KestrelMetricServerOptions(); - optionsCallback(options); - return new MetricsExporterService(options); - }); + var options = new KestrelMetricServerOptions(); + optionsCallback(options); + return new MetricsExporterService(options); + }); + } + + private sealed class MetricsExporterService : BackgroundService + { + public MetricsExporterService(KestrelMetricServerOptions options) + { + _options = options; } - private sealed class MetricsExporterService : BackgroundService + private readonly KestrelMetricServerOptions _options; + + protected override async Task ExecuteAsync(CancellationToken cancel) { - public MetricsExporterService(KestrelMetricServerOptions options) + using var metricServer = new KestrelMetricServer(_options); + metricServer.Start(); + + try { - _options = options; + // Wait forever, until we are told to stop. + await Task.Delay(-1, cancel); } - - private readonly KestrelMetricServerOptions _options; - - protected override async Task ExecuteAsync(CancellationToken cancel) + catch (OperationCanceledException) when (cancel.IsCancellationRequested) { - using var metricServer = new KestrelMetricServer(_options); - metricServer.Start(); - - try - { - // Wait forever, until we are told to stop. - await Task.Delay(-1, cancel); - } - catch (OperationCanceledException) when (cancel.IsCancellationRequested) - { - // Time to stop. - } + // Time to stop. } } - } + } diff --git a/Prometheus.AspNetCore/MetricServerMiddlewareExtensions.cs b/Prometheus.AspNetCore/MetricServerMiddlewareExtensions.cs index 0271de9a..742befb7 100644 --- a/Prometheus.AspNetCore/MetricServerMiddlewareExtensions.cs +++ b/Prometheus.AspNetCore/MetricServerMiddlewareExtensions.cs @@ -3,130 +3,129 @@ using Microsoft.AspNetCore.Routing; using System.ComponentModel; -namespace Prometheus -{ - public static class MetricServerMiddlewareExtensions - { - private const string DefaultDisplayName = "Prometheus metrics"; +namespace Prometheus; - /// - /// Starts a Prometheus metrics exporter using endpoint routing. - /// The default URL is /metrics, which is a Prometheus convention. - /// Use static methods on the class to create your metrics. - /// - public static IEndpointConventionBuilder MapMetrics( - this IEndpointRouteBuilder endpoints, - Action configure, - string pattern = "/metrics" - ) - { - var pipeline = endpoints - .CreateApplicationBuilder() - .InternalUseMiddleware(configure) - .Build(); +public static class MetricServerMiddlewareExtensions +{ + private const string DefaultDisplayName = "Prometheus metrics"; - return endpoints - .Map(pattern, pipeline) - .WithDisplayName(DefaultDisplayName); - } + /// + /// Starts a Prometheus metrics exporter using endpoint routing. + /// The default URL is /metrics, which is a Prometheus convention. + /// Use static methods on the class to create your metrics. + /// + public static IEndpointConventionBuilder MapMetrics( + this IEndpointRouteBuilder endpoints, + Action configure, + string pattern = "/metrics" + ) + { + var pipeline = endpoints + .CreateApplicationBuilder() + .InternalUseMiddleware(configure) + .Build(); - /// - /// Starts a Prometheus metrics exporter, filtering to only handle requests received on a specific port. - /// The default URL is /metrics, which is a Prometheus convention. - /// Use static methods on the class to create your metrics. - /// - public static IApplicationBuilder UseMetricServer( - this IApplicationBuilder builder, - int port, - Action configure, - string? url = "/metrics") - { - // If no URL, use root URL. - url ??= "/"; + return endpoints + .Map(pattern, pipeline) + .WithDisplayName(DefaultDisplayName); + } - return builder - .Map(url, b => b.MapWhen(PortMatches(), b1 => b1.InternalUseMiddleware(configure))); + /// + /// Starts a Prometheus metrics exporter, filtering to only handle requests received on a specific port. + /// The default URL is /metrics, which is a Prometheus convention. + /// Use static methods on the class to create your metrics. + /// + public static IApplicationBuilder UseMetricServer( + this IApplicationBuilder builder, + int port, + Action configure, + string? url = "/metrics") + { + // If no URL, use root URL. + url ??= "/"; - Func PortMatches() - { - return c => c.Connection.LocalPort == port; - } - } + return builder + .Map(url, b => b.MapWhen(PortMatches(), b1 => b1.InternalUseMiddleware(configure))); - /// - /// Starts a Prometheus metrics exporter. - /// The default URL is /metrics, which is a Prometheus convention. - /// Use static methods on the class to create your metrics. - /// - public static IApplicationBuilder UseMetricServer( - this IApplicationBuilder builder, - Action configure, - string? url = "/metrics") + Func PortMatches() { - if (url != null) - return builder.Map(url, b => b.InternalUseMiddleware(configure)); - else - return builder.InternalUseMiddleware(configure); + return c => c.Connection.LocalPort == port; } + } - #region Legacy methods without the configure action - /// - /// Starts a Prometheus metrics exporter using endpoint routing. - /// The default URL is /metrics, which is a Prometheus convention. - /// Use static methods on the class to create your metrics. - /// - [EditorBrowsable(EditorBrowsableState.Never)] // It is not exactly obsolete but let's de-emphasize it. - public static IEndpointConventionBuilder MapMetrics( - this IEndpointRouteBuilder endpoints, - string pattern = "/metrics", - CollectorRegistry? registry = null - ) - { - return MapMetrics(endpoints, LegacyConfigure(registry), pattern); - } + /// + /// Starts a Prometheus metrics exporter. + /// The default URL is /metrics, which is a Prometheus convention. + /// Use static methods on the class to create your metrics. + /// + public static IApplicationBuilder UseMetricServer( + this IApplicationBuilder builder, + Action configure, + string? url = "/metrics") + { + if (url != null) + return builder.Map(url, b => b.InternalUseMiddleware(configure)); + else + return builder.InternalUseMiddleware(configure); + } - /// - /// Starts a Prometheus metrics exporter, filtering to only handle requests received on a specific port. - /// The default URL is /metrics, which is a Prometheus convention. - /// Use static methods on the class to create your metrics. - /// - [EditorBrowsable(EditorBrowsableState.Never)] // It is not exactly obsolete but let's de-emphasize it. - public static IApplicationBuilder UseMetricServer( - this IApplicationBuilder builder, - int port, - string? url = "/metrics", - CollectorRegistry? registry = null) - { - return UseMetricServer(builder, port, LegacyConfigure(registry), url); - } + #region Legacy methods without the configure action + /// + /// Starts a Prometheus metrics exporter using endpoint routing. + /// The default URL is /metrics, which is a Prometheus convention. + /// Use static methods on the class to create your metrics. + /// + [EditorBrowsable(EditorBrowsableState.Never)] // It is not exactly obsolete but let's de-emphasize it. + public static IEndpointConventionBuilder MapMetrics( + this IEndpointRouteBuilder endpoints, + string pattern = "/metrics", + CollectorRegistry? registry = null + ) + { + return MapMetrics(endpoints, LegacyConfigure(registry), pattern); + } - /// - /// Starts a Prometheus metrics exporter. - /// The default URL is /metrics, which is a Prometheus convention. - /// Use static methods on the class to create your metrics. - /// - [EditorBrowsable(EditorBrowsableState.Never)] // It is not exactly obsolete but let's de-emphasize it. - public static IApplicationBuilder UseMetricServer( - this IApplicationBuilder builder, - string? url = "/metrics", - CollectorRegistry? registry = null) - { - return UseMetricServer(builder, LegacyConfigure(registry), url); - } + /// + /// Starts a Prometheus metrics exporter, filtering to only handle requests received on a specific port. + /// The default URL is /metrics, which is a Prometheus convention. + /// Use static methods on the class to create your metrics. + /// + [EditorBrowsable(EditorBrowsableState.Never)] // It is not exactly obsolete but let's de-emphasize it. + public static IApplicationBuilder UseMetricServer( + this IApplicationBuilder builder, + int port, + string? url = "/metrics", + CollectorRegistry? registry = null) + { + return UseMetricServer(builder, port, LegacyConfigure(registry), url); + } - private static Action LegacyConfigure(CollectorRegistry? registry) => - (MetricServerMiddleware.Settings settings) => - { - settings.Registry = registry; - }; - #endregion + /// + /// Starts a Prometheus metrics exporter. + /// The default URL is /metrics, which is a Prometheus convention. + /// Use static methods on the class to create your metrics. + /// + [EditorBrowsable(EditorBrowsableState.Never)] // It is not exactly obsolete but let's de-emphasize it. + public static IApplicationBuilder UseMetricServer( + this IApplicationBuilder builder, + string? url = "/metrics", + CollectorRegistry? registry = null) + { + return UseMetricServer(builder, LegacyConfigure(registry), url); + } - private static IApplicationBuilder InternalUseMiddleware(this IApplicationBuilder builder, Action configure) + private static Action LegacyConfigure(CollectorRegistry? registry) => + (MetricServerMiddleware.Settings settings) => { - var settings = new MetricServerMiddleware.Settings(); - configure(settings); + settings.Registry = registry; + }; + #endregion - return builder.UseMiddleware(settings); - } + private static IApplicationBuilder InternalUseMiddleware(this IApplicationBuilder builder, Action configure) + { + var settings = new MetricServerMiddleware.Settings(); + configure(settings); + + return builder.UseMiddleware(settings); } } diff --git a/Prometheus/GaugeConfiguration.cs b/Prometheus/GaugeConfiguration.cs index ca732340..fea1b1f1 100644 --- a/Prometheus/GaugeConfiguration.cs +++ b/Prometheus/GaugeConfiguration.cs @@ -1,7 +1,6 @@ -namespace Prometheus +namespace Prometheus; + +public sealed class GaugeConfiguration : MetricConfiguration { - public sealed class GaugeConfiguration : MetricConfiguration - { - internal static readonly GaugeConfiguration Default = new GaugeConfiguration(); - } + internal static readonly GaugeConfiguration Default = new(); } diff --git a/Prometheus/HistogramConfiguration.cs b/Prometheus/HistogramConfiguration.cs index 7f147403..e4c449d9 100644 --- a/Prometheus/HistogramConfiguration.cs +++ b/Prometheus/HistogramConfiguration.cs @@ -1,18 +1,17 @@ -namespace Prometheus +namespace Prometheus; + +public sealed class HistogramConfiguration : MetricConfiguration { - public sealed class HistogramConfiguration : MetricConfiguration - { - internal static readonly HistogramConfiguration Default = new HistogramConfiguration(); + internal static readonly HistogramConfiguration Default = new HistogramConfiguration(); - /// - /// Custom histogram buckets to use. If null, will use Histogram.DefaultBuckets. - /// - public double[]? Buckets { get; set; } + /// + /// Custom histogram buckets to use. If null, will use Histogram.DefaultBuckets. + /// + public double[]? Buckets { get; set; } - /// - /// Allows you to configure how exemplars are applied to the published metric. - /// If null, inherits the exemplar behavior from the metric factory. - /// - public ExemplarBehavior? ExemplarBehavior { get; set; } - } + /// + /// Allows you to configure how exemplars are applied to the published metric. + /// If null, inherits the exemplar behavior from the metric factory. + /// + public ExemplarBehavior? ExemplarBehavior { get; set; } } diff --git a/Prometheus/HttpClientMetrics/HttpClientDelegatingHandlerBase.cs b/Prometheus/HttpClientMetrics/HttpClientDelegatingHandlerBase.cs index 4f4ab71d..2f80afad 100644 --- a/Prometheus/HttpClientMetrics/HttpClientDelegatingHandlerBase.cs +++ b/Prometheus/HttpClientMetrics/HttpClientDelegatingHandlerBase.cs @@ -1,107 +1,106 @@ -namespace Prometheus.HttpClientMetrics +namespace Prometheus.HttpClientMetrics; + +/// +/// This base class performs the data management necessary to associate the correct labels and values +/// with HttpClient metrics, depending on the options the user has provided for the HttpClient metric handler. +/// +/// The following labels are supported: +/// 'method' (HTTP request method) +/// 'host' (The host name of HTTP request) +/// 'client' (The name of the HttpClient) +/// 'code' (HTTP response status code) +/// +internal abstract class HttpClientDelegatingHandlerBase : DelegatingHandler + where TCollector : class, ICollector + where TChild : class, ICollectorChild { /// - /// This base class performs the data management necessary to associate the correct labels and values - /// with HttpClient metrics, depending on the options the user has provided for the HttpClient metric handler. - /// - /// The following labels are supported: - /// 'method' (HTTP request method) - /// 'host' (The host name of HTTP request) - /// 'client' (The name of the HttpClient) - /// 'code' (HTTP response status code) + /// The set of labels from among the defaults that this metric supports. /// - internal abstract class HttpClientDelegatingHandlerBase : DelegatingHandler - where TCollector : class, ICollector - where TChild : class, ICollectorChild - { - /// - /// The set of labels from among the defaults that this metric supports. - /// - protected abstract string[] DefaultLabels { get; } + protected abstract string[] DefaultLabels { get; } - /// - /// The factory to use for creating the default metric for this middleware. - /// Not used if a custom metric is already provided in options. - /// - protected MetricFactory MetricFactory { get; } + /// + /// The factory to use for creating the default metric for this middleware. + /// Not used if a custom metric is already provided in options. + /// + protected MetricFactory MetricFactory { get; } - /// - /// Creates the default metric instance with the specified set of labels. - /// Only used if the caller does not provide a custom metric instance in the options. - /// - protected abstract TCollector CreateMetricInstance(string[] labelNames); + /// + /// Creates the default metric instance with the specified set of labels. + /// Only used if the caller does not provide a custom metric instance in the options. + /// + protected abstract TCollector CreateMetricInstance(string[] labelNames); - // Internal only for tests. - internal readonly TCollector _metric; + // Internal only for tests. + internal readonly TCollector _metric; - protected HttpClientDelegatingHandlerBase(HttpClientMetricsOptionsBase? options, TCollector? customMetric, HttpClientIdentity identity) - { - _identity = identity; + protected HttpClientDelegatingHandlerBase(HttpClientMetricsOptionsBase? options, TCollector? customMetric, HttpClientIdentity identity) + { + _identity = identity; - MetricFactory = Metrics.WithCustomRegistry(options?.Registry ?? Metrics.DefaultRegistry); + MetricFactory = Metrics.WithCustomRegistry(options?.Registry ?? Metrics.DefaultRegistry); - if (customMetric != null) - { - _metric = customMetric; + if (customMetric != null) + { + _metric = customMetric; - ValidateNoUnexpectedLabelNames(); - } - else - { - _metric = CreateMetricInstance(HttpClientRequestLabelNames.All); - } + ValidateNoUnexpectedLabelNames(); + } + else + { + _metric = CreateMetricInstance(HttpClientRequestLabelNames.All); } + } - private readonly HttpClientIdentity _identity; + private readonly HttpClientIdentity _identity; - /// - /// Creates the metric child instance to use for measurements. - /// - /// - /// Internal for testing purposes. - /// - protected internal TChild CreateChild(HttpRequestMessage request, HttpResponseMessage? response) - { - if (!_metric.LabelNames.Any()) - return _metric.Unlabelled; + /// + /// Creates the metric child instance to use for measurements. + /// + /// + /// Internal for testing purposes. + /// + protected internal TChild CreateChild(HttpRequestMessage request, HttpResponseMessage? response) + { + if (!_metric.LabelNames.Any()) + return _metric.Unlabelled; - var labelValues = new string[_metric.LabelNames.Length]; + var labelValues = new string[_metric.LabelNames.Length]; - for (var i = 0; i < labelValues.Length; i++) + for (var i = 0; i < labelValues.Length; i++) + { + switch (_metric.LabelNames[i]) { - switch (_metric.LabelNames[i]) - { - case HttpClientRequestLabelNames.Method: - labelValues[i] = request.Method.Method; - break; - case HttpClientRequestLabelNames.Host: - labelValues[i] = request.RequestUri?.Host ?? ""; - break; - case HttpClientRequestLabelNames.Client: - labelValues[i] = _identity.Name; - break; - case HttpClientRequestLabelNames.Code: - labelValues[i] = response != null ? ((int)response.StatusCode).ToString() : ""; - break; - default: - // We validate the label set on initialization, so this is impossible. - throw new NotSupportedException($"Found unsupported label on metric: {_metric.LabelNames[i]}"); - } + case HttpClientRequestLabelNames.Method: + labelValues[i] = request.Method.Method; + break; + case HttpClientRequestLabelNames.Host: + labelValues[i] = request.RequestUri?.Host ?? ""; + break; + case HttpClientRequestLabelNames.Client: + labelValues[i] = _identity.Name; + break; + case HttpClientRequestLabelNames.Code: + labelValues[i] = response != null ? ((int)response.StatusCode).ToString() : ""; + break; + default: + // We validate the label set on initialization, so this is impossible. + throw new NotSupportedException($"Found unsupported label on metric: {_metric.LabelNames[i]}"); } - - return _metric.WithLabels(labelValues); } - /// - /// If we use a custom metric, it should not have labels that are not among the defaults. - /// - private void ValidateNoUnexpectedLabelNames() - { - var allowedLabels = HttpClientRequestLabelNames.All; - var unexpected = _metric.LabelNames.Except(allowedLabels); + return _metric.WithLabels(labelValues); + } - if (unexpected.Any()) - throw new ArgumentException($"Provided custom HttpClient metric instance for {GetType().Name} has some unexpected labels: {string.Join(", ", unexpected)}."); - } + /// + /// If we use a custom metric, it should not have labels that are not among the defaults. + /// + private void ValidateNoUnexpectedLabelNames() + { + var allowedLabels = HttpClientRequestLabelNames.All; + var unexpected = _metric.LabelNames.Except(allowedLabels); + + if (unexpected.Any()) + throw new ArgumentException($"Provided custom HttpClient metric instance for {GetType().Name} has some unexpected labels: {string.Join(", ", unexpected)}."); } } \ No newline at end of file diff --git a/Prometheus/HttpClientMetrics/HttpClientExporterOptions.cs b/Prometheus/HttpClientMetrics/HttpClientExporterOptions.cs index 6f58df20..c1bb529c 100644 --- a/Prometheus/HttpClientMetrics/HttpClientExporterOptions.cs +++ b/Prometheus/HttpClientMetrics/HttpClientExporterOptions.cs @@ -1,10 +1,9 @@ -namespace Prometheus.HttpClientMetrics +namespace Prometheus.HttpClientMetrics; + +public sealed class HttpClientExporterOptions { - public sealed class HttpClientExporterOptions - { - public HttpClientInProgressOptions InProgress { get; set; } = new HttpClientInProgressOptions(); - public HttpClientRequestCountOptions RequestCount { get; set; } = new HttpClientRequestCountOptions(); - public HttpClientRequestDurationOptions RequestDuration { get; set; } = new HttpClientRequestDurationOptions(); - public HttpClientResponseDurationOptions ResponseDuration { get; set; } = new HttpClientResponseDurationOptions(); - } + public HttpClientInProgressOptions InProgress { get; set; } = new HttpClientInProgressOptions(); + public HttpClientRequestCountOptions RequestCount { get; set; } = new HttpClientRequestCountOptions(); + public HttpClientRequestDurationOptions RequestDuration { get; set; } = new HttpClientRequestDurationOptions(); + public HttpClientResponseDurationOptions ResponseDuration { get; set; } = new HttpClientResponseDurationOptions(); } \ No newline at end of file diff --git a/Prometheus/HttpClientMetrics/HttpClientIdentity.cs b/Prometheus/HttpClientMetrics/HttpClientIdentity.cs index 595e1ebe..7f8cde90 100644 --- a/Prometheus/HttpClientMetrics/HttpClientIdentity.cs +++ b/Prometheus/HttpClientMetrics/HttpClientIdentity.cs @@ -1,14 +1,13 @@ -namespace Prometheus.HttpClientMetrics +namespace Prometheus.HttpClientMetrics; + +public sealed class HttpClientIdentity { - public sealed class HttpClientIdentity - { - public static readonly HttpClientIdentity Default = new HttpClientIdentity("default"); + public static readonly HttpClientIdentity Default = new HttpClientIdentity("default"); - public string Name { get; } + public string Name { get; } - public HttpClientIdentity(string name) - { - Name = name; - } + public HttpClientIdentity(string name) + { + Name = name; } } diff --git a/Prometheus/HttpClientMetrics/HttpClientInProgressHandler.cs b/Prometheus/HttpClientMetrics/HttpClientInProgressHandler.cs index e539c2e6..4624b02a 100644 --- a/Prometheus/HttpClientMetrics/HttpClientInProgressHandler.cs +++ b/Prometheus/HttpClientMetrics/HttpClientInProgressHandler.cs @@ -1,26 +1,25 @@ -namespace Prometheus.HttpClientMetrics +namespace Prometheus.HttpClientMetrics; + +internal sealed class HttpClientInProgressHandler : HttpClientDelegatingHandlerBase, IGauge> { - internal sealed class HttpClientInProgressHandler : HttpClientDelegatingHandlerBase, IGauge> + public HttpClientInProgressHandler(HttpClientInProgressOptions? options, HttpClientIdentity identity) + : base(options, options?.Gauge, identity) { - public HttpClientInProgressHandler(HttpClientInProgressOptions? options, HttpClientIdentity identity) - : base(options, options?.Gauge, identity) - { - } + } - protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + using (CreateChild(request, null).TrackInProgress()) { - using (CreateChild(request, null).TrackInProgress()) - { - // Returns when the response HEADERS are seen. - return await base.SendAsync(request, cancellationToken); - } + // Returns when the response HEADERS are seen. + return await base.SendAsync(request, cancellationToken); } + } - protected override string[] DefaultLabels => HttpClientRequestLabelNames.KnownInAdvance; + protected override string[] DefaultLabels => HttpClientRequestLabelNames.KnownInAdvance; - protected override ICollector CreateMetricInstance(string[] labelNames) => MetricFactory.CreateGauge( - "httpclient_requests_in_progress", - "Number of requests currently being executed by an HttpClient that have not yet received response headers. Value is decremented once response headers are received.", - labelNames); - } + protected override ICollector CreateMetricInstance(string[] labelNames) => MetricFactory.CreateGauge( + "httpclient_requests_in_progress", + "Number of requests currently being executed by an HttpClient that have not yet received response headers. Value is decremented once response headers are received.", + labelNames); } \ No newline at end of file diff --git a/Prometheus/HttpClientMetrics/HttpClientInProgressOptions.cs b/Prometheus/HttpClientMetrics/HttpClientInProgressOptions.cs index 68853edb..e701914e 100644 --- a/Prometheus/HttpClientMetrics/HttpClientInProgressOptions.cs +++ b/Prometheus/HttpClientMetrics/HttpClientInProgressOptions.cs @@ -1,10 +1,9 @@ -namespace Prometheus.HttpClientMetrics +namespace Prometheus.HttpClientMetrics; + +public sealed class HttpClientInProgressOptions : HttpClientMetricsOptionsBase { - public sealed class HttpClientInProgressOptions : HttpClientMetricsOptionsBase - { - /// - /// Set this to use a custom metric instead of the default. - /// - public ICollector? Gauge { get; set; } - } + /// + /// Set this to use a custom metric instead of the default. + /// + public ICollector? Gauge { get; set; } } \ No newline at end of file diff --git a/Prometheus/HttpClientMetrics/HttpClientMetricsOptionsBase.cs b/Prometheus/HttpClientMetrics/HttpClientMetricsOptionsBase.cs index 95192635..f810d5c9 100644 --- a/Prometheus/HttpClientMetrics/HttpClientMetricsOptionsBase.cs +++ b/Prometheus/HttpClientMetrics/HttpClientMetricsOptionsBase.cs @@ -1,13 +1,12 @@ -namespace Prometheus.HttpClientMetrics +namespace Prometheus.HttpClientMetrics; + +public abstract class HttpClientMetricsOptionsBase { - public abstract class HttpClientMetricsOptionsBase - { - public bool Enabled { get; set; } = true; + public bool Enabled { get; set; } = true; - /// - /// Allows you to override the registry used to create the default metric instance. - /// Value is ignored if you specify a custom metric instance in the options. - /// - public CollectorRegistry? Registry { get; set; } - } + /// + /// Allows you to override the registry used to create the default metric instance. + /// Value is ignored if you specify a custom metric instance in the options. + /// + public CollectorRegistry? Registry { get; set; } } \ No newline at end of file diff --git a/Prometheus/HttpClientMetrics/HttpClientRequestCountHandler.cs b/Prometheus/HttpClientMetrics/HttpClientRequestCountHandler.cs index c2555cd2..57d2ee51 100644 --- a/Prometheus/HttpClientMetrics/HttpClientRequestCountHandler.cs +++ b/Prometheus/HttpClientMetrics/HttpClientRequestCountHandler.cs @@ -1,32 +1,31 @@ -namespace Prometheus.HttpClientMetrics +namespace Prometheus.HttpClientMetrics; + +internal sealed class HttpClientRequestCountHandler : HttpClientDelegatingHandlerBase, ICounter> { - internal sealed class HttpClientRequestCountHandler : HttpClientDelegatingHandlerBase, ICounter> + public HttpClientRequestCountHandler(HttpClientRequestCountOptions? options, HttpClientIdentity identity) + : base(options, options?.Counter, identity) { - public HttpClientRequestCountHandler(HttpClientRequestCountOptions? options, HttpClientIdentity identity) - : base(options, options?.Counter, identity) + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + HttpResponseMessage? response = null; + + try { + response = await base.SendAsync(request, cancellationToken); + return response; } - - protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + finally { - HttpResponseMessage? response = null; - - try - { - response = await base.SendAsync(request, cancellationToken); - return response; - } - finally - { - CreateChild(request, response).Inc(); - } + CreateChild(request, response).Inc(); } + } - protected override string[] DefaultLabels => HttpClientRequestLabelNames.All; + protected override string[] DefaultLabels => HttpClientRequestLabelNames.All; - protected override ICollector CreateMetricInstance(string[] labelNames) => MetricFactory.CreateCounter( - "httpclient_requests_sent_total", - "Count of HTTP requests that have been completed by an HttpClient.", - labelNames); - } + protected override ICollector CreateMetricInstance(string[] labelNames) => MetricFactory.CreateCounter( + "httpclient_requests_sent_total", + "Count of HTTP requests that have been completed by an HttpClient.", + labelNames); } \ No newline at end of file diff --git a/Prometheus/HttpClientMetrics/HttpClientRequestCountOptions.cs b/Prometheus/HttpClientMetrics/HttpClientRequestCountOptions.cs index 5985c2fb..c94bfd2d 100644 --- a/Prometheus/HttpClientMetrics/HttpClientRequestCountOptions.cs +++ b/Prometheus/HttpClientMetrics/HttpClientRequestCountOptions.cs @@ -1,10 +1,9 @@ -namespace Prometheus.HttpClientMetrics +namespace Prometheus.HttpClientMetrics; + +public sealed class HttpClientRequestCountOptions : HttpClientMetricsOptionsBase { - public sealed class HttpClientRequestCountOptions : HttpClientMetricsOptionsBase - { - /// - /// Set this to use a custom metric instead of the default. - /// - public ICollector? Counter { get; set; } - } + /// + /// Set this to use a custom metric instead of the default. + /// + public ICollector? Counter { get; set; } } \ No newline at end of file diff --git a/Prometheus/HttpClientMetrics/HttpClientRequestDurationHandler.cs b/Prometheus/HttpClientMetrics/HttpClientRequestDurationHandler.cs index 4fcc7dab..45b9c732 100644 --- a/Prometheus/HttpClientMetrics/HttpClientRequestDurationHandler.cs +++ b/Prometheus/HttpClientMetrics/HttpClientRequestDurationHandler.cs @@ -1,41 +1,40 @@ -namespace Prometheus.HttpClientMetrics +namespace Prometheus.HttpClientMetrics; + +internal sealed class HttpClientRequestDurationHandler : HttpClientDelegatingHandlerBase, IHistogram> { - internal sealed class HttpClientRequestDurationHandler : HttpClientDelegatingHandlerBase, IHistogram> + public HttpClientRequestDurationHandler(HttpClientRequestDurationOptions? options, HttpClientIdentity identity) + : base(options, options?.Histogram, identity) { - public HttpClientRequestDurationHandler(HttpClientRequestDurationOptions? options, HttpClientIdentity identity) - : base(options, options?.Histogram, identity) - { - } + } - protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - var stopWatch = ValueStopwatch.StartNew(); + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var stopWatch = ValueStopwatch.StartNew(); - HttpResponseMessage? response = null; + HttpResponseMessage? response = null; - try - { - // We measure until SendAsync returns - which is when the response HEADERS are seen. - // The response body may continue streaming for a long time afterwards, which this does not measure. - response = await base.SendAsync(request, cancellationToken); - return response; - } - finally - { - CreateChild(request, response).Observe(stopWatch.GetElapsedTime().TotalSeconds); - } + try + { + // We measure until SendAsync returns - which is when the response HEADERS are seen. + // The response body may continue streaming for a long time afterwards, which this does not measure. + response = await base.SendAsync(request, cancellationToken); + return response; + } + finally + { + CreateChild(request, response).Observe(stopWatch.GetElapsedTime().TotalSeconds); } + } - protected override string[] DefaultLabels => HttpClientRequestLabelNames.All; + protected override string[] DefaultLabels => HttpClientRequestLabelNames.All; - protected override ICollector CreateMetricInstance(string[] labelNames) => MetricFactory.CreateHistogram( - "httpclient_request_duration_seconds", - "Duration histogram of HTTP requests performed by an HttpClient.", - labelNames, - new HistogramConfiguration - { - // 1 ms to 32K ms buckets - Buckets = Histogram.ExponentialBuckets(0.001, 2, 16), - }); - } + protected override ICollector CreateMetricInstance(string[] labelNames) => MetricFactory.CreateHistogram( + "httpclient_request_duration_seconds", + "Duration histogram of HTTP requests performed by an HttpClient.", + labelNames, + new HistogramConfiguration + { + // 1 ms to 32K ms buckets + Buckets = Histogram.ExponentialBuckets(0.001, 2, 16), + }); } \ No newline at end of file diff --git a/Prometheus/HttpClientMetrics/HttpClientRequestDurationOptions.cs b/Prometheus/HttpClientMetrics/HttpClientRequestDurationOptions.cs index 8177e968..84c91252 100644 --- a/Prometheus/HttpClientMetrics/HttpClientRequestDurationOptions.cs +++ b/Prometheus/HttpClientMetrics/HttpClientRequestDurationOptions.cs @@ -1,10 +1,9 @@ -namespace Prometheus.HttpClientMetrics +namespace Prometheus.HttpClientMetrics; + +public sealed class HttpClientRequestDurationOptions : HttpClientMetricsOptionsBase { - public sealed class HttpClientRequestDurationOptions : HttpClientMetricsOptionsBase - { - /// - /// Set this to use a custom metric instead of the default. - /// - public ICollector? Histogram { get; set; } - } + /// + /// Set this to use a custom metric instead of the default. + /// + public ICollector? Histogram { get; set; } } \ No newline at end of file diff --git a/Prometheus/HttpClientMetrics/HttpClientRequestLabelNames.cs b/Prometheus/HttpClientMetrics/HttpClientRequestLabelNames.cs index a06a119d..2684fd77 100644 --- a/Prometheus/HttpClientMetrics/HttpClientRequestLabelNames.cs +++ b/Prometheus/HttpClientMetrics/HttpClientRequestLabelNames.cs @@ -1,30 +1,29 @@ -namespace Prometheus.HttpClientMetrics +namespace Prometheus.HttpClientMetrics; + +/// +/// Label names reserved for the use by the HttpClient metrics. +/// +public static class HttpClientRequestLabelNames { - /// - /// Label names reserved for the use by the HttpClient metrics. - /// - public static class HttpClientRequestLabelNames - { - public const string Method = "method"; - public const string Host = "host"; - public const string Client = "client"; - public const string Code = "code"; + public const string Method = "method"; + public const string Host = "host"; + public const string Client = "client"; + public const string Code = "code"; - public static readonly string[] All = - { - Method, - Host, - Client, - Code - }; + public static readonly string[] All = + { + Method, + Host, + Client, + Code + }; - // The labels known before receiving the response. - // Everything except the response status code, basically. - public static readonly string[] KnownInAdvance = - { - Method, - Host, - Client - }; - } + // The labels known before receiving the response. + // Everything except the response status code, basically. + public static readonly string[] KnownInAdvance = + { + Method, + Host, + Client + }; } \ No newline at end of file diff --git a/Prometheus/HttpClientMetrics/HttpClientResponseDurationHandler.cs b/Prometheus/HttpClientMetrics/HttpClientResponseDurationHandler.cs index a0b42a7a..69ff3529 100644 --- a/Prometheus/HttpClientMetrics/HttpClientResponseDurationHandler.cs +++ b/Prometheus/HttpClientMetrics/HttpClientResponseDurationHandler.cs @@ -1,142 +1,141 @@ using System.Net.Http.Headers; -namespace Prometheus.HttpClientMetrics +namespace Prometheus.HttpClientMetrics; + +internal sealed class HttpClientResponseDurationHandler : HttpClientDelegatingHandlerBase, IHistogram> { - internal sealed class HttpClientResponseDurationHandler : HttpClientDelegatingHandlerBase, IHistogram> + public HttpClientResponseDurationHandler(HttpClientResponseDurationOptions? options, HttpClientIdentity identity) + : base(options, options?.Histogram, identity) { - public HttpClientResponseDurationHandler(HttpClientResponseDurationOptions? options, HttpClientIdentity identity) - : base(options, options?.Histogram, identity) - { - } - - protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - var stopWatch = ValueStopwatch.StartNew(); + } - var response = await base.SendAsync(request, cancellationToken); + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var stopWatch = ValueStopwatch.StartNew(); - Stream oldStream = await response.Content.ReadAsStreamAsync(); + var response = await base.SendAsync(request, cancellationToken); - Wrap(response, oldStream, delegate - { - CreateChild(request, response).Observe(stopWatch.GetElapsedTime().TotalSeconds); - }); + Stream oldStream = await response.Content.ReadAsStreamAsync(); - return response; - } + Wrap(response, oldStream, delegate + { + CreateChild(request, response).Observe(stopWatch.GetElapsedTime().TotalSeconds); + }); - protected override string[] DefaultLabels => HttpClientRequestLabelNames.All; + return response; + } - protected override ICollector CreateMetricInstance(string[] labelNames) => MetricFactory.CreateHistogram( - "httpclient_response_duration_seconds", - "Duration histogram of HTTP requests performed by an HttpClient, measuring the duration until the HTTP response finished being processed.", - labelNames, - new HistogramConfiguration - { - // 1 ms to 32K ms buckets - Buckets = Histogram.ExponentialBuckets(0.001, 2, 16), - }); + protected override string[] DefaultLabels => HttpClientRequestLabelNames.All; - private void Wrap(HttpResponseMessage response, Stream oldStream, Action onEndOfStream) + protected override ICollector CreateMetricInstance(string[] labelNames) => MetricFactory.CreateHistogram( + "httpclient_response_duration_seconds", + "Duration histogram of HTTP requests performed by an HttpClient, measuring the duration until the HTTP response finished being processed.", + labelNames, + new HistogramConfiguration { - var newContent = new StreamContent(new EndOfStreamDetectingStream(oldStream, onEndOfStream)); + // 1 ms to 32K ms buckets + Buckets = Histogram.ExponentialBuckets(0.001, 2, 16), + }); + + private void Wrap(HttpResponseMessage response, Stream oldStream, Action onEndOfStream) + { + var newContent = new StreamContent(new EndOfStreamDetectingStream(oldStream, onEndOfStream)); - var oldHeaders = response.Content.Headers; - var newHeaders = newContent.Headers; + var oldHeaders = response.Content.Headers; + var newHeaders = newContent.Headers; #if NET6_0_OR_GREATER - foreach (KeyValuePair header in oldHeaders.NonValidated) + foreach (KeyValuePair header in oldHeaders.NonValidated) + { + if (header.Value.Count > 1) { - if (header.Value.Count > 1) - { - newHeaders.TryAddWithoutValidation(header.Key, header.Value); - } - else - { - newHeaders.TryAddWithoutValidation(header.Key, header.Value.ToString()); - } + newHeaders.TryAddWithoutValidation(header.Key, header.Value); } -#else - foreach (var header in oldHeaders) + else { - newHeaders.TryAddWithoutValidation(header.Key, header.Value); + newHeaders.TryAddWithoutValidation(header.Key, header.Value.ToString()); } + } +#else + foreach (var header in oldHeaders) + { + newHeaders.TryAddWithoutValidation(header.Key, header.Value); + } #endif - response.Content = newContent; + response.Content = newContent; + } + + private sealed class EndOfStreamDetectingStream : Stream + { + public EndOfStreamDetectingStream(Stream inner, Action onEndOfStream) + { + _inner = inner; + _onEndOfStream = onEndOfStream; } - private sealed class EndOfStreamDetectingStream : Stream + private readonly Stream _inner; + private readonly Action _onEndOfStream; + private int _sawEndOfStream = 0; + + public override void Flush() => _inner.Flush(); + + public override int Read(byte[] buffer, int offset, int count) { - public EndOfStreamDetectingStream(Stream inner, Action onEndOfStream) + var read = _inner.Read(buffer, offset, count); + + if (read == 0 && buffer.Length != 0) { - _inner = inner; - _onEndOfStream = onEndOfStream; + SignalCompletion(); } - private readonly Stream _inner; - private readonly Action _onEndOfStream; - private int _sawEndOfStream = 0; + return read; + } - public override void Flush() => _inner.Flush(); + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return buffer.Length == 0 + ? _inner.ReadAsync(buffer, offset, count, cancellationToken) + : ReadAsyncCore(this, _inner.ReadAsync(buffer, offset, count, cancellationToken)); - public override int Read(byte[] buffer, int offset, int count) + static async Task ReadAsyncCore(EndOfStreamDetectingStream stream, Task readTask) { - var read = _inner.Read(buffer, offset, count); + int read = await readTask; - if (read == 0 && buffer.Length != 0) + if (read == 0) { - SignalCompletion(); + stream.SignalCompletion(); } return read; } + } - public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - return buffer.Length == 0 - ? _inner.ReadAsync(buffer, offset, count, cancellationToken) - : ReadAsyncCore(this, _inner.ReadAsync(buffer, offset, count, cancellationToken)); - - static async Task ReadAsyncCore(EndOfStreamDetectingStream stream, Task readTask) - { - int read = await readTask; - - if (read == 0) - { - stream.SignalCompletion(); - } - - return read; - } - } - - protected override void Dispose(bool disposing) + protected override void Dispose(bool disposing) + { + if (disposing) { - if (disposing) - { - SignalCompletion(); + SignalCompletion(); - _inner.Dispose(); - } + _inner.Dispose(); } + } - private void SignalCompletion() + private void SignalCompletion() + { + if (Interlocked.Exchange(ref _sawEndOfStream, 1) == 0) { - if (Interlocked.Exchange(ref _sawEndOfStream, 1) == 0) - { - _onEndOfStream(); - } + _onEndOfStream(); } - - public override long Seek(long offset, SeekOrigin origin) => _inner.Seek(offset, origin); - public override void SetLength(long value) => _inner.SetLength(value); - public override void Write(byte[] buffer, int offset, int count) => _inner.Write(buffer, offset, count); - public override bool CanRead => _inner.CanRead; - public override bool CanSeek => _inner.CanSeek; - public override bool CanWrite => _inner.CanWrite; - public override long Length => _inner.Length; - public override long Position { get => _inner.Position; set => _inner.Position = value; } } + + public override long Seek(long offset, SeekOrigin origin) => _inner.Seek(offset, origin); + public override void SetLength(long value) => _inner.SetLength(value); + public override void Write(byte[] buffer, int offset, int count) => _inner.Write(buffer, offset, count); + public override bool CanRead => _inner.CanRead; + public override bool CanSeek => _inner.CanSeek; + public override bool CanWrite => _inner.CanWrite; + public override long Length => _inner.Length; + public override long Position { get => _inner.Position; set => _inner.Position = value; } } } \ No newline at end of file diff --git a/Prometheus/HttpClientMetrics/HttpClientResponseDurationOptions.cs b/Prometheus/HttpClientMetrics/HttpClientResponseDurationOptions.cs index 0bd2c68f..39f53bb5 100644 --- a/Prometheus/HttpClientMetrics/HttpClientResponseDurationOptions.cs +++ b/Prometheus/HttpClientMetrics/HttpClientResponseDurationOptions.cs @@ -1,10 +1,9 @@ -namespace Prometheus.HttpClientMetrics +namespace Prometheus.HttpClientMetrics; + +public sealed class HttpClientResponseDurationOptions : HttpClientMetricsOptionsBase { - public sealed class HttpClientResponseDurationOptions : HttpClientMetricsOptionsBase - { - /// - /// Set this to use a custom metric instead of the default. - /// - public ICollector? Histogram { get; set; } - } + /// + /// Set this to use a custom metric instead of the default. + /// + public ICollector? Histogram { get; set; } } \ No newline at end of file diff --git a/Prometheus/HttpClientMetricsExtensions.cs b/Prometheus/HttpClientMetricsExtensions.cs index ca0c22ab..ab529d0f 100644 --- a/Prometheus/HttpClientMetricsExtensions.cs +++ b/Prometheus/HttpClientMetricsExtensions.cs @@ -2,100 +2,99 @@ using Microsoft.Extensions.Http; using Prometheus.HttpClientMetrics; -namespace Prometheus +namespace Prometheus; + +public static class HttpClientMetricsExtensions { - public static class HttpClientMetricsExtensions + /// + /// Configures the HttpClient pipeline to collect Prometheus metrics. + /// + public static IHttpClientBuilder UseHttpClientMetrics(this IHttpClientBuilder builder, Action configure) { - /// - /// Configures the HttpClient pipeline to collect Prometheus metrics. - /// - public static IHttpClientBuilder UseHttpClientMetrics(this IHttpClientBuilder builder, Action configure) - { - var options = new HttpClientExporterOptions(); + var options = new HttpClientExporterOptions(); - configure?.Invoke(options); + configure?.Invoke(options); - builder.UseHttpClientMetrics(options); + builder.UseHttpClientMetrics(options); - return builder; - } - - /// - /// Configures the HttpClient pipeline to collect Prometheus metrics. - /// - public static IHttpClientBuilder UseHttpClientMetrics(this IHttpClientBuilder builder, HttpClientExporterOptions? options = null) - { - options ??= new HttpClientExporterOptions(); - - var identity = new HttpClientIdentity(builder.Name); + return builder; + } - if (options.InProgress.Enabled) - { - builder = builder.AddHttpMessageHandler(x => new HttpClientInProgressHandler(options.InProgress, identity)); - } + /// + /// Configures the HttpClient pipeline to collect Prometheus metrics. + /// + public static IHttpClientBuilder UseHttpClientMetrics(this IHttpClientBuilder builder, HttpClientExporterOptions? options = null) + { + options ??= new HttpClientExporterOptions(); - if (options.RequestCount.Enabled) - { - builder = builder.AddHttpMessageHandler(x => new HttpClientRequestCountHandler(options.RequestCount, identity)); - } + var identity = new HttpClientIdentity(builder.Name); - if (options.RequestDuration.Enabled) - { - builder = builder.AddHttpMessageHandler(x => new HttpClientRequestDurationHandler(options.RequestDuration, identity)); - } + if (options.InProgress.Enabled) + { + builder = builder.AddHttpMessageHandler(x => new HttpClientInProgressHandler(options.InProgress, identity)); + } - if (options.ResponseDuration.Enabled) - { - builder = builder.AddHttpMessageHandler(x => new HttpClientResponseDurationHandler(options.ResponseDuration, identity)); - } + if (options.RequestCount.Enabled) + { + builder = builder.AddHttpMessageHandler(x => new HttpClientRequestCountHandler(options.RequestCount, identity)); + } - return builder; + if (options.RequestDuration.Enabled) + { + builder = builder.AddHttpMessageHandler(x => new HttpClientRequestDurationHandler(options.RequestDuration, identity)); } - /// - /// Configures the HttpMessageHandler pipeline to collect Prometheus metrics. - /// - public static HttpMessageHandlerBuilder UseHttpClientMetrics(this HttpMessageHandlerBuilder builder, HttpClientExporterOptions? options = null) + if (options.ResponseDuration.Enabled) { - options ??= new HttpClientExporterOptions(); + builder = builder.AddHttpMessageHandler(x => new HttpClientResponseDurationHandler(options.ResponseDuration, identity)); + } - var identity = new HttpClientIdentity(builder.Name); + return builder; + } - if (options.InProgress.Enabled) - { - builder.AdditionalHandlers.Add(new HttpClientInProgressHandler(options.InProgress, identity)); - } + /// + /// Configures the HttpMessageHandler pipeline to collect Prometheus metrics. + /// + public static HttpMessageHandlerBuilder UseHttpClientMetrics(this HttpMessageHandlerBuilder builder, HttpClientExporterOptions? options = null) + { + options ??= new HttpClientExporterOptions(); - if (options.RequestCount.Enabled) - { - builder.AdditionalHandlers.Add(new HttpClientRequestCountHandler(options.RequestCount, identity)); - } + var identity = new HttpClientIdentity(builder.Name); - if (options.RequestDuration.Enabled) - { - builder.AdditionalHandlers.Add(new HttpClientRequestDurationHandler(options.RequestDuration, identity)); - } + if (options.InProgress.Enabled) + { + builder.AdditionalHandlers.Add(new HttpClientInProgressHandler(options.InProgress, identity)); + } - if (options.ResponseDuration.Enabled) - { - builder.AdditionalHandlers.Add(new HttpClientResponseDurationHandler(options.ResponseDuration, identity)); - } + if (options.RequestCount.Enabled) + { + builder.AdditionalHandlers.Add(new HttpClientRequestCountHandler(options.RequestCount, identity)); + } - return builder; + if (options.RequestDuration.Enabled) + { + builder.AdditionalHandlers.Add(new HttpClientRequestDurationHandler(options.RequestDuration, identity)); } - /// - /// Configures the service container to collect Prometheus metrics from all registered HttpClients. - /// - public static IServiceCollection UseHttpClientMetrics(this IServiceCollection services, HttpClientExporterOptions? options = null) + if (options.ResponseDuration.Enabled) { - return services.ConfigureAll((HttpClientFactoryOptions optionsToConfigure) => + builder.AdditionalHandlers.Add(new HttpClientResponseDurationHandler(options.ResponseDuration, identity)); + } + + return builder; + } + + /// + /// Configures the service container to collect Prometheus metrics from all registered HttpClients. + /// + public static IServiceCollection UseHttpClientMetrics(this IServiceCollection services, HttpClientExporterOptions? options = null) + { + return services.ConfigureAll((HttpClientFactoryOptions optionsToConfigure) => + { + optionsToConfigure.HttpMessageHandlerBuilderActions.Add(builder => { - optionsToConfigure.HttpMessageHandlerBuilderActions.Add(builder => - { - builder.UseHttpClientMetrics(options); - }); + builder.UseHttpClientMetrics(options); }); - } + }); } } \ No newline at end of file diff --git a/Prometheus/ICollector.cs b/Prometheus/ICollector.cs index ffc1a8e5..7fd7cf8d 100644 --- a/Prometheus/ICollector.cs +++ b/Prometheus/ICollector.cs @@ -1,22 +1,21 @@ -namespace Prometheus +namespace Prometheus; + +/// +/// Child-type-specific interface implemented by all collectors, used to enable substitution in test code. +/// +public interface ICollector : ICollector + where TChild : ICollectorChild { - /// - /// Child-type-specific interface implemented by all collectors, used to enable substitution in test code. - /// - public interface ICollector : ICollector - where TChild : ICollectorChild - { - TChild Unlabelled { get; } - TChild WithLabels(params string[] labelValues); - } + TChild Unlabelled { get; } + TChild WithLabels(params string[] labelValues); +} - /// - /// Interface implemented by all collectors, used to enable substitution in test code. - /// - public interface ICollector - { - string Name { get; } - string Help { get; } - string[] LabelNames { get; } - } +/// +/// Interface implemented by all collectors, used to enable substitution in test code. +/// +public interface ICollector +{ + string Name { get; } + string Help { get; } + string[] LabelNames { get; } } diff --git a/Prometheus/ICollectorChild.cs b/Prometheus/ICollectorChild.cs index 422ea653..c7dc32e6 100644 --- a/Prometheus/ICollectorChild.cs +++ b/Prometheus/ICollectorChild.cs @@ -1,9 +1,8 @@ -namespace Prometheus +namespace Prometheus; + +/// +/// Interface shared by all labelled collector children. +/// +public interface ICollectorChild { - /// - /// Interface shared by all labelled collector children. - /// - public interface ICollectorChild - { - } } diff --git a/Prometheus/ICollectorRegistry.cs b/Prometheus/ICollectorRegistry.cs index 24163f4c..bca0bf06 100644 --- a/Prometheus/ICollectorRegistry.cs +++ b/Prometheus/ICollectorRegistry.cs @@ -1,17 +1,16 @@ -namespace Prometheus +namespace Prometheus; + +/// +/// Allows for substitution of CollectorRegistry in tests. +/// Not used by prometheus-net itself - you cannot provide your own implementation to prometheus-net code, only to your own code. +/// +public interface ICollectorRegistry { - /// - /// Allows for substitution of CollectorRegistry in tests. - /// Not used by prometheus-net itself - you cannot provide your own implementation to prometheus-net code, only to your own code. - /// - public interface ICollectorRegistry - { - void AddBeforeCollectCallback(Action callback); - void AddBeforeCollectCallback(Func callback); + void AddBeforeCollectCallback(Action callback); + void AddBeforeCollectCallback(Func callback); - IEnumerable> StaticLabels { get; } - void SetStaticLabels(IDictionary labels); + IEnumerable> StaticLabels { get; } + void SetStaticLabels(IDictionary labels); - Task CollectAndExportAsTextAsync(Stream to, ExpositionFormat format = ExpositionFormat.PrometheusText, CancellationToken cancel = default); - } + Task CollectAndExportAsTextAsync(Stream to, ExpositionFormat format = ExpositionFormat.PrometheusText, CancellationToken cancel = default); } diff --git a/Prometheus/IDelayer.cs b/Prometheus/IDelayer.cs index c25a5bf9..7c121534 100644 --- a/Prometheus/IDelayer.cs +++ b/Prometheus/IDelayer.cs @@ -1,11 +1,10 @@ -namespace Prometheus +namespace Prometheus; + +/// +/// Abstraction over Task.Delay() to allow custom delay logic to be injected in tests. +/// +internal interface IDelayer { - /// - /// Abstraction over Task.Delay() to allow custom delay logic to be injected in tests. - /// - internal interface IDelayer - { - Task Delay(TimeSpan duration); - Task Delay(TimeSpan duration, CancellationToken cancel); - } + Task Delay(TimeSpan duration); + Task Delay(TimeSpan duration, CancellationToken cancel); } diff --git a/Prometheus/IGauge.cs b/Prometheus/IGauge.cs index ac5c50fd..2e7ebda7 100644 --- a/Prometheus/IGauge.cs +++ b/Prometheus/IGauge.cs @@ -1,12 +1,11 @@ -namespace Prometheus +namespace Prometheus; + +public interface IGauge : ICollectorChild { - public interface IGauge : ICollectorChild - { - void Inc(double increment = 1); - void Set(double val); - void Dec(double decrement = 1); - void IncTo(double targetValue); - void DecTo(double targetValue); - double Value { get; } - } + void Inc(double increment = 1); + void Set(double val); + void Dec(double decrement = 1); + void IncTo(double targetValue); + void DecTo(double targetValue); + double Value { get; } } diff --git a/Prometheus/IMetricFactory.cs b/Prometheus/IMetricFactory.cs index 442771c3..b58fde35 100644 --- a/Prometheus/IMetricFactory.cs +++ b/Prometheus/IMetricFactory.cs @@ -1,49 +1,48 @@ using System.ComponentModel; -namespace Prometheus +namespace Prometheus; + +/// +/// Allows for substitution of MetricFactory in tests. +/// You cannot provide your own implementation to prometheus-net code, only to your own code. +/// +public interface IMetricFactory { - /// - /// Allows for substitution of MetricFactory in tests. - /// You cannot provide your own implementation to prometheus-net code, only to your own code. - /// - public interface IMetricFactory - { - // These require you to allocate a Configuration for each instance, which can be wasteful because often the only thing that differs is the label names. - // We will mark them as non-browsable to discourage their use. They still work, so they are not obsolete or anything like that. Just discouraged. - [EditorBrowsable(EditorBrowsableState.Never)] - Counter CreateCounter(string name, string help, CounterConfiguration? configuration = null); - [EditorBrowsable(EditorBrowsableState.Never)] - Gauge CreateGauge(string name, string help, GaugeConfiguration? configuration = null); - [EditorBrowsable(EditorBrowsableState.Never)] - Histogram CreateHistogram(string name, string help, HistogramConfiguration? configuration = null); - [EditorBrowsable(EditorBrowsableState.Never)] - Summary CreateSummary(string name, string help, SummaryConfiguration? configuration = null); + // These require you to allocate a Configuration for each instance, which can be wasteful because often the only thing that differs is the label names. + // We will mark them as non-browsable to discourage their use. They still work, so they are not obsolete or anything like that. Just discouraged. + [EditorBrowsable(EditorBrowsableState.Never)] + Counter CreateCounter(string name, string help, CounterConfiguration? configuration = null); + [EditorBrowsable(EditorBrowsableState.Never)] + Gauge CreateGauge(string name, string help, GaugeConfiguration? configuration = null); + [EditorBrowsable(EditorBrowsableState.Never)] + Histogram CreateHistogram(string name, string help, HistogramConfiguration? configuration = null); + [EditorBrowsable(EditorBrowsableState.Never)] + Summary CreateSummary(string name, string help, SummaryConfiguration? configuration = null); - // These allow you to reuse a Configuration and only provide the label names. The reduced memory allocations can make a difference in high performance scenarios. - // If label names are provided in both, they must match. Otherwise, label names in the Configuration object may be null. - Counter CreateCounter(string name, string help, string[] labelNames, CounterConfiguration? configuration = null); - Gauge CreateGauge(string name, string help, string[] labelNames, GaugeConfiguration? configuration = null); - Histogram CreateHistogram(string name, string help, string[] labelNames, HistogramConfiguration? configuration = null); - Summary CreateSummary(string name, string help, string[] labelNames, SummaryConfiguration? configuration = null); + // These allow you to reuse a Configuration and only provide the label names. The reduced memory allocations can make a difference in high performance scenarios. + // If label names are provided in both, they must match. Otherwise, label names in the Configuration object may be null. + Counter CreateCounter(string name, string help, string[] labelNames, CounterConfiguration? configuration = null); + Gauge CreateGauge(string name, string help, string[] labelNames, GaugeConfiguration? configuration = null); + Histogram CreateHistogram(string name, string help, string[] labelNames, HistogramConfiguration? configuration = null); + Summary CreateSummary(string name, string help, string[] labelNames, SummaryConfiguration? configuration = null); - /// - /// Returns a new metric factory that will add the specified labels to any metrics created using it. - /// - IMetricFactory WithLabels(IDictionary labels); + /// + /// Returns a new metric factory that will add the specified labels to any metrics created using it. + /// + IMetricFactory WithLabels(IDictionary labels); - /// - /// Returns a factory that creates metrics with a managed lifetime. - /// - /// - /// Metrics created from this factory will expire after this time span elapses, enabling automatic deletion of unused metrics. - /// The expiration timer is reset to zero for the duration of any active lifetime-extension lease that is taken on a specific metric. - /// - IManagedLifetimeMetricFactory WithManagedLifetime(TimeSpan expiresAfter); + /// + /// Returns a factory that creates metrics with a managed lifetime. + /// + /// + /// Metrics created from this factory will expire after this time span elapses, enabling automatic deletion of unused metrics. + /// The expiration timer is reset to zero for the duration of any active lifetime-extension lease that is taken on a specific metric. + /// + IManagedLifetimeMetricFactory WithManagedLifetime(TimeSpan expiresAfter); - /// - /// Allows you to configure how exemplars are applied to published metrics. If null, uses default behavior (see ). - /// This is inherited by all metrics by default, although may be overridden in the configuration of an individual metric. - /// - ExemplarBehavior? ExemplarBehavior { get; set; } - } + /// + /// Allows you to configure how exemplars are applied to published metrics. If null, uses default behavior (see ). + /// This is inherited by all metrics by default, although may be overridden in the configuration of an individual metric. + /// + ExemplarBehavior? ExemplarBehavior { get; set; } } diff --git a/Prometheus/IMetricServer.cs b/Prometheus/IMetricServer.cs index be038fc3..34652a49 100644 --- a/Prometheus/IMetricServer.cs +++ b/Prometheus/IMetricServer.cs @@ -1,26 +1,25 @@ -namespace Prometheus +namespace Prometheus; + +/// +/// A metric server exposes a Prometheus metric exporter endpoint in the background, +/// operating independently and serving metrics until it is instructed to stop. +/// +public interface IMetricServer : IDisposable { /// - /// A metric server exposes a Prometheus metric exporter endpoint in the background, - /// operating independently and serving metrics until it is instructed to stop. + /// Starts serving metrics. + /// + /// Returns the same instance that was called (for fluent-API-style chaining). /// - public interface IMetricServer : IDisposable - { - /// - /// Starts serving metrics. - /// - /// Returns the same instance that was called (for fluent-API-style chaining). - /// - IMetricServer Start(); + IMetricServer Start(); - /// - /// Instructs the metric server to stop and returns a task you can await for it to stop. - /// - Task StopAsync(); + /// + /// Instructs the metric server to stop and returns a task you can await for it to stop. + /// + Task StopAsync(); - /// - /// Instructs the metric server to stop and waits for it to stop. - /// - void Stop(); - } + /// + /// Instructs the metric server to stop and waits for it to stop. + /// + void Stop(); } diff --git a/Prometheus/IObserver.cs b/Prometheus/IObserver.cs index 70b70393..a5ffd912 100644 --- a/Prometheus/IObserver.cs +++ b/Prometheus/IObserver.cs @@ -1,13 +1,12 @@ -namespace Prometheus +namespace Prometheus; + +/// +/// Implemented by metric types that observe individual events with specific values. +/// +public interface IObserver : ICollectorChild { /// - /// Implemented by metric types that observe individual events with specific values. + /// Observes a single event with the given value. /// - public interface IObserver : ICollectorChild - { - /// - /// Observes a single event with the given value. - /// - void Observe(double val); - } + void Observe(double val); } \ No newline at end of file diff --git a/Prometheus/ISummary.cs b/Prometheus/ISummary.cs index aeed5840..a775d53c 100644 --- a/Prometheus/ISummary.cs +++ b/Prometheus/ISummary.cs @@ -1,6 +1,5 @@ -namespace Prometheus +namespace Prometheus; + +public interface ISummary : IObserver { - public interface ISummary : IObserver - { - } } diff --git a/Prometheus/ITimer.cs b/Prometheus/ITimer.cs index 922d343a..3334e836 100644 --- a/Prometheus/ITimer.cs +++ b/Prometheus/ITimer.cs @@ -1,15 +1,14 @@ -namespace Prometheus +namespace Prometheus; + +/// +/// A timer that can be used to observe a duration of elapsed time. +/// +/// The observation is made either when ObserveDuration is called or when the instance is disposed of. +/// +public interface ITimer : IDisposable { /// - /// A timer that can be used to observe a duration of elapsed time. - /// - /// The observation is made either when ObserveDuration is called or when the instance is disposed of. + /// Observes the duration (in seconds) and returns the observed value. /// - public interface ITimer : IDisposable - { - /// - /// Observes the duration (in seconds) and returns the observed value. - /// - TimeSpan ObserveDuration(); - } + TimeSpan ObserveDuration(); } diff --git a/Prometheus/MetricPusher.cs b/Prometheus/MetricPusher.cs index 601b926c..da9d0a1c 100644 --- a/Prometheus/MetricPusher.cs +++ b/Prometheus/MetricPusher.cs @@ -56,10 +56,9 @@ public MetricPusher(MetricPusherOptions options) } } - Uri? targetUrl; - if (!Uri.TryCreate(sb.ToString(), UriKind.Absolute, out targetUrl) || targetUrl == null) + if (!Uri.TryCreate(sb.ToString(), UriKind.Absolute, out var targetUrl) || targetUrl == null) { - throw new ArgumentException("Endpoint must be a valid url", "endpoint"); + throw new ArgumentException("Endpoint must be a valid url", nameof(options.Endpoint)); } _targetUrl = targetUrl; @@ -70,7 +69,7 @@ public MetricPusher(MetricPusherOptions options) _method = options.ReplaceOnPush ? HttpMethod.Put : HttpMethod.Post; } - private static readonly HttpClient _singletonHttpClient = new HttpClient(); + private static readonly HttpClient _singletonHttpClient = new(); private readonly CollectorRegistry _registry; private readonly Action? _onError; diff --git a/Prometheus/MetricPusherOptions.cs b/Prometheus/MetricPusherOptions.cs index 72794436..c9214af2 100644 --- a/Prometheus/MetricPusherOptions.cs +++ b/Prometheus/MetricPusherOptions.cs @@ -1,33 +1,32 @@ -namespace Prometheus +namespace Prometheus; + +public sealed class MetricPusherOptions { - public sealed class MetricPusherOptions - { - internal static readonly MetricPusherOptions Default = new MetricPusherOptions(); + internal static readonly MetricPusherOptions Default = new(); - public string? Endpoint { get; set; } - public string? Job { get; set; } - public string? Instance { get; set; } - public long IntervalMilliseconds { get; set; } = 1000; - public IEnumerable>? AdditionalLabels { get; set; } - public CollectorRegistry? Registry { get; set; } + public string? Endpoint { get; set; } + public string? Job { get; set; } + public string? Instance { get; set; } + public long IntervalMilliseconds { get; set; } = 1000; + public IEnumerable>? AdditionalLabels { get; set; } + public CollectorRegistry? Registry { get; set; } - /// - /// Callback for when a metric push fails. - /// - public Action? OnError { get; set; } + /// + /// Callback for when a metric push fails. + /// + public Action? OnError { get; set; } - /// - /// If null, a singleton HttpClient will be used. - /// - public Func? HttpClientProvider { get; set; } + /// + /// If null, a singleton HttpClient will be used. + /// + public Func? HttpClientProvider { get; set; } - /// - /// If true, replace the metrics in the group (identified by Job, Instance, AdditionalLabels). - /// - /// Replace means a HTTP PUT request will be made, otherwise a HTTP POST request will be made (which means add metrics to the group, if it already exists). - /// - /// Note: Other implementations of the pushgateway client default to replace, however to preserve backwards compatibility this implementation defaults to add. - /// - public bool ReplaceOnPush { get; set; } = false; - } + /// + /// If true, replace the metrics in the group (identified by Job, Instance, AdditionalLabels). + /// + /// Replace means a HTTP PUT request will be made, otherwise a HTTP POST request will be made (which means add metrics to the group, if it already exists). + /// + /// Note: Other implementations of the pushgateway client default to replace, however to preserve backwards compatibility this implementation defaults to add. + /// + public bool ReplaceOnPush { get; set; } = false; } diff --git a/Prometheus/MetricServer.cs b/Prometheus/MetricServer.cs index 405a7326..7b28eb4d 100644 --- a/Prometheus/MetricServer.cs +++ b/Prometheus/MetricServer.cs @@ -9,7 +9,7 @@ namespace Prometheus; /// public class MetricServer : MetricHandler { - private readonly HttpListener _httpListener = new HttpListener(); + private readonly HttpListener _httpListener = new(); /// /// Only requests that match this predicate will be served by the metric server. This allows you to add authorization checks. diff --git a/Prometheus/MetricType.cs b/Prometheus/MetricType.cs index 75ca46a9..aa844296 100644 --- a/Prometheus/MetricType.cs +++ b/Prometheus/MetricType.cs @@ -1,10 +1,9 @@ -namespace Prometheus +namespace Prometheus; + +internal enum MetricType { - internal enum MetricType - { - Counter, - Gauge, - Summary, - Histogram - } + Counter, + Gauge, + Summary, + Histogram } diff --git a/Prometheus/Metrics.cs b/Prometheus/Metrics.cs index 96acc858..41851cf4 100644 --- a/Prometheus/Metrics.cs +++ b/Prometheus/Metrics.cs @@ -1,180 +1,178 @@ -namespace Prometheus +namespace Prometheus; + +/// +/// Static class for easy creation of metrics. Acts as the entry point to the prometheus-net metrics recording API. +/// +/// Some built-in metrics are registered by default in the default collector registry. If these default metrics are +/// not desired, call to remove them before registering your own. +/// +public static class Metrics { /// - /// Static class for easy creation of metrics. Acts as the entry point to the prometheus-net metrics recording API. - /// - /// Some built-in metrics are registered by default in the default collector registry. If these default metrics are - /// not desired, call to remove them before registering your own. + /// The default registry where all metrics are registered by default. /// - public static class Metrics + public static CollectorRegistry DefaultRegistry { get; private set; } + + /// + /// The default metric factory used to create collectors in the default registry. + /// + public static MetricFactory DefaultFactory { get; private set; } + + /// + /// Creates a new registry. You may want to use multiple registries if you want to + /// export different sets of metrics via different exporters (e.g. on different URLs). + /// + public static CollectorRegistry NewCustomRegistry() => new(); + + /// + /// Returns an instance of that you can use to register metrics in a custom registry. + /// + public static MetricFactory WithCustomRegistry(CollectorRegistry registry) => new(registry); + + /// + /// Adds the specified static labels to all metrics created using the returned factory. + /// + public static IMetricFactory WithLabels(IDictionary labels) => + new MetricFactory(DefaultRegistry, LabelSequence.From(labels)); + + /// + /// Returns a factory that creates metrics with a managed lifetime. + /// + /// + /// Metrics created from this factory will expire after this time span elapses, enabling automatic deletion of unused metrics. + /// The expiration timer is reset to zero for the duration of any active lifetime-extension lease that is taken on a specific metric. + /// + public static IManagedLifetimeMetricFactory WithManagedLifetime(TimeSpan expiresAfter) => + DefaultFactory.WithManagedLifetime(expiresAfter); + + /// + /// Counters only increase in value and reset to zero when the process restarts. + /// + public static Counter CreateCounter(string name, string help, CounterConfiguration? configuration = null) => + DefaultFactory.CreateCounter(name, help, configuration); + + /// + /// Gauges can have any numeric value and change arbitrarily. + /// + public static Gauge CreateGauge(string name, string help, GaugeConfiguration? configuration = null) => + DefaultFactory.CreateGauge(name, help, configuration); + + /// + /// Summaries track the trends in events over time (10 minutes by default). + /// + public static Summary CreateSummary(string name, string help, SummaryConfiguration? configuration = null) => + DefaultFactory.CreateSummary(name, help, configuration); + + /// + /// Histograms track the size and number of events in buckets. + /// + public static Histogram CreateHistogram(string name, string help, HistogramConfiguration? configuration = null) => + DefaultFactory.CreateHistogram(name, help, configuration); + + /// + /// Counters only increase in value and reset to zero when the process restarts. + /// + public static Counter CreateCounter(string name, string help, string[] labelNames, CounterConfiguration? configuration = null) => + DefaultFactory.CreateCounter(name, help, labelNames, configuration); + + /// + /// Gauges can have any numeric value and change arbitrarily. + /// + public static Gauge CreateGauge(string name, string help, string[] labelNames, GaugeConfiguration? configuration = null) => + DefaultFactory.CreateGauge(name, help, labelNames, configuration); + + /// + /// Summaries track the trends in events over time (10 minutes by default). + /// + public static Summary CreateSummary(string name, string help, string[] labelNames, SummaryConfiguration? configuration = null) => + DefaultFactory.CreateSummary(name, help, labelNames, configuration); + + /// + /// Histograms track the size and number of events in buckets. + /// + public static Histogram CreateHistogram(string name, string help, string[] labelNames, HistogramConfiguration? configuration = null) => + DefaultFactory.CreateHistogram(name, help, labelNames, configuration); + + /// + /// Counters only increase in value and reset to zero when the process restarts. + /// + public static Counter CreateCounter(string name, string help, params string[] labelNames) => + DefaultFactory.CreateCounter(name, help, labelNames); + + /// + /// Gauges can have any numeric value and change arbitrarily. + /// + public static Gauge CreateGauge(string name, string help, params string[] labelNames) => + DefaultFactory.CreateGauge(name, help, labelNames); + + /// + /// Summaries track the trends in events over time (10 minutes by default). + /// + public static Summary CreateSummary(string name, string help, params string[] labelNames) => + DefaultFactory.CreateSummary(name, help, labelNames); + + /// + /// Histograms track the size and number of events in buckets. + /// + public static Histogram CreateHistogram(string name, string help, params string[] labelNames) => + DefaultFactory.CreateHistogram(name, help, labelNames); + + static Metrics() { - /// - /// The default registry where all metrics are registered by default. - /// - public static CollectorRegistry DefaultRegistry { get; private set; } - - /// - /// The default metric factory used to create collectors in the default registry. - /// - public static MetricFactory DefaultFactory { get; private set; } - - /// - /// Creates a new registry. You may want to use multiple registries if you want to - /// export different sets of metrics via different exporters (e.g. on different URLs). - /// - public static CollectorRegistry NewCustomRegistry() => new CollectorRegistry(); - - /// - /// Returns an instance of that you can use to register metrics in a custom registry. - /// - public static MetricFactory WithCustomRegistry(CollectorRegistry registry) => - new MetricFactory(registry); - - /// - /// Adds the specified static labels to all metrics created using the returned factory. - /// - public static IMetricFactory WithLabels(IDictionary labels) => - new MetricFactory(DefaultRegistry, LabelSequence.From(labels)); - - /// - /// Returns a factory that creates metrics with a managed lifetime. - /// - /// - /// Metrics created from this factory will expire after this time span elapses, enabling automatic deletion of unused metrics. - /// The expiration timer is reset to zero for the duration of any active lifetime-extension lease that is taken on a specific metric. - /// - public static IManagedLifetimeMetricFactory WithManagedLifetime(TimeSpan expiresAfter) => - DefaultFactory.WithManagedLifetime(expiresAfter); - - /// - /// Counters only increase in value and reset to zero when the process restarts. - /// - public static Counter CreateCounter(string name, string help, CounterConfiguration? configuration = null) => - DefaultFactory.CreateCounter(name, help, configuration); - - /// - /// Gauges can have any numeric value and change arbitrarily. - /// - public static Gauge CreateGauge(string name, string help, GaugeConfiguration? configuration = null) => - DefaultFactory.CreateGauge(name, help, configuration); - - /// - /// Summaries track the trends in events over time (10 minutes by default). - /// - public static Summary CreateSummary(string name, string help, SummaryConfiguration? configuration = null) => - DefaultFactory.CreateSummary(name, help, configuration); - - /// - /// Histograms track the size and number of events in buckets. - /// - public static Histogram CreateHistogram(string name, string help, HistogramConfiguration? configuration = null) => - DefaultFactory.CreateHistogram(name, help, configuration); - - /// - /// Counters only increase in value and reset to zero when the process restarts. - /// - public static Counter CreateCounter(string name, string help, string[] labelNames, CounterConfiguration? configuration = null) => - DefaultFactory.CreateCounter(name, help, labelNames, configuration); - - /// - /// Gauges can have any numeric value and change arbitrarily. - /// - public static Gauge CreateGauge(string name, string help, string[] labelNames, GaugeConfiguration? configuration = null) => - DefaultFactory.CreateGauge(name, help, labelNames, configuration); - - /// - /// Summaries track the trends in events over time (10 minutes by default). - /// - public static Summary CreateSummary(string name, string help, string[] labelNames, SummaryConfiguration? configuration = null) => - DefaultFactory.CreateSummary(name, help, labelNames, configuration); - - /// - /// Histograms track the size and number of events in buckets. - /// - public static Histogram CreateHistogram(string name, string help, string[] labelNames, HistogramConfiguration? configuration = null) => - DefaultFactory.CreateHistogram(name, help, labelNames, configuration); - - /// - /// Counters only increase in value and reset to zero when the process restarts. - /// - public static Counter CreateCounter(string name, string help, params string[] labelNames) => - DefaultFactory.CreateCounter(name, help, labelNames); - - /// - /// Gauges can have any numeric value and change arbitrarily. - /// - public static Gauge CreateGauge(string name, string help, params string[] labelNames) => - DefaultFactory.CreateGauge(name, help, labelNames); - - /// - /// Summaries track the trends in events over time (10 minutes by default). - /// - public static Summary CreateSummary(string name, string help, params string[] labelNames) => - DefaultFactory.CreateSummary(name, help, labelNames); - - /// - /// Histograms track the size and number of events in buckets. - /// - public static Histogram CreateHistogram(string name, string help, params string[] labelNames) => - DefaultFactory.CreateHistogram(name, help, labelNames); - - static Metrics() - { - DefaultRegistry = new CollectorRegistry(); + DefaultRegistry = new CollectorRegistry(); - // Configures defaults to their default behaviors, can be overridden by user if they desire (before first collection). - SuppressDefaultMetrics(SuppressDefaultMetricOptions.SuppressNone); + // Configures defaults to their default behaviors, can be overridden by user if they desire (before first collection). + SuppressDefaultMetrics(SuppressDefaultMetricOptions.SuppressNone); - DefaultFactory = new MetricFactory(DefaultRegistry); - } + DefaultFactory = new MetricFactory(DefaultRegistry); + } - /// - /// Suppresses the registration of the default sample metrics from the default registry. - /// Has no effect if not called on startup (it will not remove metrics from a registry already in use). - /// - public static void SuppressDefaultMetrics() => SuppressDefaultMetrics(SuppressDefaultMetricOptions.SuppressAll); + /// + /// Suppresses the registration of the default sample metrics from the default registry. + /// Has no effect if not called on startup (it will not remove metrics from a registry already in use). + /// + public static void SuppressDefaultMetrics() => SuppressDefaultMetrics(SuppressDefaultMetricOptions.SuppressAll); - /// - /// Suppresses the registration of the default sample metrics from the default registry. - /// Has no effect if not called on startup (it will not remove metrics from a registry already in use). - /// - public static void SuppressDefaultMetrics(SuppressDefaultMetricOptions options) - { - options ??= SuppressDefaultMetricOptions.SuppressAll; + /// + /// Suppresses the registration of the default sample metrics from the default registry. + /// Has no effect if not called on startup (it will not remove metrics from a registry already in use). + /// + public static void SuppressDefaultMetrics(SuppressDefaultMetricOptions options) + { + options ??= SuppressDefaultMetricOptions.SuppressAll; - // Only has effect if called before the registry is collected from. Otherwise a no-op. - DefaultRegistry.SetBeforeFirstCollectCallback(delegate + // Only has effect if called before the registry is collected from. Otherwise a no-op. + DefaultRegistry.SetBeforeFirstCollectCallback(delegate + { + var configureCallbacks = new SuppressDefaultMetricOptions.ConfigurationCallbacks() { - var configureCallbacks = new SuppressDefaultMetricOptions.ConfigurationCallbacks() - { #if NET - ConfigureEventCounterAdapter = _configureEventCounterAdapterCallback, + ConfigureEventCounterAdapter = _configureEventCounterAdapterCallback, #endif #if NET6_0_OR_GREATER - ConfigureMeterAdapter = _configureMeterAdapterOptions + ConfigureMeterAdapter = _configureMeterAdapterOptions #endif - }; + }; - options.ApplyToDefaultRegistry(configureCallbacks); - }); - } + options.ApplyToDefaultRegistry(configureCallbacks); + }); + } #if NET - private static Action _configureEventCounterAdapterCallback = delegate { }; + private static Action _configureEventCounterAdapterCallback = delegate { }; - /// - /// Configures the event counter adapter that is enabled by default on startup. - /// - public static void ConfigureEventCounterAdapter(Action callback) => _configureEventCounterAdapterCallback = callback; + /// + /// Configures the event counter adapter that is enabled by default on startup. + /// + public static void ConfigureEventCounterAdapter(Action callback) => _configureEventCounterAdapterCallback = callback; #endif #if NET6_0_OR_GREATER - private static Action _configureMeterAdapterOptions = delegate { }; + private static Action _configureMeterAdapterOptions = delegate { }; - /// - /// Configures the meter adapter that is enabled by default on startup. - /// - public static void ConfigureMeterAdapter(Action callback) => _configureMeterAdapterOptions = callback; + /// + /// Configures the meter adapter that is enabled by default on startup. + /// + public static void ConfigureMeterAdapter(Action callback) => _configureMeterAdapterOptions = callback; #endif - } } \ No newline at end of file diff --git a/Prometheus/PrometheusNameHelpers.cs b/Prometheus/PrometheusNameHelpers.cs index 92d91451..704ca57b 100644 --- a/Prometheus/PrometheusNameHelpers.cs +++ b/Prometheus/PrometheusNameHelpers.cs @@ -1,58 +1,57 @@ using System.Text; using System.Text.RegularExpressions; -namespace Prometheus +namespace Prometheus; + +/// +/// Transforms external names in different character sets into Prometheus (metric or label) names. +/// +internal static class PrometheusNameHelpers { - /// - /// Transforms external names in different character sets into Prometheus (metric or label) names. - /// - internal static class PrometheusNameHelpers + private static readonly Regex NameRegex = new("^[a-zA-Z_][a-zA-Z0-9_]*$", RegexOptions.Compiled); + private const string FirstCharacterCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_"; + private const string NonFirstCharacterCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789"; + + public static string TranslateNameToPrometheusName(string inputName) { - private static readonly Regex NameRegex = new Regex("^[a-zA-Z_][a-zA-Z0-9_]*$", RegexOptions.Compiled); - private const string FirstCharacterCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_"; - private const string NonFirstCharacterCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_0123456789"; + // Transformations done: + // * all lowercase + // * special characters to underscore + // * must match: [a-zA-Z_][a-zA-Z0-9_]* + // * colon is "permitted" by spec but reserved for recording rules - public static string TranslateNameToPrometheusName(string inputName) - { - // Transformations done: - // * all lowercase - // * special characters to underscore - // * must match: [a-zA-Z_][a-zA-Z0-9_]* - // * colon is "permitted" by spec but reserved for recording rules + var sb = new StringBuilder(); - var sb = new StringBuilder(); + foreach (char inputCharacter in inputName) + { + // All lowercase. + var c = Char.ToLowerInvariant(inputCharacter); - foreach (char inputCharacter in inputName) + if (sb.Length == 0) { - // All lowercase. - var c = Char.ToLowerInvariant(inputCharacter); - - if (sb.Length == 0) - { - // If first character is not from allowed charset, prefix it with underscore to minimize first character data loss. - if (!FirstCharacterCharset.Contains(c)) - sb.Append('_'); + // If first character is not from allowed charset, prefix it with underscore to minimize first character data loss. + if (!FirstCharacterCharset.Contains(c)) + sb.Append('_'); - sb.Append(c); - } + sb.Append(c); + } + else + { + // Standard rules. + // If character is not permitted, replace with underscore. Simple as that! + if (!NonFirstCharacterCharset.Contains(c)) + sb.Append('_'); else - { - // Standard rules. - // If character is not permitted, replace with underscore. Simple as that! - if (!NonFirstCharacterCharset.Contains(c)) - sb.Append('_'); - else - sb.Append(c); - } + sb.Append(c); } + } - var name = sb.ToString(); + var name = sb.ToString(); - // Sanity check. - if (!NameRegex.IsMatch(name)) - throw new Exception("Self-check failed: generated name did not match our own naming rules."); + // Sanity check. + if (!NameRegex.IsMatch(name)) + throw new Exception("Self-check failed: generated name did not match our own naming rules."); - return name; - } + return name; } } diff --git a/Prometheus/PushStreamContentInternal.cs b/Prometheus/PushStreamContentInternal.cs index 31f96070..eeb1da03 100644 --- a/Prometheus/PushStreamContentInternal.cs +++ b/Prometheus/PushStreamContentInternal.cs @@ -6,80 +6,79 @@ using System.Net; using System.Net.Http.Headers; -namespace Prometheus +namespace Prometheus; + +/// +/// Provides an implementation that exposes an output +/// which can be written to directly. The ability to push data to the output stream differs from the +/// where data is pulled and not pushed. +/// +sealed class PushStreamContentInternal : HttpContent { + private readonly Func _onStreamAvailable; + + private static readonly MediaTypeHeaderValue OctetStreamHeaderValue = MediaTypeHeaderValue.Parse("application/octet-stream"); + /// - /// Provides an implementation that exposes an output - /// which can be written to directly. The ability to push data to the output stream differs from the - /// where data is pulled and not pushed. + /// Initializes a new instance of the class with the given . /// - sealed class PushStreamContentInternal : HttpContent + public PushStreamContentInternal(Func onStreamAvailable, MediaTypeHeaderValue mediaType) { - private readonly Func _onStreamAvailable; + _onStreamAvailable = onStreamAvailable; + Headers.ContentType = mediaType ?? OctetStreamHeaderValue; + } - private static readonly MediaTypeHeaderValue OctetStreamHeaderValue = MediaTypeHeaderValue.Parse("application/octet-stream"); + /// + /// When this method is called, it calls the action provided in the constructor with the output + /// stream to write to. Once the action has completed its work it closes the stream which will + /// close this content instance and complete the HTTP request or response. + /// + /// The to which to write. + /// The associated . + /// A instance that is asynchronously serializing the object's content. + [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception is passed as task result.")] + protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context) + { + TaskCompletionSource serializeToStreamTask = new TaskCompletionSource(); - /// - /// Initializes a new instance of the class with the given . - /// - public PushStreamContentInternal(Func onStreamAvailable, MediaTypeHeaderValue mediaType) - { - _onStreamAvailable = onStreamAvailable; - Headers.ContentType = mediaType ?? OctetStreamHeaderValue; - } + Stream wrappedStream = new CompleteTaskOnCloseStream(stream, serializeToStreamTask); + await _onStreamAvailable(wrappedStream, this, context); - /// - /// When this method is called, it calls the action provided in the constructor with the output - /// stream to write to. Once the action has completed its work it closes the stream which will - /// close this content instance and complete the HTTP request or response. - /// - /// The to which to write. - /// The associated . - /// A instance that is asynchronously serializing the object's content. - [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception is passed as task result.")] - protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context) - { - TaskCompletionSource serializeToStreamTask = new TaskCompletionSource(); + // wait for wrappedStream.Close/Dispose to get called. + await serializeToStreamTask.Task; + } - Stream wrappedStream = new CompleteTaskOnCloseStream(stream, serializeToStreamTask); - await _onStreamAvailable(wrappedStream, this, context); + /// + /// Computes the length of the stream if possible. + /// + /// The computed length of the stream. + /// true if the length has been computed; otherwise false. + protected override bool TryComputeLength(out long length) + { + // We can't know the length of the content being pushed to the output stream. + length = -1; + return false; + } - // wait for wrappedStream.Close/Dispose to get called. - await serializeToStreamTask.Task; - } + internal class CompleteTaskOnCloseStream : DelegatingStreamInternal + { + private TaskCompletionSource _serializeToStreamTask; - /// - /// Computes the length of the stream if possible. - /// - /// The computed length of the stream. - /// true if the length has been computed; otherwise false. - protected override bool TryComputeLength(out long length) + public CompleteTaskOnCloseStream(Stream innerStream, TaskCompletionSource serializeToStreamTask) + : base(innerStream) { - // We can't know the length of the content being pushed to the output stream. - length = -1; - return false; + _serializeToStreamTask = serializeToStreamTask; } - internal class CompleteTaskOnCloseStream : DelegatingStreamInternal + [SuppressMessage( + "Microsoft.Usage", + "CA2215:Dispose methods should call base class dispose", + Justification = "See comments, this is intentional.")] + protected override void Dispose(bool disposing) { - private TaskCompletionSource _serializeToStreamTask; - - public CompleteTaskOnCloseStream(Stream innerStream, TaskCompletionSource serializeToStreamTask) - : base(innerStream) - { - _serializeToStreamTask = serializeToStreamTask; - } - - [SuppressMessage( - "Microsoft.Usage", - "CA2215:Dispose methods should call base class dispose", - Justification = "See comments, this is intentional.")] - protected override void Dispose(bool disposing) - { - // We don't dispose the underlying stream because we don't own it. Dispose in this case just signifies - // that the user's action is finished. - _serializeToStreamTask.TrySetResult(true); - } + // We don't dispose the underlying stream because we don't own it. Dispose in this case just signifies + // that the user's action is finished. + _serializeToStreamTask.TrySetResult(true); } } } diff --git a/Prometheus/QuantileEpsilonPair.cs b/Prometheus/QuantileEpsilonPair.cs index 1ccf40a7..52b2eb5e 100644 --- a/Prometheus/QuantileEpsilonPair.cs +++ b/Prometheus/QuantileEpsilonPair.cs @@ -1,14 +1,7 @@ -namespace Prometheus -{ - public readonly struct QuantileEpsilonPair - { - public QuantileEpsilonPair(double quantile, double epsilon) - { - Quantile = quantile; - Epsilon = epsilon; - } +namespace Prometheus; - public double Quantile { get; } - public double Epsilon { get; } - } +public readonly struct QuantileEpsilonPair(double quantile, double epsilon) +{ + public double Quantile { get; } = quantile; + public double Epsilon { get; } = epsilon; } diff --git a/Prometheus/RealDelayer.cs b/Prometheus/RealDelayer.cs index d8e94007..d31b9755 100644 --- a/Prometheus/RealDelayer.cs +++ b/Prometheus/RealDelayer.cs @@ -1,18 +1,17 @@ using System.Diagnostics; -namespace Prometheus +namespace Prometheus; + +/// +/// An implementation that uses Task.Delay(), for use at runtime. +/// +internal sealed class RealDelayer : IDelayer { - /// - /// An implementation that uses Task.Delay(), for use at runtime. - /// - internal sealed class RealDelayer : IDelayer - { - public static readonly RealDelayer Instance = new(); + public static readonly RealDelayer Instance = new(); - [DebuggerStepThrough] - public Task Delay(TimeSpan duration) => Task.Delay(duration); + [DebuggerStepThrough] + public Task Delay(TimeSpan duration) => Task.Delay(duration); - [DebuggerStepThrough] - public Task Delay(TimeSpan duration, CancellationToken cancel) => Task.Delay(duration, cancel); - } + [DebuggerStepThrough] + public Task Delay(TimeSpan duration, CancellationToken cancel) => Task.Delay(duration, cancel); } diff --git a/Prometheus/SummaryConfiguration.cs b/Prometheus/SummaryConfiguration.cs index 40baa923..e6162e97 100644 --- a/Prometheus/SummaryConfiguration.cs +++ b/Prometheus/SummaryConfiguration.cs @@ -1,32 +1,31 @@ -namespace Prometheus +namespace Prometheus; + +public sealed class SummaryConfiguration : MetricConfiguration { - public sealed class SummaryConfiguration : MetricConfiguration - { - internal static readonly SummaryConfiguration Default = new SummaryConfiguration(); + internal static readonly SummaryConfiguration Default = new SummaryConfiguration(); - /// - /// Pairs of quantiles and allowed error values (epsilon). - /// - /// For example, a quantile of 0.95 with an epsilon of 0.01 means the calculated value - /// will be between the 94th and 96th quantile. - /// - /// If null, no quantiles will be calculated! - /// - public IReadOnlyList Objectives { get; set; } = Summary.DefObjectivesArray; + /// + /// Pairs of quantiles and allowed error values (epsilon). + /// + /// For example, a quantile of 0.95 with an epsilon of 0.01 means the calculated value + /// will be between the 94th and 96th quantile. + /// + /// If null, no quantiles will be calculated! + /// + public IReadOnlyList Objectives { get; set; } = Summary.DefObjectivesArray; - /// - /// Time span over which to calculate the summary. - /// - public TimeSpan MaxAge { get; set; } = Summary.DefMaxAge; + /// + /// Time span over which to calculate the summary. + /// + public TimeSpan MaxAge { get; set; } = Summary.DefMaxAge; - /// - /// Number of buckets used to control measurement expiration. - /// - public int AgeBuckets { get; set; } = Summary.DefAgeBuckets; + /// + /// Number of buckets used to control measurement expiration. + /// + public int AgeBuckets { get; set; } = Summary.DefAgeBuckets; - /// - /// Buffer size limit. Use multiples of 500 to avoid waste, as internal buffers use that size. - /// - public int BufferSize { get; set; } = Summary.DefBufCap; - } + /// + /// Buffer size limit. Use multiples of 500 to avoid waste, as internal buffers use that size. + /// + public int BufferSize { get; set; } = Summary.DefBufCap; } diff --git a/Prometheus/SuppressDefaultMetricOptions.cs b/Prometheus/SuppressDefaultMetricOptions.cs index 26d55d5b..a78af281 100644 --- a/Prometheus/SuppressDefaultMetricOptions.cs +++ b/Prometheus/SuppressDefaultMetricOptions.cs @@ -1,96 +1,95 @@ -namespace Prometheus +namespace Prometheus; + +public sealed class SuppressDefaultMetricOptions { - public sealed class SuppressDefaultMetricOptions + internal static readonly SuppressDefaultMetricOptions SuppressAll = new() { - internal static readonly SuppressDefaultMetricOptions SuppressAll = new() - { - SuppressProcessMetrics = true, - SuppressDebugMetrics = true, + SuppressProcessMetrics = true, + SuppressDebugMetrics = true, #if NET - SuppressEventCounters = true, + SuppressEventCounters = true, #endif #if NET6_0_OR_GREATER - SuppressMeters = true + SuppressMeters = true #endif - }; + }; - internal static readonly SuppressDefaultMetricOptions SuppressNone = new() - { - SuppressProcessMetrics = false, - SuppressDebugMetrics = false, + internal static readonly SuppressDefaultMetricOptions SuppressNone = new() + { + SuppressProcessMetrics = false, + SuppressDebugMetrics = false, #if NET - SuppressEventCounters = false, + SuppressEventCounters = false, #endif #if NET6_0_OR_GREATER - SuppressMeters = false + SuppressMeters = false #endif - }; + }; - /// - /// Suppress the current-process-inspecting metrics (uptime, resource use, ...). - /// - public bool SuppressProcessMetrics { get; set; } + /// + /// Suppress the current-process-inspecting metrics (uptime, resource use, ...). + /// + public bool SuppressProcessMetrics { get; set; } - /// - /// Suppress metrics that prometheus-net uses to report debug information about itself (e.g. number of metrics exported). - /// - public bool SuppressDebugMetrics { get; set; } + /// + /// Suppress metrics that prometheus-net uses to report debug information about itself (e.g. number of metrics exported). + /// + public bool SuppressDebugMetrics { get; set; } #if NET - /// - /// Suppress the default .NET Event Counter integration. - /// - public bool SuppressEventCounters { get; set; } + /// + /// Suppress the default .NET Event Counter integration. + /// + public bool SuppressEventCounters { get; set; } #endif #if NET6_0_OR_GREATER - /// - /// Suppress the .NET Meter API integration. - /// - public bool SuppressMeters { get; set; } + /// + /// Suppress the .NET Meter API integration. + /// + public bool SuppressMeters { get; set; } #endif - internal sealed class ConfigurationCallbacks - { + internal sealed class ConfigurationCallbacks + { #if NET - public Action ConfigureEventCounterAdapter = delegate { }; + public Action ConfigureEventCounterAdapter = delegate { }; #endif #if NET6_0_OR_GREATER - public Action ConfigureMeterAdapter = delegate { }; + public Action ConfigureMeterAdapter = delegate { }; #endif - } + } - /// - /// Configures the default metrics registry based on the requested defaults behavior. - /// - internal void ApplyToDefaultRegistry(ConfigurationCallbacks configurationCallbacks) - { - if (!SuppressProcessMetrics) - DotNetStats.RegisterDefault(); + /// + /// Configures the default metrics registry based on the requested defaults behavior. + /// + internal void ApplyToDefaultRegistry(ConfigurationCallbacks configurationCallbacks) + { + if (!SuppressProcessMetrics) + DotNetStats.RegisterDefault(); - if (!SuppressDebugMetrics) - Metrics.DefaultRegistry.StartCollectingRegistryMetrics(); + if (!SuppressDebugMetrics) + Metrics.DefaultRegistry.StartCollectingRegistryMetrics(); #if NET - if (!SuppressEventCounters) - { - var options = new EventCounterAdapterOptions(); - configurationCallbacks.ConfigureEventCounterAdapter(options); - EventCounterAdapter.StartListening(options); - } + if (!SuppressEventCounters) + { + var options = new EventCounterAdapterOptions(); + configurationCallbacks.ConfigureEventCounterAdapter(options); + EventCounterAdapter.StartListening(options); + } #endif #if NET6_0_OR_GREATER - if (!SuppressMeters) - { - var options = new MeterAdapterOptions(); - configurationCallbacks.ConfigureMeterAdapter(options); - MeterAdapter.StartListening(options); - } -#endif + if (!SuppressMeters) + { + var options = new MeterAdapterOptions(); + configurationCallbacks.ConfigureMeterAdapter(options); + MeterAdapter.StartListening(options); } +#endif } } diff --git a/Prometheus/ThreadSafeLong.cs b/Prometheus/ThreadSafeLong.cs index 222a262d..a81f7aab 100644 --- a/Prometheus/ThreadSafeLong.cs +++ b/Prometheus/ThreadSafeLong.cs @@ -35,8 +35,8 @@ public override string ToString() public override bool Equals(object? obj) { - if (obj is ThreadSafeLong) - return Value.Equals(((ThreadSafeLong)obj).Value); + if (obj is ThreadSafeLong other) + return Value.Equals(other.Value); return Value.Equals(obj); } diff --git a/Prometheus/TimerExtensions.cs b/Prometheus/TimerExtensions.cs index dabb6569..b1f3bcf3 100644 --- a/Prometheus/TimerExtensions.cs +++ b/Prometheus/TimerExtensions.cs @@ -1,67 +1,66 @@ -namespace Prometheus +namespace Prometheus; + +public static class TimerExtensions { - public static class TimerExtensions + private sealed class Timer : ITimer { - private sealed class Timer : ITimer - { - private readonly ValueStopwatch _stopwatch = ValueStopwatch.StartNew(); - private readonly Action _observeDurationAction; - - public Timer(IObserver observer) - { - _observeDurationAction = duration => observer.Observe(duration); - } - - public Timer(IGauge gauge) - { - _observeDurationAction = duration => gauge.Set(duration); - } - - public Timer(ICounter counter) - { - _observeDurationAction = duration => counter.Inc(duration); - } + private readonly ValueStopwatch _stopwatch = ValueStopwatch.StartNew(); + private readonly Action _observeDurationAction; - public TimeSpan ObserveDuration() - { - var duration = _stopwatch.GetElapsedTime(); - _observeDurationAction.Invoke(duration.TotalSeconds); - - return duration; - } + public Timer(IObserver observer) + { + _observeDurationAction = observer.Observe; + } - public void Dispose() - { - ObserveDuration(); - } + public Timer(IGauge gauge) + { + _observeDurationAction = gauge.Set; } - /// - /// Enables you to easily report elapsed seconds in the value of an observer. - /// Dispose of the returned instance to report the elapsed duration. - /// - public static ITimer NewTimer(this IObserver observer) + public Timer(ICounter counter) { - return new Timer(observer); + _observeDurationAction = counter.Inc; } - /// - /// Enables you to easily report elapsed seconds in the value of a gauge. - /// Dispose of the returned instance to report the elapsed duration. - /// - public static ITimer NewTimer(this IGauge gauge) + public TimeSpan ObserveDuration() { - return new Timer(gauge); + var duration = _stopwatch.GetElapsedTime(); + _observeDurationAction(duration.TotalSeconds); + + return duration; } - /// - /// Enables you to easily report elapsed seconds in the value of a counter. - /// The duration (in seconds) will be added to the value of the counter. - /// Dispose of the returned instance to report the elapsed duration. - /// - public static ITimer NewTimer(this ICounter counter) + public void Dispose() { - return new Timer(counter); + ObserveDuration(); } } + + /// + /// Enables you to easily report elapsed seconds in the value of an observer. + /// Dispose of the returned instance to report the elapsed duration. + /// + public static ITimer NewTimer(this IObserver observer) + { + return new Timer(observer); + } + + /// + /// Enables you to easily report elapsed seconds in the value of a gauge. + /// Dispose of the returned instance to report the elapsed duration. + /// + public static ITimer NewTimer(this IGauge gauge) + { + return new Timer(gauge); + } + + /// + /// Enables you to easily report elapsed seconds in the value of a counter. + /// The duration (in seconds) will be added to the value of the counter. + /// Dispose of the returned instance to report the elapsed duration. + /// + public static ITimer NewTimer(this ICounter counter) + { + return new Timer(counter); + } } diff --git a/Prometheus/TimestampHelpers.cs b/Prometheus/TimestampHelpers.cs index baca4c10..c47ce850 100644 --- a/Prometheus/TimestampHelpers.cs +++ b/Prometheus/TimestampHelpers.cs @@ -1,26 +1,25 @@ -namespace Prometheus +namespace Prometheus; + +static class TimestampHelpers { - static class TimestampHelpers - { - // Math copypasted from DateTimeOffset.cs in .NET Framework. + // Math copypasted from DateTimeOffset.cs in .NET Framework. - // Number of days in a non-leap year - private const int DaysPerYear = 365; - // Number of days in 4 years - private const int DaysPer4Years = DaysPerYear * 4 + 1; // 1461 - // Number of days in 100 years - private const int DaysPer100Years = DaysPer4Years * 25 - 1; // 36524 - // Number of days in 400 years - private const int DaysPer400Years = DaysPer100Years * 4 + 1; // 146097 - private const int DaysTo1970 = DaysPer400Years * 4 + DaysPer100Years * 3 + DaysPer4Years * 17 + DaysPerYear; // 719,162 - private const long UnixEpochTicks = TimeSpan.TicksPerDay * DaysTo1970; // 621,355,968,000,000,000 - private const long UnixEpochSeconds = UnixEpochTicks / TimeSpan.TicksPerSecond; // 62,135,596,800 + // Number of days in a non-leap year + private const int DaysPerYear = 365; + // Number of days in 4 years + private const int DaysPer4Years = DaysPerYear * 4 + 1; // 1461 + // Number of days in 100 years + private const int DaysPer100Years = DaysPer4Years * 25 - 1; // 36524 + // Number of days in 400 years + private const int DaysPer400Years = DaysPer100Years * 4 + 1; // 146097 + private const int DaysTo1970 = DaysPer400Years * 4 + DaysPer100Years * 3 + DaysPer4Years * 17 + DaysPerYear; // 719,162 + private const long UnixEpochTicks = TimeSpan.TicksPerDay * DaysTo1970; // 621,355,968,000,000,000 + private const long UnixEpochSeconds = UnixEpochTicks / TimeSpan.TicksPerSecond; // 62,135,596,800 - public static double ToUnixTimeSecondsAsDouble(DateTimeOffset timestamp) - { - // This gets us sub-millisecond precision, which is better than ToUnixTimeMilliseconds(). - var ticksSinceUnixEpoch = timestamp.ToUniversalTime().Ticks - UnixEpochSeconds * TimeSpan.TicksPerSecond; - return ticksSinceUnixEpoch / (double)TimeSpan.TicksPerSecond; - } + public static double ToUnixTimeSecondsAsDouble(DateTimeOffset timestamp) + { + // This gets us sub-millisecond precision, which is better than ToUnixTimeMilliseconds(). + var ticksSinceUnixEpoch = timestamp.ToUniversalTime().Ticks - UnixEpochSeconds * TimeSpan.TicksPerSecond; + return ticksSinceUnixEpoch / (double)TimeSpan.TicksPerSecond; } } diff --git a/Prometheus/ValueStopwatch.cs b/Prometheus/ValueStopwatch.cs index 2dee1891..87aa33d0 100644 --- a/Prometheus/ValueStopwatch.cs +++ b/Prometheus/ValueStopwatch.cs @@ -1,36 +1,32 @@ using System.Diagnostics; // Copied from: https://github.com/dotnet/extensions/blob/master/src/Shared/src/ValueStopwatch/ValueStopwatch.cs -namespace Prometheus -{ - internal struct ValueStopwatch - { - private static readonly double TimestampToTicks = TimeSpan.TicksPerSecond / (double)Stopwatch.Frequency; +namespace Prometheus; - private long _startTimestamp; +internal readonly struct ValueStopwatch +{ + private static readonly double TimestampToTicks = TimeSpan.TicksPerSecond / (double)Stopwatch.Frequency; - public bool IsActive => _startTimestamp != 0; + private readonly long _startTimestamp; - private ValueStopwatch(long startTimestamp) - { - _startTimestamp = startTimestamp; - } + private ValueStopwatch(long startTimestamp) + { + _startTimestamp = startTimestamp; + } - public static ValueStopwatch StartNew() => new ValueStopwatch(Stopwatch.GetTimestamp()); + public static ValueStopwatch StartNew() => new(Stopwatch.GetTimestamp()); - public TimeSpan GetElapsedTime() - { - // Start timestamp can't be zero in an initialized ValueStopwatch. It would have to be literally the first thing executed when the machine boots to be 0. - // So it being 0 is a clear indication of default(ValueStopwatch) - if (!IsActive) - { - throw new InvalidOperationException("An uninitialized, or 'default', ValueStopwatch cannot be used to get elapsed time."); - } + public TimeSpan GetElapsedTime() + { + // Start timestamp can't be zero in an initialized ValueStopwatch. + // It would have to be literally the first thing executed when the machine boots to be 0. + // So it being 0 is a clear indication of default(ValueStopwatch) + if (_startTimestamp == 0) + throw new InvalidOperationException("An uninitialized, or 'default', ValueStopwatch cannot be used to get elapsed time."); - var end = Stopwatch.GetTimestamp(); - var timestampDelta = end - _startTimestamp; - var ticks = (long)(TimestampToTicks * timestampDelta); - return new TimeSpan(ticks); - } + var end = Stopwatch.GetTimestamp(); + var timestampDelta = end - _startTimestamp; + var ticks = (long)(TimestampToTicks * timestampDelta); + return new TimeSpan(ticks); } } From 31ebd1b6657c1fbf77ab006ddce39ab0860892bb Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Mon, 4 Dec 2023 18:12:51 +0200 Subject: [PATCH 222/230] Use slightly maybe sort of a bit faster atomic logic in random places Seems to benefit exemplar case the most? --- Docs/MeasurementsBenchmarks.xlsx | Bin 10088 -> 10087 bytes Prometheus/ChildBase.cs | 6 +++--- Prometheus/Exemplar.cs | 16 ++++++++-------- Prometheus/MeterAdapter.cs | 7 ++++--- README.md | 8 ++++---- 5 files changed, 19 insertions(+), 18 deletions(-) diff --git a/Docs/MeasurementsBenchmarks.xlsx b/Docs/MeasurementsBenchmarks.xlsx index 52647c125dbd5f92a04702a8765715d436eacd45..f2b297267b3f1723c5959faf392bf4fd6b091464 100644 GIT binary patch delta 2026 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 @@ -274,14 +273,15 @@ internal void ReturnToPoolIfNotEmpty() ExemplarPool.Return(this); } - private volatile bool _consumed; + private long _consumed; + + private const long IsConsumed = 1; + private const long IsNotConsumed = 0; internal void MarkAsConsumed() { - if (_consumed) + if (Interlocked.Exchange(ref _consumed, IsConsumed) == IsConsumed) throw new InvalidOperationException($"An instance of {nameof(Exemplar)} was reused. You must obtain a new instance via Exemplar.From() or Exemplar.Clone() for each metric value observation."); - - _consumed = true; } /// @@ -289,7 +289,7 @@ internal void MarkAsConsumed() /// public Exemplar Clone() { - if (_consumed) + if (Interlocked.Read(ref _consumed) == IsConsumed) throw new InvalidOperationException($"An instance of {nameof(Exemplar)} cannot be cloned after it has already been used."); var clone = AllocateFromPool(Length); diff --git a/Prometheus/MeterAdapter.cs b/Prometheus/MeterAdapter.cs index 00b7d8eb..1ea773d8 100644 --- a/Prometheus/MeterAdapter.cs +++ b/Prometheus/MeterAdapter.cs @@ -65,8 +65,9 @@ private MeterAdapter(MeterAdapterOptions options) { // ICollectorRegistry does not support unregistering the callback, so we just no-op when disposed. // The expected pattern is that any disposal of the pipeline also throws away the ICollectorRegistry. - if (_disposed) - return; + lock (_disposedLock) + if (_disposed) + return; // Seems OK to call even when _listener has been disposed. _listener.RecordObservableInstruments(); @@ -83,7 +84,7 @@ private MeterAdapter(MeterAdapterOptions options) private readonly MeterListener _listener = new MeterListener(); - private volatile bool _disposed; + private bool _disposed; private readonly object _disposedLock = new(); public void Dispose() diff --git a/README.md b/README.md index da9b6d07..03809e30 100644 --- a/README.md +++ b/README.md @@ -877,10 +877,10 @@ As an example of the performance of measuring data using prometheus-net, we have | Metric type | Measurements per second | |-------------------------|------------------------:| -| Counter | 259 million | -| Gauge | 593 million | -| Histogram (16 buckets) | 106 million | -| Histogram (128 buckets) | 58 million | +| Counter | 261 million | +| Gauge | 591 million | +| Histogram (16 buckets) | 105 million | +| Histogram (128 buckets) | 65 million | Another popular .NET SDK with Prometheus support is the OpenTelemetry SDK. To help you choose, we have [SdkComparisonBenchmarks.cs](Benchmark.NetCore/SdkComparisonBenchmarks.cs) to compare the two SDKs and give some idea of how they differer in the performance tradeoffs made. Both SDKs are evaluated in single-threaded mode under a comparable workload and enabled feature set. A representative result is here: From 99f640fbd60e09e1e6dcf62366077db74cec9719 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Mon, 4 Dec 2023 18:48:01 +0200 Subject: [PATCH 223/230] Optimize StringSequence Concat() for one-empty cases --- Benchmark.NetCore/StringSequenceBenchmarks.cs | 12 ++++++++++++ Prometheus/StringSequence.cs | 15 +++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/Benchmark.NetCore/StringSequenceBenchmarks.cs b/Benchmark.NetCore/StringSequenceBenchmarks.cs index 0a052efe..08e61c02 100644 --- a/Benchmark.NetCore/StringSequenceBenchmarks.cs +++ b/Benchmark.NetCore/StringSequenceBenchmarks.cs @@ -61,4 +61,16 @@ public void Equals_Negative() { FromValues3.Equals(Other); } + + [Benchmark] + public void Concat_Empty() + { + FromValues3.Concat(StringSequence.Empty); + } + + [Benchmark] + public void Concat_ToEmpty() + { + StringSequence.Empty.Concat(FromValues3); + } } diff --git a/Prometheus/StringSequence.cs b/Prometheus/StringSequence.cs index e68f3fee..74334106 100644 --- a/Prometheus/StringSequence.cs +++ b/Prometheus/StringSequence.cs @@ -159,18 +159,33 @@ public static StringSequence From(ReadOnlyMemory values) // Creates a new sequence, inheriting all current values and optionally adding more. New values are prepended to the sequence, inherited values come last. public StringSequence InheritAndPrepend(params string[] prependValues) { + if (prependValues.Length == 0) + return this; + return new StringSequence(this, Empty, prependValues); } // Creates a new sequence, inheriting all current values and optionally adding more. New values are prepended to the sequence, inherited values come last. public StringSequence InheritAndPrepend(StringSequence prependValues) { + if (prependValues.IsEmpty) + return this; + + if (IsEmpty) + return prependValues; + return new StringSequence(this, prependValues, null); } // Creates a new sequence, concatenating another string sequence (by inheriting from it). public StringSequence Concat(StringSequence concatenatedValues) { + if (concatenatedValues.IsEmpty) + return this; + + if (IsEmpty) + return concatenatedValues; + return new StringSequence(concatenatedValues, this, null); } From 78cd754fcb1bd316c725a84d92f7a6202caff4ef Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Mon, 4 Dec 2023 23:47:20 +0200 Subject: [PATCH 224/230] ServerGC in benchmarks --- Benchmark.NetCore/Benchmark.NetCore.csproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Benchmark.NetCore/Benchmark.NetCore.csproj b/Benchmark.NetCore/Benchmark.NetCore.csproj index 183b1bd4..56a86c7b 100644 --- a/Benchmark.NetCore/Benchmark.NetCore.csproj +++ b/Benchmark.NetCore/Benchmark.NetCore.csproj @@ -13,6 +13,8 @@ True 1591 + true + latest 9999 From 7837255da30035379547c5a5b77b0b557081d74c Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Tue, 5 Dec 2023 00:25:27 +0200 Subject: [PATCH 225/230] Use Dictionary.TryAdd() to reduce CPU cost of adding new items to dictionaries --- .../MetricExpirationBenchmarks.cs | 2 + Benchmark.NetCore/SdkComparisonBenchmarks.cs | 18 +++--- Prometheus/Collector.cs | 12 +++- Prometheus/CollectorFamily.cs | 13 +++- Prometheus/CollectorRegistry.cs | 12 +++- ...elEnrichingManagedLifetimeMetricFactory.cs | 52 +++++++++++++-- Prometheus/ManagedLifetimeMetricFactory.cs | 64 +++++++++++++++---- Prometheus/ManagedLifetimeMetricHandle.cs | 44 ++++++++----- Prometheus/MeterAdapter.cs | 16 +++-- README.md | 12 ++-- 10 files changed, 187 insertions(+), 58 deletions(-) diff --git a/Benchmark.NetCore/MetricExpirationBenchmarks.cs b/Benchmark.NetCore/MetricExpirationBenchmarks.cs index 4bac6ccd..f957ae0b 100644 --- a/Benchmark.NetCore/MetricExpirationBenchmarks.cs +++ b/Benchmark.NetCore/MetricExpirationBenchmarks.cs @@ -10,6 +10,8 @@ namespace Benchmark.NetCore; [MemoryDiagnoser] // This seems to need a lot of warmup to stabilize. [WarmupCount(80)] +// This seems to need a lot of iterations to stabilize. +[IterationCount(100)] //[EventPipeProfiler(BenchmarkDotNet.Diagnosers.EventPipeProfile.GcVerbose)] public class MetricExpirationBenchmarks { diff --git a/Benchmark.NetCore/SdkComparisonBenchmarks.cs b/Benchmark.NetCore/SdkComparisonBenchmarks.cs index 200b66c9..eb95dc2d 100644 --- a/Benchmark.NetCore/SdkComparisonBenchmarks.cs +++ b/Benchmark.NetCore/SdkComparisonBenchmarks.cs @@ -11,17 +11,17 @@ namespace Benchmark.NetCore; .NET SDK 8.0.100 [Host] : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 DefaultJob : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 - Job-IZHPUA : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 + Job-PPCGVJ : .NET 8.0.0 (8.0.23.53103), X64 RyuJIT AVX2 -| Method | Job | MaxIterationCount | Mean | Error | StdDev | Gen0 | Gen1 | Allocated | -|------------------------------ |----------- |------------------ |------------:|----------:|----------:|--------:|--------:|----------:| -| PromNetCounter | DefaultJob | Default | 237.1 us | 1.71 us | 1.43 us | - | - | - | -| PromNetHistogram | DefaultJob | Default | 1,236.2 us | 9.99 us | 8.86 us | - | - | 2 B | -| OTelCounter | DefaultJob | Default | 10,981.5 us | 64.49 us | 57.17 us | - | - | 11 B | -| OTelHistogram | DefaultJob | Default | 12,078.9 us | 126.10 us | 117.95 us | - | - | 24 B | -| PromNetHistogramForAdHocLabel | Job-NDQITE | 16 | 1,877.7 us | 104.83 us | 87.54 us | 50.7813 | 48.8281 | 872701 B | -| OTelHistogramForAdHocLabel | Job-NDQITE | 16 | 354.0 us | 4.05 us | 3.78 us | 5.3711 | - | 96000 B | +| Method | Job | MaxIterationCount | Mean | Error | StdDev | Allocated | +|------------------------------ |----------- |------------------ |------------:|---------:|---------:|----------:| +| PromNetCounter | DefaultJob | Default | 230.4 μs | 1.20 μs | 1.12 μs | - | +| PromNetHistogram | DefaultJob | Default | 956.7 μs | 3.28 μs | 3.07 μs | - | +| OTelCounter | DefaultJob | Default | 10,998.2 μs | 35.54 μs | 31.51 μs | 11 B | +| OTelHistogram | DefaultJob | Default | 12,110.3 μs | 17.08 μs | 14.26 μs | 11 B | +| PromNetHistogramForAdHocLabel | Job-PPCGVJ | 16 | 716.2 μs | 30.20 μs | 26.77 μs | 664000 B | +| OTelHistogramForAdHocLabel | Job-PPCGVJ | 16 | 350.5 μs | 1.91 μs | 1.79 μs | 96000 B | */ /// diff --git a/Prometheus/Collector.cs b/Prometheus/Collector.cs index d731d75c..45c45b6c 100644 --- a/Prometheus/Collector.cs +++ b/Prometheus/Collector.cs @@ -359,16 +359,26 @@ private bool TryGetLabelled(LabelSequence instanceLabels, out TChild? child) private TChild CreateLabelled(LabelSequence instanceLabels) { + var newChild = _createdLabelledChildFunc(instanceLabels); + _childrenLock.EnterWriteLock(); try { +#if NET + // It could be that someone beats us to it! Probably not, though. + if (_children.TryAdd(instanceLabels, newChild)) + return newChild; + + return _children[instanceLabels]; +#else + // On .NET Fx we need to do the pessimistic case first because there is no TryAdd(). if (_children.TryGetValue(instanceLabels, out var existing)) return existing; - var newChild = _createdLabelledChildFunc(instanceLabels); _children.Add(instanceLabels, newChild); return newChild; +#endif } finally { diff --git a/Prometheus/CollectorFamily.cs b/Prometheus/CollectorFamily.cs index 12ea17c7..6db89569 100644 --- a/Prometheus/CollectorFamily.cs +++ b/Prometheus/CollectorFamily.cs @@ -96,18 +96,27 @@ internal Collector GetOrAdd( _lock.ExitReadLock(); } - // Then we grab a write lock. This is the slow path. It could still be that someone beats us to it! + // Then we grab a write lock. This is the slow path. + var newCollector = initializer(name, help, identity.InstanceLabelNames, identity.StaticLabels, configuration, exemplarBehavior); _lock.EnterWriteLock(); try { +#if NET + // It could be that someone beats us to it! Probably not, though. + if (_collectors.TryAdd(identity, newCollector)) + return newCollector; + + return _collectors[identity]; +#else + // On .NET Fx we need to do the pessimistic case first because there is no TryAdd(). if (_collectors.TryGetValue(identity, out var collector)) return collector; - var newCollector = initializer(name, help, identity.InstanceLabelNames, identity.StaticLabels, configuration, exemplarBehavior); _collectors.Add(identity, newCollector); return newCollector; +#endif } finally { diff --git a/Prometheus/CollectorRegistry.cs b/Prometheus/CollectorRegistry.cs index b91ebb6c..78d01e71 100644 --- a/Prometheus/CollectorRegistry.cs +++ b/Prometheus/CollectorRegistry.cs @@ -199,16 +199,26 @@ static CollectorFamily ValidateFamily(CollectorFamily candidate) } // It does not exist. OK, just create it. + var newFamily = new CollectorFamily(typeof(TCollector)); + _familiesLock.EnterWriteLock(); try { +#if NET + // It could be that someone beats us to it! Probably not, though. + if (_families.TryAdd(finalName, newFamily)) + return newFamily; + + return ValidateFamily(_families[finalName]); +#else + // On .NET Fx we need to do the pessimistic case first because there is no TryAdd(). if (_families.TryGetValue(finalName, out var existing)) return ValidateFamily(existing); - var newFamily = new CollectorFamily(typeof(TCollector)); _families.Add(finalName, newFamily); return newFamily; +#endif } finally { diff --git a/Prometheus/LabelEnrichingManagedLifetimeMetricFactory.cs b/Prometheus/LabelEnrichingManagedLifetimeMetricFactory.cs index 4340f345..11d6d7a7 100644 --- a/Prometheus/LabelEnrichingManagedLifetimeMetricFactory.cs +++ b/Prometheus/LabelEnrichingManagedLifetimeMetricFactory.cs @@ -1,6 +1,4 @@ -using System.Collections.Concurrent; - -namespace Prometheus; +namespace Prometheus; /// /// Applies a set of static labels to lifetime-managed metrics. Multiple instances are functionally equivalent for the same label set. @@ -48,16 +46,26 @@ public IManagedLifetimeMetricHandle CreateCounter(string name, string _countersLock.ExitReadLock(); } + var instance = CreateCounterCore(innerHandle); + _countersLock.EnterWriteLock(); try { +#if NET + // It could be that someone beats us to it! Probably not, though. + if (_counters.TryAdd(innerHandle, instance)) + return instance; + + return _counters[innerHandle]; +#else + // On .NET Fx we need to do the pessimistic case first because there is no TryAdd(). if (_counters.TryGetValue(innerHandle, out var existing)) return existing; - var instance = CreateCounterCore(innerHandle); _counters.Add(innerHandle, instance); return instance; +#endif } finally { @@ -90,16 +98,26 @@ public IManagedLifetimeMetricHandle CreateGauge(string name, string help _gaugesLock.ExitReadLock(); } + var instance = CreateGaugeCore(innerHandle); + _gaugesLock.EnterWriteLock(); try { +#if NET + // It could be that someone beats us to it! Probably not, though. + if (_gauges.TryAdd(innerHandle, instance)) + return instance; + + return _gauges[innerHandle]; +#else + // On .NET Fx we need to do the pessimistic case first because there is no TryAdd(). if (_gauges.TryGetValue(innerHandle, out var existing)) return existing; - var instance = CreateGaugeCore(innerHandle); _gauges.Add(innerHandle, instance); return instance; +#endif } finally { @@ -131,16 +149,26 @@ public IManagedLifetimeMetricHandle CreateHistogram(string name, str _histogramsLock.ExitReadLock(); } + var instance = CreateHistogramCore(innerHandle); + _histogramsLock.EnterWriteLock(); try { +#if NET + // It could be that someone beats us to it! Probably not, though. + if (_histograms.TryAdd(innerHandle, instance)) + return instance; + + return _histograms[innerHandle]; +#else + // On .NET Fx we need to do the pessimistic case first because there is no TryAdd(). if (_histograms.TryGetValue(innerHandle, out var existing)) return existing; - var instance = CreateHistogramCore(innerHandle); _histograms.Add(innerHandle, instance); return instance; +#endif } finally { @@ -172,16 +200,26 @@ public IManagedLifetimeMetricHandle CreateSummary(string name, string _summariesLock.ExitReadLock(); } + var instance = CreateSummaryCore(innerHandle); + _summariesLock.EnterWriteLock(); try { +#if NET + // It could be that someone beats us to it! Probably not, though. + if (_summaries.TryAdd(innerHandle, instance)) + return instance; + + return _summaries[innerHandle]; +#else + // On .NET Fx we need to do the pessimistic case first because there is no TryAdd(). if (_summaries.TryGetValue(innerHandle, out var existing)) return existing; - var instance = CreateSummaryCore(innerHandle); _summaries.Add(innerHandle, instance); return instance; +#endif } finally { diff --git a/Prometheus/ManagedLifetimeMetricFactory.cs b/Prometheus/ManagedLifetimeMetricFactory.cs index 2daf53d4..22d67178 100644 --- a/Prometheus/ManagedLifetimeMetricFactory.cs +++ b/Prometheus/ManagedLifetimeMetricFactory.cs @@ -38,17 +38,27 @@ public IManagedLifetimeMetricHandle CreateCounter(string name, string _countersLock.ExitReadLock(); } + var metric = _inner.CreateCounter(identity.MetricFamilyName, help, identity.InstanceLabelNames, configuration); + var instance = new ManagedLifetimeCounter(metric, _expiresAfter); + _countersLock.EnterWriteLock(); try { +#if NET + // It could be that someone beats us to it! Probably not, though. + if (_counters.TryAdd(identity, instance)) + return instance; + + return _counters[identity]; +#else + // On .NET Fx we need to do the pessimistic case first because there is no TryAdd(). if (_counters.TryGetValue(identity, out var existing)) return existing; - - var metric = _inner.CreateCounter(identity.MetricFamilyName, help, identity.InstanceLabelNames, configuration); - var instance = new ManagedLifetimeCounter(metric, _expiresAfter); + _counters.Add(identity, instance); return instance; +#endif } finally { @@ -73,17 +83,27 @@ public IManagedLifetimeMetricHandle CreateGauge(string name, string help _gaugesLock.ExitReadLock(); } + var metric = _inner.CreateGauge(identity.MetricFamilyName, help, identity.InstanceLabelNames, configuration); + var instance = new ManagedLifetimeGauge(metric, _expiresAfter); + _gaugesLock.EnterWriteLock(); try { +#if NET + // It could be that someone beats us to it! Probably not, though. + if (_gauges.TryAdd(identity, instance)) + return instance; + + return _gauges[identity]; +#else + // On .NET Fx we need to do the pessimistic case first because there is no TryAdd(). if (_gauges.TryGetValue(identity, out var existing)) return existing; - - var metric = _inner.CreateGauge(identity.MetricFamilyName, help, identity.InstanceLabelNames, configuration); - var instance = new ManagedLifetimeGauge(metric, _expiresAfter); + _gauges.Add(identity, instance); return instance; +#endif } finally { @@ -108,17 +128,27 @@ public IManagedLifetimeMetricHandle CreateHistogram(string name, str _histogramsLock.ExitReadLock(); } + var metric = _inner.CreateHistogram(identity.MetricFamilyName, help, identity.InstanceLabelNames, configuration); + var instance = new ManagedLifetimeHistogram(metric, _expiresAfter); + _histogramsLock.EnterWriteLock(); try { +#if NET + // It could be that someone beats us to it! Probably not, though. + if (_histograms.TryAdd(identity, instance)) + return instance; + + return _histograms[identity]; +#else + // On .NET Fx we need to do the pessimistic case first because there is no TryAdd(). if (_histograms.TryGetValue(identity, out var existing)) return existing; - - var metric = _inner.CreateHistogram(identity.MetricFamilyName, help, identity.InstanceLabelNames, configuration); - var instance = new ManagedLifetimeHistogram(metric, _expiresAfter); + _histograms.Add(identity, instance); return instance; +#endif } finally { @@ -143,17 +173,27 @@ public IManagedLifetimeMetricHandle CreateSummary(string name, string _summariesLock.ExitReadLock(); } + var metric = _inner.CreateSummary(identity.MetricFamilyName, help, identity.InstanceLabelNames, configuration); + var instance = new ManagedLifetimeSummary(metric, _expiresAfter); + _summariesLock.EnterWriteLock(); try { +#if NET + // It could be that someone beats us to it! Probably not, though. + if (_summaries.TryAdd(identity, instance)) + return instance; + + return _summaries[identity]; +#else + // On .NET Fx we need to do the pessimistic case first because there is no TryAdd(). if (_summaries.TryGetValue(identity, out var existing)) return existing; - - var metric = _inner.CreateSummary(identity.MetricFamilyName, help, identity.InstanceLabelNames, configuration); - var instance = new ManagedLifetimeSummary(metric, _expiresAfter); + _summaries.Add(identity, instance); return instance; +#endif } finally { diff --git a/Prometheus/ManagedLifetimeMetricHandle.cs b/Prometheus/ManagedLifetimeMetricHandle.cs index e7c6b244..3045338a 100644 --- a/Prometheus/ManagedLifetimeMetricHandle.cs +++ b/Prometheus/ManagedLifetimeMetricHandle.cs @@ -265,11 +265,11 @@ private ChildLifetimeInfo GetOrCreateLifetimeAndIncrementLeaseCount(TChild child try { // Ideally, there already exists a registered lifetime for this metric instance. - if (_lifetimes.TryGetValue(child, out var lifetime)) + if (_lifetimes.TryGetValue(child, out var existing)) { // Immediately increment it, to reduce the risk of any concurrent activities ending the lifetime. - Interlocked.Increment(ref lifetime.LeaseCount); - return lifetime; + Interlocked.Increment(ref existing.LeaseCount); + return existing; } } finally @@ -278,27 +278,39 @@ private ChildLifetimeInfo GetOrCreateLifetimeAndIncrementLeaseCount(TChild child } // No lifetime registered yet - we need to take a write lock and register it. + var newLifetime = new ChildLifetimeInfo + { + LeaseCount = 1 + }; _lifetimesLock.EnterWriteLock(); try { - // Did we get lucky and someone already registered it? - if (_lifetimes.TryGetValue(child, out var lifetime)) +#if NET + // It could be that someone beats us to it! Probably not, though. + if (_lifetimes.TryAdd(child, newLifetime)) + return newLifetime; + + var existing = _lifetimes[child]; + + // Immediately increment it, to reduce the risk of any concurrent activities ending the lifetime. + // Even if something does, it is not the end of the world - the reaper will create a new lifetime when it realizes this happened. + Interlocked.Increment(ref existing.LeaseCount); + return existing; +#else + // On .NET Fx we need to do the pessimistic case first because there is no TryAdd(). + if (_lifetimes.TryGetValue(child, out var existing)) { // Immediately increment it, to reduce the risk of any concurrent activities ending the lifetime. - Interlocked.Increment(ref lifetime.LeaseCount); - return lifetime; + // Even if something does, it is not the end of the world - the reaper will create a new lifetime when it realizes this happened. + Interlocked.Increment(ref existing.LeaseCount); + return existing; } - // Did not get lucky. Make a new one. - lifetime = new ChildLifetimeInfo - { - LeaseCount = 1 - }; - - _lifetimes.Add(child, lifetime); - return lifetime; + _lifetimes.Add(child, newLifetime); + return newLifetime; +#endif } finally { @@ -334,7 +346,7 @@ private sealed class Lease(ManagedLifetimeMetricHandle { public void Dispose() => parent.OnLeaseEnded(child, lifetime); } - #endregion +#endregion #region Reaper // Whether the reaper is currently active. This is set to true when a metric instance is created and diff --git a/Prometheus/MeterAdapter.cs b/Prometheus/MeterAdapter.cs index 1ea773d8..ed41a76f 100644 --- a/Prometheus/MeterAdapter.cs +++ b/Prometheus/MeterAdapter.cs @@ -339,18 +339,26 @@ private MetricContext CreateMetricContext( // Create the context before taking any locks, to avoid holding the cache too long. DeterminePrometheusLabels(tags, out var prometheusLabelNames, out var prometheusLabelValueIndexes); var metricHandle = metricFactory(instrument, _instrumentPrometheusNames[instrument], _instrumentPrometheusHelp[instrument], prometheusLabelNames); + var newContext = new MetricContext(metricHandle, prometheusLabelValueIndexes); cacheLock.EnterWriteLock(); try { - // It is theoretically possible that another thread got to it already, in which case we exit early. +#if NET + // It could be that someone beats us to it! Probably not, though. + if (cache.TryAdd(cacheKey, newContext)) + return newContext; + + return cache[cacheKey]; +#else + // On .NET Fx we need to do the pessimistic case first because there is no TryAdd(). if (cache.TryGetValue(cacheKey, out var context)) return context; - context = new MetricContext(metricHandle, prometheusLabelValueIndexes); - cache.Add(cacheKey, context); - return context; + cache.Add(cacheKey, newContext); + return newContext; +#endif } finally { diff --git a/README.md b/README.md index 03809e30..9ddfd113 100644 --- a/README.md +++ b/README.md @@ -886,12 +886,12 @@ Another popular .NET SDK with Prometheus support is the OpenTelemetry SDK. To he | SDK | Benchmark scenario | CPU time | Memory | |----------------|---------------------------------------|---------:|-------:| -| prometheus-net | Counter (existing timeseries) x100K | 233 µs | None | -| OpenTelemetry | Counter (existing timeseries) x100K | 10770 µs | None | -| prometheus-net | Histogram (existing timeseries) x100K | 958 µs | None | -| OpenTelemetry | Histogram (existing timeseries) x100K | 11997 µs | None | -| prometheus-net | Histogram (new timeseries) x1K | 992 µs | 664 KB | -| OpenTelemetry | Histogram (new timeseries) x1K | 386 µs | 96 KB | +| prometheus-net | Counter (existing timeseries) x100K | 230 µs | None | +| OpenTelemetry | Counter (existing timeseries) x100K | 10998 µs | None | +| prometheus-net | Histogram (existing timeseries) x100K | 957 µs | None | +| OpenTelemetry | Histogram (existing timeseries) x100K | 12110 µs | None | +| prometheus-net | Histogram (new timeseries) x1K | 716 µs | 664 KB | +| OpenTelemetry | Histogram (new timeseries) x1K | 350 µs | 96 KB | # Community projects From 9cb24e893f5f945895d93574467f8466a452e7f6 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Tue, 5 Dec 2023 15:35:04 +0200 Subject: [PATCH 226/230] Add WithLabels(ROM) and WithLabels (ROS) to ICollector and tidy up some benchmarks to reduce duplication and improve usefulness of the data --- Benchmark.NetCore/Benchmark.NetCore.csproj | 1 - Benchmark.NetCore/ManualDelayer.cs | 55 ++++++ Benchmark.NetCore/MetricCreationBenchmarks.cs | 117 +++++++++--- .../MetricExpirationBenchmarks.cs | 174 ++++++------------ Benchmark.NetCore/MetricPusherBenchmarks.cs | 109 ++++++----- Benchmark.NetCore/SummaryBenchmarks.cs | 107 ++++++----- Prometheus/ICollector.cs | 2 + Prometheus/LabelEnrichingAutoLeasingMetric.cs | 69 ++++++- Prometheus/ManagedLifetimeCounter.cs | 33 ++-- Prometheus/ManagedLifetimeGauge.cs | 54 +++--- Prometheus/ManagedLifetimeHistogram.cs | 33 ++-- Prometheus/ManagedLifetimeSummary.cs | 26 ++- Tests.NetCore/BreakableDelayer.cs | 111 ++++++----- 13 files changed, 512 insertions(+), 379 deletions(-) create mode 100644 Benchmark.NetCore/ManualDelayer.cs diff --git a/Benchmark.NetCore/Benchmark.NetCore.csproj b/Benchmark.NetCore/Benchmark.NetCore.csproj index 56a86c7b..880d2090 100644 --- a/Benchmark.NetCore/Benchmark.NetCore.csproj +++ b/Benchmark.NetCore/Benchmark.NetCore.csproj @@ -23,7 +23,6 @@ - diff --git a/Benchmark.NetCore/ManualDelayer.cs b/Benchmark.NetCore/ManualDelayer.cs new file mode 100644 index 00000000..20897c7b --- /dev/null +++ b/Benchmark.NetCore/ManualDelayer.cs @@ -0,0 +1,55 @@ +using Prometheus; + +namespace Benchmark.NetCore; + +/// +/// A delayer implementation that only returns from a delay when commanded to. +/// +internal sealed class ManualDelayer : IDelayer, IDisposable +{ + public void BreakAllDelays() + { + lock (_lock) + { + _tcs.TrySetResult(); + + _tcs = new(TaskCreationOptions.RunContinuationsAsynchronously); + _delayTask = _tcs.Task; + } + } + + public void Dispose() + { + lock (_lock) + { + // If anything is still waiting, it shall not wait no more. + _tcs.TrySetResult(); + + // If anything will still wait in the future, it shall not wait at all. + // Beware of creating spinning loops if you dispose the delayer when something still expects to be delayed. + _delayTask = Task.CompletedTask; + } + } + + private readonly object _lock = new(); + private Task _delayTask; + private TaskCompletionSource _tcs; + + public ManualDelayer() + { + _tcs = new(TaskCreationOptions.RunContinuationsAsynchronously); + _delayTask = _tcs.Task; + } + + public Task Delay(TimeSpan duration) + { + lock (_lock) + return _delayTask; + } + + public Task Delay(TimeSpan duration, CancellationToken cancel) + { + lock (_lock) + return _delayTask.WaitAsync(cancel); + } +} diff --git a/Benchmark.NetCore/MetricCreationBenchmarks.cs b/Benchmark.NetCore/MetricCreationBenchmarks.cs index 3e823de3..09028762 100644 --- a/Benchmark.NetCore/MetricCreationBenchmarks.cs +++ b/Benchmark.NetCore/MetricCreationBenchmarks.cs @@ -10,7 +10,7 @@ namespace Benchmark.NetCore; [MemoryDiagnoser] // This seems to need a lot of warmup to stabilize. [WarmupCount(50)] -//[EventPipeProfiler(BenchmarkDotNet.Diagnosers.EventPipeProfile.CpuSampling)] +//[EventPipeProfiler(BenchmarkDotNet.Diagnosers.EventPipeProfile.GcVerbose)] public class MetricCreationBenchmarks { /// @@ -24,12 +24,6 @@ public class MetricCreationBenchmarks [Params(1, 10)] public int RepeatCount { get; set; } - /// - /// How many times we should try to register a metric that already exists. - /// - [Params(1, 10)] - public int DuplicateCount { get; set; } - [Params(true, false)] public bool IncludeStaticLabels { get; set; } @@ -47,6 +41,7 @@ static MetricCreationBenchmarks() private CollectorRegistry _registry; private IMetricFactory _factory; + private IManagedLifetimeMetricFactory _managedLifetimeFactory; [IterationSetup] public void Setup() @@ -75,6 +70,8 @@ public void Setup() { "static_gaa5", "static_bar" }, }); } + + _managedLifetimeFactory = _factory.WithManagedLifetime(expiresAfter: TimeSpan.FromHours(1)); } // We use the same strings both for the names and the values. @@ -86,14 +83,74 @@ public void Setup() private static readonly HistogramConfiguration _histogramConfiguration = HistogramConfiguration.Default; [Benchmark] - public void Counter() + public void Counter_ArrayLabels() + { + for (var i = 0; i < _metricCount; i++) + { + var metric = _factory.CreateCounter(_metricNames[i], _help, _labels, _counterConfiguration); + + for (var repeat = 0; repeat < RepeatCount; repeat++) + metric.WithLabels(_labels).Inc(); + } + } + + [Benchmark] + public void Counter_MemoryLabels() + { + // The overloads accepting string[] and ROM are functionally equivalent, though any conversion adds some overhead. + var labelsMemory = _labels.AsMemory(); + + for (var i = 0; i < _metricCount; i++) + { + var metric = _factory.CreateCounter(_metricNames[i], _help, _labels, _counterConfiguration); + + for (var repeat = 0; repeat < RepeatCount; repeat++) + metric.WithLabels(labelsMemory).Inc(); + } + } + + [Benchmark] + public void Counter_SpanLabels() + { + var labelsSpan = _labels.AsSpan(); + + for (var i = 0; i < _metricCount; i++) + { + var metric = _factory.CreateCounter(_metricNames[i], _help, _labels, _counterConfiguration); + + for (var repeat = 0; repeat < RepeatCount; repeat++) + // If code is aware that it is repeating the registration, using the Span overloads offers optimal performance. + metric.WithLabels(labelsSpan).Inc(); + } + } + + [Benchmark] + public void Counter_ManagedLifetime() { - for (var dupe = 0; dupe < DuplicateCount; dupe++) + // Typical usage for explicitly lifetime-managed metrics is to pass the label values as span, as they may already be known. + var labelsSpan = _labels.AsSpan(); + + for (var i = 0; i < _metricCount; i++) + { + var metric = _managedLifetimeFactory.CreateCounter(_metricNames[i], _help, _labels, _counterConfiguration); + + for (var repeat = 0; repeat < RepeatCount; repeat++) + metric.WithLease(static x => x.Inc(), labelsSpan); + } + } + + [Benchmark] + public void Counter_10Duplicates() + { + // We try to register the same metric 10 times, which is wasteful but shows us the overhead from doing this. + + for (var dupe = 0; dupe < 10; dupe++) for (var i = 0; i < _metricCount; i++) { var metric = _factory.CreateCounter(_metricNames[i], _help, _labels, _counterConfiguration); for (var repeat = 0; repeat < RepeatCount; repeat++) + // We do not use the Span overload here to exemplify "naive" code not aware of the repetition. metric.WithLabels(_labels).Inc(); } } @@ -101,40 +158,40 @@ public void Counter() [Benchmark] public void Gauge() { - for (var dupe = 0; dupe < DuplicateCount; dupe++) - for (var i = 0; i < _metricCount; i++) - { - var metric = _factory.CreateGauge(_metricNames[i], _help, _labels, _gaugeConfiguration); + for (var i = 0; i < _metricCount; i++) + { + var metric = _factory.CreateGauge(_metricNames[i], _help, _labels, _gaugeConfiguration); - for (var repeat = 0; repeat < RepeatCount; repeat++) - metric.WithLabels(_labels).Set(repeat); - } + for (var repeat = 0; repeat < RepeatCount; repeat++) + // We do not use the Span overload here to exemplify "naive" code not aware of the repetition. + metric.WithLabels(_labels).Set(repeat); + } } // Disabled because it is slow and Summary is a legacy metric type that is not recommended for new usage. //[Benchmark] public void Summary() { - for (var dupe = 0; dupe < DuplicateCount; dupe++) - for (var i = 0; i < _metricCount; i++) - { - var metric = _factory.CreateSummary(_metricNames[i], _help, _labels, _summaryConfiguration); + for (var i = 0; i < _metricCount; i++) + { + var metric = _factory.CreateSummary(_metricNames[i], _help, _labels, _summaryConfiguration); - for (var repeat = 0; repeat < RepeatCount; repeat++) - metric.WithLabels(_labels).Observe(123); - } + for (var repeat = 0; repeat < RepeatCount; repeat++) + // We do not use the Span overload here to exemplify "naive" code not aware of the repetition. + metric.WithLabels(_labels).Observe(123); + } } [Benchmark] public void Histogram() { - for (var dupe = 0; dupe < DuplicateCount; dupe++) - for (var i = 0; i < _metricCount; i++) - { - var metric = _factory.CreateHistogram(_metricNames[i], _help, _labels, _histogramConfiguration); + for (var i = 0; i < _metricCount; i++) + { + var metric = _factory.CreateHistogram(_metricNames[i], _help, _labels, _histogramConfiguration); - for (var repeat = 0; repeat < RepeatCount; repeat++) - metric.WithLabels(_labels).Observe(123); - } + for (var repeat = 0; repeat < RepeatCount; repeat++) + // We do not use the Span overload here to exemplify "naive" code not aware of the repetition. + metric.WithLabels(_labels).Observe(123); + } } } diff --git a/Benchmark.NetCore/MetricExpirationBenchmarks.cs b/Benchmark.NetCore/MetricExpirationBenchmarks.cs index f957ae0b..883ad131 100644 --- a/Benchmark.NetCore/MetricExpirationBenchmarks.cs +++ b/Benchmark.NetCore/MetricExpirationBenchmarks.cs @@ -1,6 +1,5 @@ using BenchmarkDotNet.Attributes; using Prometheus; -using Prometheus.Tests; namespace Benchmark.NetCore; @@ -9,34 +8,23 @@ namespace Benchmark.NetCore; /// [MemoryDiagnoser] // This seems to need a lot of warmup to stabilize. -[WarmupCount(80)] +[WarmupCount(50)] // This seems to need a lot of iterations to stabilize. -[IterationCount(100)] +[IterationCount(50)] //[EventPipeProfiler(BenchmarkDotNet.Diagnosers.EventPipeProfile.GcVerbose)] public class MetricExpirationBenchmarks { /// /// Just to ensure that a benchmark iteration has enough to do for stable and meaningful results. /// - private const int _metricCount = 1_000; + private const int _metricCount = 25_000; /// - /// Some benchmarks try to register metrics that already exist. - /// - private const int _duplicateCount = 5; - - /// - /// How many times we repeat acquiring and incrementing the same instance. - /// - [Params(1, 10)] - public int RepeatCount { get; set; } - - /// - /// If true, we preallocate a lifetime manager for every metric, so the benchmark only measures the actual usage - /// of the metric and not the creation, as these two components of a metric lifetime can have different impact in different cases. + /// If true, we preallocate a lifetime for every metric, so the benchmark only measures the actual usage + /// of the metric and not the first-lease setup, as these two components of a metric lifetime can have different impact in different cases. /// [Params(true, false)] - public bool PreallocateLifetimeManager { get; set; } + public bool PreallocateLifetime { get; set; } private const string _help = "arbitrary help message for metric, not relevant for benchmarking"; @@ -56,7 +44,9 @@ static MetricExpirationBenchmarks() // We use the same strings both for the names and the values. private static readonly string[] _labels = ["foo", "bar", "baz"]; - private BreakableDelayer _delayer; + private ManualDelayer _delayer; + + private readonly ManagedLifetimeMetricHandle[] _counters = new ManagedLifetimeMetricHandle[_metricCount]; [IterationSetup] public void Setup() @@ -66,27 +56,16 @@ public void Setup() // We enable lifetime management but set the expiration timer very high to avoid expiration in the middle of the benchmark. .WithManagedLifetime(expiresAfter: TimeSpan.FromHours(24)); - var regularFactory = Metrics.WithCustomRegistry(_registry); - - _delayer = new BreakableDelayer(); + _delayer = new(); - // We create non-expiring versions of the metrics to pre-warm the metrics registry. - // While not a realistic use case, it does help narrow down the benchmark workload to the actual part that is special about expiring metrics, - // which means we get more useful results from this (rather than with some metric allocation overhead mixed in). for (var i = 0; i < _metricCount; i++) { - var counter = regularFactory.CreateCounter(_metricNames[i], _help, _labels); - counter.WithLabels(_labels); - - // If the params say so, we even preallocate the lifetime manager to ensure that we only measure the usage of the metric. - // Both the usage and the lifetime manager allocation matter but we want to bring them out separately in the benchmarks. - if (PreallocateLifetimeManager) - { - var managedLifetimeCounter = CreateCounter(_metricNames[i], _help, _labels); + var counter = CreateCounter(_metricNames[i], _help, _labels); + _counters[i] = counter; - // And also take the first lease to pre-warm the lifetime manager. - managedLifetimeCounter.AcquireLease(out _, _labels).Dispose(); - } + // Both the usage and the lifetime allocation matter but we want to bring them out separately in the benchmarks. + if (PreallocateLifetime) + counter.AcquireRefLease(out _, _labels).Dispose(); } } @@ -95,16 +74,11 @@ public void Cleanup() { // Ensure that all metrics are marked as expired, so the expiration processing logic destroys them all. // This causes some extra work during cleanup but on the other hand, it ensures good isolation between iterations, so fine. - for (var i = 0; i < _metricCount; i++) - { - var counter = CreateCounter(_metricNames[i], _help, _labels); + foreach (var counter in _counters) counter.SetAllKeepaliveTimestampsToDistantPast(); - } // Twice and with some sleep time, just for good measure. - // BenchmarkDotNet today does not support async here, so we do a sync sleep or two. - _delayer.BreakAllDelays(); - Thread.Sleep(millisecondsTimeout: 5); + // BenchmarkDotNet today does not support async here, so we do a sleep to let the reaper thread process things. _delayer.BreakAllDelays(); Thread.Sleep(millisecondsTimeout: 5); } @@ -121,90 +95,70 @@ public void Cleanup() } [Benchmark] - public void CreateAndUse_AutoLease() + public void Use_AutoLease_Once() { for (var i = 0; i < _metricCount; i++) { - var metric = CreateCounter(_metricNames[i], _help, _labels).WithExtendLifetimeOnUse(); + var wrapper = _counters[i].WithExtendLifetimeOnUse(); - for (var repeat = 0; repeat < RepeatCount; repeat++) - metric.WithLabels(_labels).Inc(); + // Auto-leasing is used as a drop-in replacement in a context that is not aware the metric is lifetime-managed. + // This means the typical usage is to pass a string[] (or ROM) and not a span (which would be a hint that it already exists). + wrapper.WithLabels(_labels).Inc(); } } [Benchmark] - public void CreateAndUse_AutoLease_WithDuplicates() + public void Use_AutoLease_With10Duplicates() { - for (var dupe = 0; dupe < _duplicateCount; dupe++) + for (var dupe = 0; dupe < 10; dupe++) for (var i = 0; i < _metricCount; i++) { - var metric = CreateCounter(_metricNames[i], _help, _labels).WithExtendLifetimeOnUse(); + var wrapper = _counters[i].WithExtendLifetimeOnUse(); - for (var repeat = 0; repeat < RepeatCount; repeat++) - metric.WithLabels(_labels).Inc(); + // Auto-leasing is used as a drop-in replacement in a context that is not aware the metric is lifetime-managed. + // This means the typical usage is to pass a string[] (or ROM) and not a span (which would be a hint that it already exists). + wrapper.WithLabels(_labels).Inc(); } } - [Benchmark(Baseline = true)] - public void CreateAndUse_ManualLease() + [Benchmark] + public void Use_AutoLease_Once_With10Repeats() { for (var i = 0; i < _metricCount; i++) { - var counter = CreateCounter(_metricNames[i], _help, _labels); + var wrapper = _counters[i].WithExtendLifetimeOnUse(); - for (var repeat = 0; repeat < RepeatCount; repeat++) - { - using var lease = counter.AcquireLease(out var instance, _labels); - instance.Inc(); - } + for (var repeat = 0; repeat < 10; repeat++) + // Auto-leasing is used as a drop-in replacement in a context that is not aware the metric is lifetime-managed. + // This means the typical usage is to pass a string[] (or ROM) and not a span (which would be a hint that it already exists). + wrapper.WithLabels(_labels).Inc(); } } - [Benchmark] - public void CreateAndUse_ManualLease_WithDuplicates() + [Benchmark(Baseline = true)] + public void Use_ManualLease() { - for (var dupe = 0; dupe < _duplicateCount; dupe++) - for (var i = 0; i < _metricCount; i++) - { - var counter = CreateCounter(_metricNames[i], _help, _labels); - - for (var repeat = 0; repeat < RepeatCount; repeat++) - { - using var lease = counter.AcquireLease(out var instance, _labels); - instance.Inc(); - } - } - } + // Typical usage for explicitly lifetime-managed metrics is to pass the label values as span, as they may already be known. + var labelValues = _labels.AsSpan(); - //[Benchmark] - public void CreateAndUse_ManualRefLease() - { for (var i = 0; i < _metricCount; i++) { - var counter = CreateCounter(_metricNames[i], _help, _labels); - - for (var repeat = 0; repeat < RepeatCount; repeat++) - { - using var lease = counter.AcquireRefLease(out var instance, _labels); - instance.Inc(); - } + using var lease = _counters[i].AcquireLease(out var instance, labelValues); + instance.Inc(); } } [Benchmark] - public void CreateAndUse_ManualRefLease_WithDuplicates() + public void Use_ManualRefLease() { - for (var dupe = 0; dupe < _duplicateCount; dupe++) - for (var i = 0; i < _metricCount; i++) - { - var counter = CreateCounter(_metricNames[i], _help, _labels); + // Typical usage for explicitly lifetime-managed metrics is to pass the label values as span, as they may already be known. + var labelValues = _labels.AsSpan(); - for (var repeat = 0; repeat < RepeatCount; repeat++) - { - using var lease = counter.AcquireRefLease(out var instance, _labels); - instance.Inc(); - } - } + for (var i = 0; i < _metricCount; i++) + { + using var lease = _counters[i].AcquireRefLease(out var instance, labelValues); + instance.Inc(); + } } private static void IncrementCounter(ICounter counter) @@ -213,35 +167,15 @@ private static void IncrementCounter(ICounter counter) } [Benchmark] - public void CreateAndUse_WithLease() + public void Use_WithLease() { - // Reuse the delegate. - Action incrementCounterAction = IncrementCounter; + // Typical usage for explicitly lifetime-managed metrics is to pass the label values as span, as they may already be known. + var labelValues = _labels.AsSpan(); - for (var i = 0; i < _metricCount; i++) - { - var counter = CreateCounter(_metricNames[i], _help, _labels); - - for (var repeat = 0; repeat < RepeatCount; repeat++) - { - counter.WithLease(incrementCounterAction, _labels); - } - } - } - - [Benchmark] - public void CreateAndUse_WithLease_WithDuplicates() - { // Reuse the delegate. Action incrementCounterAction = IncrementCounter; - for (var dupe = 0; dupe < _duplicateCount; dupe++) - for (var i = 0; i < _metricCount; i++) - { - var counter = CreateCounter(_metricNames[i], _help, _labels); - - for (var repeat = 0; repeat < RepeatCount; repeat++) - counter.WithLease(incrementCounterAction, _labels); - } + for (var i = 0; i < _metricCount; i++) + _counters[i].WithLease(incrementCounterAction, labelValues); } } diff --git a/Benchmark.NetCore/MetricPusherBenchmarks.cs b/Benchmark.NetCore/MetricPusherBenchmarks.cs index 1d946831..83943efa 100644 --- a/Benchmark.NetCore/MetricPusherBenchmarks.cs +++ b/Benchmark.NetCore/MetricPusherBenchmarks.cs @@ -3,77 +3,76 @@ using Prometheus; using tester; -namespace Benchmark.NetCore +namespace Benchmark.NetCore; + +// NB! This benchmark requires the Tester project to be running and the MetricPusherTester module to be active (to receive the data). +// If there is no tester listening, the results will be overly good because the runtime is under less I/O load. +[MemoryDiagnoser] +[SimpleJob(RunStrategy.Monitoring, warmupCount: 0)] +public class MetricPusherBenchmarks { - // NB! This benchmark requires the Tester project to be running and the MetricPusherTester module to be active (to receive the data). - // If there is no tester listening, the results will be overly good because the runtime is under less I/O load. - [MemoryDiagnoser] - [SimpleJob(RunStrategy.Monitoring, warmupCount: 0)] - public class MetricPusherBenchmarks + private static string MetricPusherUrl = $"http://localhost:{TesterConstants.TesterPort}"; + + [GlobalSetup] + public async Task GlobalSetup() { - private static string MetricPusherUrl = $"http://localhost:{TesterConstants.TesterPort}"; + // Verify that there is a MetricPusher listening on Tester. - [GlobalSetup] - public async Task GlobalSetup() + using (var client = new HttpClient()) { - // Verify that there is a MetricPusher listening on Tester. - - using (var client = new HttpClient()) + try { - try - { - var result = await client.GetAsync(MetricPusherUrl); - result.EnsureSuccessStatusCode(); - } - catch - { - throw new Exception("You must start the Tester.NetCore project and configure it to use MetricPusherTester in its Program.cs before running this benchmark."); - } + var result = await client.GetAsync(MetricPusherUrl); + result.EnsureSuccessStatusCode(); + } + catch + { + throw new Exception("You must start the Tester.NetCore project and configure it to use MetricPusherTester in its Program.cs before running this benchmark."); } } + } - [Benchmark] - public async Task PushTest() - { - var registry = Metrics.NewCustomRegistry(); - var factory = Metrics.WithCustomRegistry(registry); + [Benchmark] + public async Task PushTest() + { + var registry = Metrics.NewCustomRegistry(); + var factory = Metrics.WithCustomRegistry(registry); - var pusher = new MetricPusher(new MetricPusherOptions - { - Endpoint = MetricPusherUrl, - Registry = registry, - IntervalMilliseconds = 30, - Job = "job" - }); - pusher.Start(); + var pusher = new MetricPusher(new MetricPusherOptions + { + Endpoint = MetricPusherUrl, + Registry = registry, + IntervalMilliseconds = 30, + Job = "job" + }); + pusher.Start(); - var counters = new List(); - for (int i = 0; i < 1000; i++) - { - var counter = factory.CreateCounter($"Counter{i}", String.Empty); - counters.Add(counter); - } + var counters = new List(); + for (int i = 0; i < 1000; i++) + { + var counter = factory.CreateCounter($"Counter{i}", String.Empty); + counters.Add(counter); + } - var ct = new CancellationTokenSource(); - var incTask = Task.Run(async () => + var ct = new CancellationTokenSource(); + var incTask = Task.Run(async () => + { + while (!ct.IsCancellationRequested) { - while (!ct.IsCancellationRequested) + foreach (var counter in counters) { - foreach (var counter in counters) - { - counter.Inc(); - } - - await Task.Delay(30); + counter.Inc(); } - }); - await Task.Delay(5000); - ct.Cancel(); - await incTask; + await Task.Delay(30); + } + }); - pusher.Stop(); - } + await Task.Delay(5000); + ct.Cancel(); + await incTask; + + pusher.Stop(); } } \ No newline at end of file diff --git a/Benchmark.NetCore/SummaryBenchmarks.cs b/Benchmark.NetCore/SummaryBenchmarks.cs index a4169737..1a42a91a 100644 --- a/Benchmark.NetCore/SummaryBenchmarks.cs +++ b/Benchmark.NetCore/SummaryBenchmarks.cs @@ -1,77 +1,76 @@ using BenchmarkDotNet.Attributes; using Prometheus; -namespace Benchmark.NetCore +namespace Benchmark.NetCore; + +/// +/// Summary can be quite expensive to use due to its quantile measurement logic. +/// This benchmark helps get a grip on the facts. +/// +[MemoryDiagnoser] +public class SummaryBenchmarks { - /// - /// Summary can be quite expensive to use due to its quantile measurement logic. - /// This benchmark helps get a grip on the facts. - /// - [MemoryDiagnoser] - public class SummaryBenchmarks + // Arbitrary but reasonable objectices we might use for a summary. + private static readonly QuantileEpsilonPair[] Objectives = new[] { - // Arbitrary but reasonable objectices we might use for a summary. - private static readonly QuantileEpsilonPair[] Objectives = new[] - { - new QuantileEpsilonPair(0.5, 0.05), - new QuantileEpsilonPair(0.9, 0.01), - new QuantileEpsilonPair(0.95, 0.01), - new QuantileEpsilonPair(0.99, 0.005) - }; + new QuantileEpsilonPair(0.5, 0.05), + new QuantileEpsilonPair(0.9, 0.01), + new QuantileEpsilonPair(0.95, 0.01), + new QuantileEpsilonPair(0.99, 0.005) + }; - // We pre-generate some random data that we feed into the benchmark, to avoid measuring data generation. - private static readonly double[] Values = new double[1 * 1024 * 1024]; + // We pre-generate some random data that we feed into the benchmark, to avoid measuring data generation. + private static readonly double[] Values = new double[1 * 1024 * 1024]; - private static readonly TimeSpan ExportInterval = TimeSpan.FromMinutes(1); + private static readonly TimeSpan ExportInterval = TimeSpan.FromMinutes(1); - static SummaryBenchmarks() - { - var rnd = new Random(); + static SummaryBenchmarks() + { + var rnd = new Random(); - for (var i = 0; i < Values.Length; i++) - Values[i] = rnd.NextDouble(); - } + for (var i = 0; i < Values.Length; i++) + Values[i] = rnd.NextDouble(); + } - private CollectorRegistry _registry; - private MetricFactory _factory; + private CollectorRegistry _registry; + private MetricFactory _factory; - [IterationSetup] - public void Setup() - { - _registry = Metrics.NewCustomRegistry(); - _factory = Metrics.WithCustomRegistry(_registry); - } + [IterationSetup] + public void Setup() + { + _registry = Metrics.NewCustomRegistry(); + _factory = Metrics.WithCustomRegistry(_registry); + } - [Params(1, 10, 100, 1000, 10000)] - public int MeasurementsPerSecond { get; set; } + [Params(1, 10, 100, 1000, 10000)] + public int MeasurementsPerSecond { get; set; } - [Benchmark] - public async Task Summary_NPerSecond_For10Minutes() + [Benchmark] + public async Task Summary_NPerSecond_For10Minutes() + { + var summary = _factory.CreateSummary("metric_name", "help_string", new SummaryConfiguration { - var summary = _factory.CreateSummary("metric_name", "help_string", new SummaryConfiguration - { - Objectives = Objectives - }); + Objectives = Objectives + }); - var now = DateTime.UtcNow; + var now = DateTime.UtcNow; - // We start far enough back to cover the entire age range of the summary. - var t = now - Summary.DefMaxAge; - var lastExport = t; + // We start far enough back to cover the entire age range of the summary. + var t = now - Summary.DefMaxAge; + var lastExport = t; - while (t < now) - { - for (var i = 0; i < MeasurementsPerSecond; i++) - summary.Observe(Values[i % Values.Length]); + while (t < now) + { + for (var i = 0; i < MeasurementsPerSecond; i++) + summary.Observe(Values[i % Values.Length]); - t += TimeSpan.FromSeconds(1); + t += TimeSpan.FromSeconds(1); - if (lastExport + ExportInterval <= t) - { - lastExport = t; + if (lastExport + ExportInterval <= t) + { + lastExport = t; - await summary.CollectAndSerializeAsync(new TextSerializer(Stream.Null), true, default); - } + await summary.CollectAndSerializeAsync(new TextSerializer(Stream.Null), true, default); } } } diff --git a/Prometheus/ICollector.cs b/Prometheus/ICollector.cs index 7fd7cf8d..6b162fbc 100644 --- a/Prometheus/ICollector.cs +++ b/Prometheus/ICollector.cs @@ -8,6 +8,8 @@ public interface ICollector : ICollector { TChild Unlabelled { get; } TChild WithLabels(params string[] labelValues); + TChild WithLabels(ReadOnlyMemory labelValues); + TChild WithLabels(ReadOnlySpan labelValues); } /// diff --git a/Prometheus/LabelEnrichingAutoLeasingMetric.cs b/Prometheus/LabelEnrichingAutoLeasingMetric.cs index 6228b5d3..b7ad7f50 100644 --- a/Prometheus/LabelEnrichingAutoLeasingMetric.cs +++ b/Prometheus/LabelEnrichingAutoLeasingMetric.cs @@ -1,4 +1,6 @@ -namespace Prometheus; +using System.Buffers; + +namespace Prometheus; internal sealed class LabelEnrichingAutoLeasingMetric : ICollector where TMetric : ICollectorChild @@ -6,18 +8,75 @@ internal sealed class LabelEnrichingAutoLeasingMetric : ICollector inner, string[] enrichWithLabelValues) { _inner = inner; - _enrichedLabelValues = enrichWithLabelValues; + _enrichWithLabelValues = enrichWithLabelValues; } private readonly ICollector _inner; - private readonly string[] _enrichedLabelValues; + private readonly string[] _enrichWithLabelValues; + + public TMetric Unlabelled + { + get + { + // If we are not provided any custom label values, we can be pretty sure the label values are not going to change + // between calls, so reuse a buffer to avoid allocations when passing the data to the inner instance. + var buffer = ArrayPool.Shared.Rent(_enrichWithLabelValues.Length); + + try + { + _enrichWithLabelValues.CopyTo(buffer, 0); + var finalLabelValues = buffer.AsSpan(0, _enrichWithLabelValues.Length); + + return _inner.WithLabels(finalLabelValues); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } + } - public TMetric Unlabelled => _inner.WithLabels(_enrichedLabelValues); public string Name => _inner.Name; public string Help => _inner.Help; // We do not display the enriched labels, they are transparent - this is only the instance-specific label names. public string[] LabelNames => _inner.LabelNames; - public TMetric WithLabels(params string[] labelValues) => _inner.WithLabels(_enrichedLabelValues.Concat(labelValues).ToArray()); + public TMetric WithLabels(params string[] labelValues) + { + // The caller passing us string[] does not signal that the allocation is not needed - in all likelihood it is. + // However, we do not want to allocate two arrays here (because we need to concatenate as well) so instead we + // use the reusable-buffer overload to avoid at least one of the allocations. + + return WithLabels(labelValues.AsSpan()); + } + + public TMetric WithLabels(ReadOnlyMemory labelValues) + { + // The caller passing us ReadOnlyMemory does not signal that the allocation is not needed - in all likelihood it is. + // However, we do not want to allocate two arrays here (because we need to concatenate as well) so instead we + // use the reusable-buffer overload to avoid at least one of the allocations. + + return WithLabels(labelValues.Span); + } + + public TMetric WithLabels(ReadOnlySpan labelValues) + { + // The ReadOnlySpan overload suggests that the label values may already be known to the metric, + // so we should strongly avoid allocating memory here. Thus we copy everything to a reusable buffer. + var buffer = ArrayPool.Shared.Rent(_enrichWithLabelValues.Length + labelValues.Length); + + try + { + _enrichWithLabelValues.CopyTo(buffer, 0); + labelValues.CopyTo(buffer.AsSpan(_enrichWithLabelValues.Length)); + var finalLabelValues = buffer.AsSpan(0, _enrichWithLabelValues.Length + labelValues.Length); + + return _inner.WithLabels(finalLabelValues); + } + finally + { + ArrayPool.Shared.Return(buffer); + } + } } diff --git a/Prometheus/ManagedLifetimeCounter.cs b/Prometheus/ManagedLifetimeCounter.cs index 327b8fe0..6e55b1c0 100644 --- a/Prometheus/ManagedLifetimeCounter.cs +++ b/Prometheus/ManagedLifetimeCounter.cs @@ -36,28 +36,31 @@ public ManagedLifetimeCounter(Collector metric, TimeSpan expiresA // These do not get cached, so are potentially expensive - user code should try avoiding re-allocating these when possible, // though admittedly this may not be so easy as often these are on the hot path and the very reason that lifetime-managed // metrics are used is that we do not have a meaningful way to reuse metrics or identify their lifetime. - public ICounter WithLabels(params string[] labelValues) + public ICounter WithLabels(params string[] labelValues) => WithLabels(labelValues.AsMemory()); + + public ICounter WithLabels(ReadOnlyMemory labelValues) { return new AutoLeasingInstance(this, labelValues); } + + public ICounter WithLabels(ReadOnlySpan labelValues) + { + // We are allocating a long-lived auto-leasing wrapper here, so there is no way we can just use the span directly. + // We must copy it to a long-lived array. Another reason to avoid re-allocating these as much as possible. + return new AutoLeasingInstance(this, labelValues.ToArray()); + } #endregion private sealed class AutoLeasingInstance : ICounter { - static AutoLeasingInstance() - { - _incCoreFunc = IncCore; - _incToCoreFunc = IncToCore; - } - - public AutoLeasingInstance(IManagedLifetimeMetricHandle inner, string[] labelValues) + public AutoLeasingInstance(IManagedLifetimeMetricHandle inner, ReadOnlyMemory labelValues) { _inner = inner; _labelValues = labelValues; } private readonly IManagedLifetimeMetricHandle _inner; - private readonly string[] _labelValues; + private readonly ReadOnlyMemory _labelValues; public double Value => throw new NotSupportedException("Read operations on a lifetime-extending-on-use expiring metric are not supported."); @@ -67,7 +70,9 @@ public AutoLeasingInstance(IManagedLifetimeMetricHandle inner, string[ public void Inc(double increment, Exemplar? exemplar) { var args = new IncArgs(increment, exemplar); - _inner.WithLease(_incCoreFunc, args, _labelValues); + + // We use the Span overload to signal that we expect the label values to be known already. + _inner.WithLease(_incCoreFunc, args, _labelValues.Span); } private readonly struct IncArgs(double increment, Exemplar? exemplar) @@ -77,12 +82,14 @@ private readonly struct IncArgs(double increment, Exemplar? exemplar) } private static void IncCore(IncArgs args, ICounter counter) => counter.Inc(args.Increment, args.Exemplar); - private static readonly Action _incCoreFunc; + private static readonly Action _incCoreFunc = IncCore; public void IncTo(double targetValue) { var args = new IncToArgs(targetValue); - _inner.WithLease(_incToCoreFunc, args, _labelValues); + + // We use the Span overload to signal that we expect the label values to be known already. + _inner.WithLease(_incToCoreFunc, args, _labelValues.Span); } private readonly struct IncToArgs(double targetValue) @@ -91,6 +98,6 @@ private readonly struct IncToArgs(double targetValue) } private static void IncToCore(IncToArgs args, ICounter counter) => counter.IncTo(args.TargetValue); - private static readonly Action _incToCoreFunc; + private static readonly Action _incToCoreFunc = IncToCore; } } diff --git a/Prometheus/ManagedLifetimeGauge.cs b/Prometheus/ManagedLifetimeGauge.cs index a4642db8..c0efd7b6 100644 --- a/Prometheus/ManagedLifetimeGauge.cs +++ b/Prometheus/ManagedLifetimeGauge.cs @@ -36,38 +36,40 @@ public ManagedLifetimeGauge(Collector metric, TimeSpan expiresAfter // These do not get cached, so are potentially expensive - user code should try avoiding re-allocating these when possible, // though admittedly this may not be so easy as often these are on the hot path and the very reason that lifetime-managed // metrics are used is that we do not have a meaningful way to reuse metrics or identify their lifetime. - public IGauge WithLabels(params string[] labelValues) + public IGauge WithLabels(params string[] labelValues) => WithLabels(labelValues.AsMemory()); + + public IGauge WithLabels(ReadOnlyMemory labelValues) { return new AutoLeasingInstance(this, labelValues); } + + public IGauge WithLabels(ReadOnlySpan labelValues) + { + // We are allocating a long-lived auto-leasing wrapper here, so there is no way we can just use the span directly. + // We must copy it to a long-lived array. Another reason to avoid re-allocating these as much as possible. + return new AutoLeasingInstance(this, labelValues.ToArray()); + } #endregion private sealed class AutoLeasingInstance : IGauge { - static AutoLeasingInstance() - { - _incCoreFunc = IncCore; - _incToCoreFunc = IncToCore; - _setCoreFunc = SetCore; - _decCoreFunc = DecCore; - _decToCoreFunc = DecToCore; - } - - public AutoLeasingInstance(IManagedLifetimeMetricHandle inner, string[] labelValues) + public AutoLeasingInstance(IManagedLifetimeMetricHandle inner, ReadOnlyMemory labelValues) { _inner = inner; _labelValues = labelValues; } private readonly IManagedLifetimeMetricHandle _inner; - private readonly string[] _labelValues; + private readonly ReadOnlyMemory _labelValues; public double Value => throw new NotSupportedException("Read operations on a lifetime-extending-on-use expiring metric are not supported."); public void Inc(double increment = 1) { var args = new IncArgs(increment); - _inner.WithLease(_incCoreFunc, args, _labelValues); + + // We use the Span overload to signal that we expect the label values to be known already. + _inner.WithLease(_incCoreFunc, args, _labelValues.Span); } private readonly struct IncArgs(double increment) @@ -76,12 +78,14 @@ private readonly struct IncArgs(double increment) } private static void IncCore(IncArgs args, IGauge gauge) => gauge.Inc(args.Increment); - private static readonly Action _incCoreFunc; + private static readonly Action _incCoreFunc = IncCore; public void Set(double val) { var args = new SetArgs(val); - _inner.WithLease(_setCoreFunc, args, _labelValues); + + // We use the Span overload to signal that we expect the label values to be known already. + _inner.WithLease(_setCoreFunc, args, _labelValues.Span); } private readonly struct SetArgs(double val) @@ -90,12 +94,14 @@ private readonly struct SetArgs(double val) } private static void SetCore(SetArgs args, IGauge gauge) => gauge.Set(args.Val); - private static readonly Action _setCoreFunc; + private static readonly Action _setCoreFunc = SetCore; public void Dec(double decrement = 1) { var args = new DecArgs(decrement); - _inner.WithLease(_decCoreFunc, args, _labelValues); + + // We use the Span overload to signal that we expect the label values to be known already. + _inner.WithLease(_decCoreFunc, args, _labelValues.Span); } private readonly struct DecArgs(double decrement) @@ -104,12 +110,14 @@ private readonly struct DecArgs(double decrement) } private static void DecCore(DecArgs args, IGauge gauge) => gauge.Dec(args.Decrement); - private static readonly Action _decCoreFunc; + private static readonly Action _decCoreFunc = DecCore; public void IncTo(double targetValue) { var args = new IncToArgs(targetValue); - _inner.WithLease(_incToCoreFunc, args, _labelValues); + + // We use the Span overload to signal that we expect the label values to be known already. + _inner.WithLease(_incToCoreFunc, args, _labelValues.Span); } private readonly struct IncToArgs(double targetValue) @@ -118,12 +126,14 @@ private readonly struct IncToArgs(double targetValue) } private static void IncToCore(IncToArgs args, IGauge gauge) => gauge.IncTo(args.TargetValue); - private static readonly Action _incToCoreFunc; + private static readonly Action _incToCoreFunc = IncToCore; public void DecTo(double targetValue) { var args = new DecToArgs(targetValue); - _inner.WithLease(_decToCoreFunc, args, _labelValues); + + // We use the Span overload to signal that we expect the label values to be known already. + _inner.WithLease(_decToCoreFunc, args, _labelValues.Span); } private readonly struct DecToArgs(double targetValue) @@ -132,6 +142,6 @@ private readonly struct DecToArgs(double targetValue) } private static void DecToCore(DecToArgs args, IGauge gauge) => gauge.DecTo(args.TargetValue); - private static readonly Action _decToCoreFunc; + private static readonly Action _decToCoreFunc = DecToCore; } } diff --git a/Prometheus/ManagedLifetimeHistogram.cs b/Prometheus/ManagedLifetimeHistogram.cs index 678697f2..6971fe46 100644 --- a/Prometheus/ManagedLifetimeHistogram.cs +++ b/Prometheus/ManagedLifetimeHistogram.cs @@ -36,28 +36,31 @@ public ManagedLifetimeHistogram(Collector metric, TimeSpan expi // These do not get cached, so are potentially expensive - user code should try avoiding re-allocating these when possible, // though admittedly this may not be so easy as often these are on the hot path and the very reason that lifetime-managed // metrics are used is that we do not have a meaningful way to reuse metrics or identify their lifetime. - public IHistogram WithLabels(params string[] labelValues) + public IHistogram WithLabels(params string[] labelValues) => WithLabels(labelValues.AsMemory()); + + public IHistogram WithLabels(ReadOnlyMemory labelValues) { return new AutoLeasingInstance(this, labelValues); } + + public IHistogram WithLabels(ReadOnlySpan labelValues) + { + // We are allocating a long-lived auto-leasing wrapper here, so there is no way we can just use the span directly. + // We must copy it to a long-lived array. Another reason to avoid re-allocating these as much as possible. + return new AutoLeasingInstance(this, labelValues.ToArray()); + } #endregion private sealed class AutoLeasingInstance : IHistogram { - static AutoLeasingInstance() - { - _observeValCountCoreFunc = ObserveValCountCore; - _observeValExemplarCoreFunc = ObserveValExemplarCore; - } - - public AutoLeasingInstance(IManagedLifetimeMetricHandle inner, string[] labelValues) + public AutoLeasingInstance(IManagedLifetimeMetricHandle inner, ReadOnlyMemory labelValues) { _inner = inner; _labelValues = labelValues; } private readonly IManagedLifetimeMetricHandle _inner; - private readonly string[] _labelValues; + private readonly ReadOnlyMemory _labelValues; public double Sum => throw new NotSupportedException("Read operations on a lifetime-extending-on-use expiring metric are not supported."); public long Count => throw new NotSupportedException("Read operations on a lifetime-extending-on-use expiring metric are not supported."); @@ -65,7 +68,9 @@ public AutoLeasingInstance(IManagedLifetimeMetricHandle inner, strin public void Observe(double val, long count) { var args = new ObserveValCountArgs(val, count); - _inner.WithLease(_observeValCountCoreFunc, args, _labelValues); + + // We use the Span overload to signal that we expect the label values to be known already. + _inner.WithLease(_observeValCountCoreFunc, args, _labelValues.Span); } private readonly struct ObserveValCountArgs(double val, long count) @@ -75,12 +80,14 @@ private readonly struct ObserveValCountArgs(double val, long count) } private static void ObserveValCountCore(ObserveValCountArgs args, IHistogram histogram) => histogram.Observe(args.Val, args.Count); - private static readonly Action _observeValCountCoreFunc; + private static readonly Action _observeValCountCoreFunc = ObserveValCountCore; public void Observe(double val, Exemplar? exemplar) { var args = new ObserveValExemplarArgs(val, exemplar); - _inner.WithLease(_observeValExemplarCoreFunc, args, _labelValues); + + // We use the Span overload to signal that we expect the label values to be known already. + _inner.WithLease(_observeValExemplarCoreFunc, args, _labelValues.Span); } private readonly struct ObserveValExemplarArgs(double val, Exemplar? exemplar) @@ -90,7 +97,7 @@ private readonly struct ObserveValExemplarArgs(double val, Exemplar? exemplar) } private static void ObserveValExemplarCore(ObserveValExemplarArgs args, IHistogram histogram) => histogram.Observe(args.Val, args.Exemplar); - private static readonly Action _observeValExemplarCoreFunc; + private static readonly Action _observeValExemplarCoreFunc = ObserveValExemplarCore; public void Observe(double val) { diff --git a/Prometheus/ManagedLifetimeSummary.cs b/Prometheus/ManagedLifetimeSummary.cs index 5260a5a0..5cb1f8d0 100644 --- a/Prometheus/ManagedLifetimeSummary.cs +++ b/Prometheus/ManagedLifetimeSummary.cs @@ -36,32 +36,38 @@ public ManagedLifetimeSummary(Collector metric, TimeSpan expiresA // These do not get cached, so are potentially expensive - user code should try avoiding re-allocating these when possible, // though admittedly this may not be so easy as often these are on the hot path and the very reason that lifetime-managed // metrics are used is that we do not have a meaningful way to reuse metrics or identify their lifetime. - public ISummary WithLabels(params string[] labelValues) + public ISummary WithLabels(params string[] labelValues) => WithLabels(labelValues.AsMemory()); + + public ISummary WithLabels(ReadOnlyMemory labelValues) { return new AutoLeasingInstance(this, labelValues); } + + public ISummary WithLabels(ReadOnlySpan labelValues) + { + // We are allocating a long-lived auto-leasing wrapper here, so there is no way we can just use the span directly. + // We must copy it to a long-lived array. Another reason to avoid re-allocating these as much as possible. + return new AutoLeasingInstance(this, labelValues.ToArray()); + } #endregion private sealed class AutoLeasingInstance : ISummary { - static AutoLeasingInstance() - { - _observeCoreFunc = ObserveCore; - } - - public AutoLeasingInstance(IManagedLifetimeMetricHandle inner, string[] labelValues) + public AutoLeasingInstance(IManagedLifetimeMetricHandle inner, ReadOnlyMemory labelValues) { _inner = inner; _labelValues = labelValues; } private readonly IManagedLifetimeMetricHandle _inner; - private readonly string[] _labelValues; + private readonly ReadOnlyMemory _labelValues; public void Observe(double val) { var args = new ObserveArgs(val); - _inner.WithLease(_observeCoreFunc, args, _labelValues); + + // We use the Span overload to signal that we expect the label values to be known already. + _inner.WithLease(_observeCoreFunc, args, _labelValues.Span); } private readonly struct ObserveArgs(double val) @@ -70,6 +76,6 @@ private readonly struct ObserveArgs(double val) } private static void ObserveCore(ObserveArgs args, ISummary summary) => summary.Observe(args.Val); - private static readonly Action _observeCoreFunc; + private static readonly Action _observeCoreFunc = ObserveCore; } } diff --git a/Tests.NetCore/BreakableDelayer.cs b/Tests.NetCore/BreakableDelayer.cs index 701525b4..04e2cc99 100644 --- a/Tests.NetCore/BreakableDelayer.cs +++ b/Tests.NetCore/BreakableDelayer.cs @@ -2,77 +2,76 @@ using System.Threading; using System.Threading.Tasks; -namespace Prometheus.Tests +namespace Prometheus.Tests; + +/// +/// A delayer that seems to work as usual except it can be instructed to end all waits immediately. +/// +/// +/// Thread-safe. +/// +public sealed class BreakableDelayer : IDelayer { /// - /// A delayer that seems to work as usual except it can be instructed to end all waits immediately. + /// Ends all delays and pretends the timers all elapsed. /// - /// - /// Thread-safe. - /// - public sealed class BreakableDelayer : IDelayer + public void BreakAllDelays() { - /// - /// Ends all delays and pretends the timers all elapsed. - /// - public void BreakAllDelays() + CancellationTokenSource old; + + lock (_lock) { - CancellationTokenSource old; + // Have to replace CTS first to ensure that any new calls get new CTS. + // Very important because canceling the CTS actually executes code until the next await. + old = _cts; + _cts = new CancellationTokenSource(); + } - lock (_lock) - { - // Have to replace CTS first to ensure that any new calls get new CTS. - // Very important because canceling the CTS actually executes code until the next await. - old = _cts; - _cts = new CancellationTokenSource(); - } + old.Cancel(); + old.Dispose(); + } - old.Cancel(); - old.Dispose(); - } + private CancellationTokenSource _cts = new CancellationTokenSource(); + private readonly object _lock = new object(); - private CancellationTokenSource _cts = new CancellationTokenSource(); - private readonly object _lock = new object(); + public async Task Delay(TimeSpan duration) + { + CancellationToken cancel; + + lock (_lock) + cancel = _cts.Token; - public async Task Delay(TimeSpan duration) + try { - CancellationToken cancel; + await Task.Delay(duration, cancel); + } + catch (TaskCanceledException) + { + } + } - lock (_lock) - cancel = _cts.Token; + public async Task Delay(TimeSpan duration, CancellationToken requestedCancel) + { + CancellationTokenSource callCts; + CancellationToken cancel; - try - { - await Task.Delay(duration, cancel); - } - catch (TaskCanceledException) - { - } + lock (_lock) + { + callCts = CancellationTokenSource.CreateLinkedTokenSource(_cts.Token, requestedCancel); + cancel = callCts.Token; } - public async Task Delay(TimeSpan duration, CancellationToken requestedCancel) + try { - CancellationTokenSource callCts; - CancellationToken cancel; - - lock (_lock) - { - callCts = CancellationTokenSource.CreateLinkedTokenSource(_cts.Token, requestedCancel); - cancel = callCts.Token; - } - - try - { - await Task.Delay(duration, cancel); - } - catch (TaskCanceledException) - { - requestedCancel.ThrowIfCancellationRequested(); - } - finally - { - callCts.Dispose(); - } + await Task.Delay(duration, cancel); + } + catch (TaskCanceledException) + { + requestedCancel.ThrowIfCancellationRequested(); + } + finally + { + callCts.Dispose(); } } } From 36b47502f15c2458e0cc7b06ace6f3e7053a8cb5 Mon Sep 17 00:00:00 2001 From: Christian Schulz <32000301+Christian-Schulz@users.noreply.github.com> Date: Tue, 5 Dec 2023 14:45:31 +0100 Subject: [PATCH 227/230] Create default gauge only if needed (#444) Co-authored-by: Sander Saares --- .../PrometheusHealthCheckPublisher.cs | 2 +- .../PrometheusHealthCheckPublisherOptions.cs | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Prometheus.AspNetCore.HealthChecks/PrometheusHealthCheckPublisher.cs b/Prometheus.AspNetCore.HealthChecks/PrometheusHealthCheckPublisher.cs index 04d87c46..95b08115 100644 --- a/Prometheus.AspNetCore.HealthChecks/PrometheusHealthCheckPublisher.cs +++ b/Prometheus.AspNetCore.HealthChecks/PrometheusHealthCheckPublisher.cs @@ -11,7 +11,7 @@ internal sealed class PrometheusHealthCheckPublisher : IHealthCheckPublisher public PrometheusHealthCheckPublisher(PrometheusHealthCheckPublisherOptions? options) { - _checkStatus = options?.Gauge ?? new PrometheusHealthCheckPublisherOptions().Gauge; + _checkStatus = options?.Gauge ?? new PrometheusHealthCheckPublisherOptions().GetDefaultGauge(); } public Task PublishAsync(HealthReport report, CancellationToken cancellationToken) diff --git a/Prometheus.AspNetCore.HealthChecks/PrometheusHealthCheckPublisherOptions.cs b/Prometheus.AspNetCore.HealthChecks/PrometheusHealthCheckPublisherOptions.cs index 0f640ee4..df2c5400 100644 --- a/Prometheus.AspNetCore.HealthChecks/PrometheusHealthCheckPublisherOptions.cs +++ b/Prometheus.AspNetCore.HealthChecks/PrometheusHealthCheckPublisherOptions.cs @@ -5,5 +5,10 @@ public sealed class PrometheusHealthCheckPublisherOptions private const string DefaultName = "aspnetcore_healthcheck_status"; private const string DefaultHelp = "ASP.NET Core health check status (0 == Unhealthy, 0.5 == Degraded, 1 == Healthy)"; - public Gauge Gauge { get; set; } = Metrics.CreateGauge(DefaultName, DefaultHelp, labelNames: ["name"]); + public Gauge? Gauge { get; set; } + + public Gauge GetDefaultGauge() + { + return Metrics.CreateGauge(DefaultName, DefaultHelp, labelNames: new[] { "name" }); + } } From 4787691ec8cb1825f8328b53ee7addfddc216a8c Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Tue, 5 Dec 2023 23:50:28 +0200 Subject: [PATCH 228/230] History update --- History | 1 + 1 file changed, 1 insertion(+) diff --git a/History b/History index 02aa6be1..7a957465 100644 --- a/History +++ b/History @@ -3,6 +3,7 @@ - .WithLabels() & similar now accept ReadOnlySpan as alternative to string[]. This enables allocation-free metric instance creation if a metric instance with these labels is already known. - Incorporated various optimizations to reduce the required CPU time and allocated memory, including #410, #443 and other contributions. - Observation of large histograms is now 10-30% faster on machines that support AVX2 instructions. +- health checks exposed via ForwardToPrometheus() no longer create a default metric if a custom metric is provided #444 * 8.1.1 - Fix bug in .NET Meters API adapter for UpDownCounter, which was incorrectly transformed to Prometheus metrics. #452 and #453 * 8.1.0 From 6a88ea9b02c76e7c6fedd606eaa3e737c713ebe8 Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Wed, 3 Jan 2024 20:38:19 +0200 Subject: [PATCH 229/230] Fix occasional "Collection was modified" exception when serializing metrics. #464 --- History | 2 + Prometheus/CollectorRegistry.cs | 64 ++++++++++++++++++++++--------- Resources/SolutionAssemblyInfo.cs | 2 +- 3 files changed, 48 insertions(+), 20 deletions(-) diff --git a/History b/History index 7a957465..2c7d57cb 100644 --- a/History +++ b/History @@ -1,3 +1,5 @@ +* 8.2.1 +- Fix occasional "Collection was modified" exception when serializing metrics. #464 * 8.2.0 - .WithLabels() & similar now accept ReadOnlyMemory as alternative to string[]. Same behavior, just easier to use if you already have a ReadOnlyMemory. - .WithLabels() & similar now accept ReadOnlySpan as alternative to string[]. This enables allocation-free metric instance creation if a metric instance with these labels is already known. diff --git a/Prometheus/CollectorRegistry.cs b/Prometheus/CollectorRegistry.cs index 78d01e71..35f1971c 100644 --- a/Prometheus/CollectorRegistry.cs +++ b/Prometheus/CollectorRegistry.cs @@ -386,33 +386,59 @@ private void UpdateRegistryMetrics() if (_metricFamiliesPerType == null || _metricInstancesPerType == null || _metricTimeseriesPerType == null) return; // Debug metrics are not enabled. - foreach (MetricType type in Enum.GetValues(typeof(MetricType))) + // We copy references to the metric families to a temporary buffer to avoid having to hold locks to keep the collection consistent. + CollectorFamily[] familiesBuffer; + + _familiesLock.EnterReadLock(); + + var familiesCount = _families.Count; + familiesBuffer = ArrayPool.Shared.Rent(familiesCount); + + try { - long families = 0; - long instances = 0; - long timeseries = 0; + try + { + _families.Values.CopyTo(familiesBuffer, 0); + } + finally + { + _familiesLock.ExitReadLock(); + } - foreach (var family in _families.Values) + foreach (MetricType type in Enum.GetValues(typeof(MetricType))) { - bool hadMatchingType = false; + long families = 0; + long instances = 0; + long timeseries = 0; - family.ForEachCollector(collector => + for (var i = 0; i < familiesCount; i++) { - if (collector.Type != type) - return; + var family = familiesBuffer[i]; - hadMatchingType = true; - instances += collector.ChildCount; - timeseries += collector.TimeseriesCount; - }); + bool hadMatchingType = false; - if (hadMatchingType) - families++; - } + family.ForEachCollector(collector => + { + if (collector.Type != type) + return; + + hadMatchingType = true; + instances += collector.ChildCount; + timeseries += collector.TimeseriesCount; + }); - _metricFamiliesPerType[type].Set(families); - _metricInstancesPerType[type].Set(instances); - _metricTimeseriesPerType[type].Set(timeseries); + if (hadMatchingType) + families++; + } + + _metricFamiliesPerType[type].Set(families); + _metricInstancesPerType[type].Set(instances); + _metricTimeseriesPerType[type].Set(timeseries); + } + } + finally + { + ArrayPool.Shared.Return(familiesBuffer, clearArray: true); } } diff --git a/Resources/SolutionAssemblyInfo.cs b/Resources/SolutionAssemblyInfo.cs index 2ec83a8b..1f300ba8 100644 --- a/Resources/SolutionAssemblyInfo.cs +++ b/Resources/SolutionAssemblyInfo.cs @@ -2,7 +2,7 @@ using System.Runtime.CompilerServices; // This is the real version number, used in NuGet packages and for display purposes. -[assembly: AssemblyFileVersion("8.2.0")] +[assembly: AssemblyFileVersion("8.2.1")] // Only use major version here, with others kept at zero, for correct assembly binding logic. [assembly: AssemblyVersion("8.0.0")] From 60e9106a83ff1274fec0022c37366f04822b1d1b Mon Sep 17 00:00:00 2001 From: Sander Saares Date: Wed, 3 Jan 2024 20:57:43 +0200 Subject: [PATCH 230/230] De-confusify histogram1 in .NET Meters API sample It had implicit conflict with static label, which caused it to behave as unlabeled metric. Correct but potentially confusing because unlabeled metrics have special case publishing rules. Added a custom label to make it act like a labeled metric. --- Sample.Console.DotNetMeters/CustomDotNetMeters.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sample.Console.DotNetMeters/CustomDotNetMeters.cs b/Sample.Console.DotNetMeters/CustomDotNetMeters.cs index 9ec01552..296c9d36 100644 --- a/Sample.Console.DotNetMeters/CustomDotNetMeters.cs +++ b/Sample.Console.DotNetMeters/CustomDotNetMeters.cs @@ -65,7 +65,8 @@ int MeasureSandLevel() if (Random.Shared.Next(10) == 0) counter1.Add(1, new KeyValuePair("wing-type", "SlaxxWing 1.0"), new KeyValuePair("wing-version", "beta")); - histogram1.Record((byte)(Random.Shared.Next(256)), new KeyValuePair("is-faulted", true)); + // is-faulted here conflicts with the static label of the same name and gets overwritten by the static label. + histogram1.Record((byte)(Random.Shared.Next(256)), new KeyValuePair("is-faulted", true), new KeyValuePair("canbus_ver", "1.0")); // .NET 7 upDown1.Add(Random.Shared.Next(-1, 2));