diff --git a/src/KubernetesClient/KubernetesClient.csproj b/src/KubernetesClient/KubernetesClient.csproj index e32d6d70b..065887026 100644 --- a/src/KubernetesClient/KubernetesClient.csproj +++ b/src/KubernetesClient/KubernetesClient.csproj @@ -10,6 +10,7 @@ + diff --git a/src/KubernetesClient/PrometheusHandler.cs b/src/KubernetesClient/PrometheusHandler.cs new file mode 100644 index 000000000..7b7bd5461 --- /dev/null +++ b/src/KubernetesClient/PrometheusHandler.cs @@ -0,0 +1,79 @@ +using System.Diagnostics; +using System.Diagnostics.Metrics; +using System.Net.Http; + +namespace k8s +{ + /// + /// Implements legacy Prometheus metrics + /// + /// Provided for compatibility for existing usages of PrometheusHandler. It is recommended + /// to transition to using OpenTelemetry and the default HttpClient metrics. + /// + /// Note that the tags/labels are not appropriately named for some metrics. This + /// incorrect naming is retained to maintain compatibility and won't be fixed on this implementation. + /// Use OpenTelemetry and the standard HttpClient metrics instead. + public class PrometheusHandler : DelegatingHandler + { + private const string Prefix = "k8s_dotnet"; + private static readonly Meter Meter = new Meter("k8s.dotnet"); + + private static readonly Counter RequestsSent = Meter.CreateCounter( + $"{Prefix}_request_total", + description: "Number of requests sent by this client"); + + private static readonly Histogram RequestLatency = Meter.CreateHistogram( + $"{Prefix}_request_latency_seconds", unit: "milliseconds", + description: "Latency of requests sent by this client"); + + private static readonly Counter ResponseCodes = Meter.CreateCounter( + $"{Prefix}_response_code_total", + description: "Number of response codes received by the client"); + + private static readonly UpDownCounter ActiveRequests = + Meter.CreateUpDownCounter( + $"{Prefix}_active_requests", + description: "Number of requests currently in progress"); + + /// + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + + var digest = KubernetesRequestDigest.Parse(request); + var timer = Stopwatch.StartNew(); + // Note that this is a tag called "method" but the value is the Verb. + // This is incorrect, but existing behavior. + var methodWithVerbValue = new KeyValuePair("method", digest.Verb); + try + { + ActiveRequests.Add(1, methodWithVerbValue); + RequestsSent.Add(1, methodWithVerbValue); + + var resp = await base.SendAsync(request, cancellationToken).ConfigureAwait(false); + ResponseCodes.Add( + 1, + new KeyValuePair("method", request.Method.ToString()), + new KeyValuePair("code", (int)resp.StatusCode)); + return resp; + } + finally + { + timer.Stop(); + ActiveRequests.Add(-1, methodWithVerbValue); + var tags = new TagList + { + { "verb", digest.Verb }, + { "group", digest.ApiGroup }, + { "version", digest.ApiVersion }, + { "kind", digest.Kind }, + } + ; + RequestLatency.Record(timer.Elapsed.TotalMilliseconds, tags); + } + } + } +}