-
Notifications
You must be signed in to change notification settings - Fork 5.1k
Draft: Perf Counter Implementation #52679
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| namespace Azure.Monitor.OpenTelemetry.Exporter.Internals | ||
| { | ||
| internal static class PerfCounterConstants | ||
| { | ||
| internal const string PerfCounterMeterName = "PerfCounterMeter"; | ||
| internal const string ExceptionRateInstrumentName = "ExceptionRate"; | ||
| internal const string RequestRateInstrumentName = "RequestRate"; | ||
| internal const string ProcessCpuInstrumentName = "ProcessCpu"; | ||
| internal const string ProcessCpuNormalizedInstrumentName = "ProcessCpuNormalized"; | ||
| internal const string ProcessPrivateBytesInstrumentName = "ProcessPrivateBytes"; | ||
|
|
||
| // breeze perf counter names | ||
| internal const string ExceptionRateMetricIdValue = "\.NET CLR Exceptions(??APP_CLR_PROC??)\# of Exceps Thrown / sec"; | ||
| internal const string RequestRateMetricIdValue = "\ASP.NET Applications(??APP_W3SVC_PROC??)\Requests/Sec"; | ||
| internal const string ProcessCpuMetricIdValue = "\Process(??APP_WIN32_PROC??)\% Processor Time"; | ||
| internal const string ProcessCpuNormalizedMetricIdValue = "\Process(??APP_WIN32_PROC??)\% Processor Time Normalized"; | ||
| internal const string ProcessPrivateBytesMetricIdValue = "\Process(??APP_WIN32_PROC??)\Private Bytes"; | ||
|
|
||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT License. | ||
|
|
||
| using System.Threading; | ||
|
|
||
| namespace Azure.Monitor.OpenTelemetry.Exporter.Internals | ||
| { | ||
| /// <summary> | ||
| /// Thread-safe counters for performance metrics. | ||
| /// </summary> | ||
| internal sealed class PerfCounterItemCounts | ||
| { | ||
| private long _totalRequestCount = 0; | ||
| private long _totalExceptionCount = 0; | ||
|
|
||
| /// <summary> | ||
| /// Atomically increments the total request count. | ||
| /// </summary> | ||
| /// <returns>The new value after incrementing.</returns> | ||
| public long IncrementRequestCount() | ||
| { | ||
| return Interlocked.Increment(ref _totalRequestCount); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Atomically increments the total exception count. | ||
| /// </summary> | ||
| /// <returns>The new value after incrementing.</returns> | ||
| public long IncrementExceptionCount() | ||
| { | ||
| return Interlocked.Increment(ref _totalExceptionCount); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Exception rate should be derived from |
||
| } | ||
|
|
||
| /// <summary> | ||
| /// Atomically reads the current total request count. | ||
| /// </summary> | ||
| /// <returns>The current request count.</returns> | ||
| public long ReadRequestCount() | ||
| { | ||
| return Interlocked.Read(ref _totalRequestCount); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Atomically reads the current total exception count. | ||
| /// </summary> | ||
| /// <returns>The current exception count.</returns> | ||
| public long ReadExceptionCount() | ||
| { | ||
| return Interlocked.Read(ref _totalExceptionCount); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT License. | ||
|
|
||
| using System.Diagnostics; | ||
| using OpenTelemetry; | ||
| using OpenTelemetry.Logs; | ||
|
|
||
| namespace Azure.Monitor.OpenTelemetry.Exporter.Internals | ||
| { | ||
| internal sealed class PerfCounterLogProcessor : BaseProcessor<LogRecord> | ||
| { | ||
| private bool _disposed; | ||
| private readonly PerfCounterItemCounts _itemCounts; | ||
|
|
||
| internal PerfCounterLogProcessor(PerfCounterItemCounts itemCounts) | ||
| { | ||
| _itemCounts = itemCounts; | ||
| } | ||
|
|
||
| public override void OnEnd(LogRecord data) | ||
| { | ||
| if (data.Exception is not null) | ||
| { | ||
| _itemCounts.IncrementExceptionCount(); | ||
| } | ||
| } | ||
|
|
||
| protected override void Dispose(bool disposing) | ||
| { | ||
| if (!_disposed) | ||
| { | ||
| if (disposing) | ||
| { | ||
| // cleanup if needed | ||
| } | ||
| _disposed = true; | ||
| } | ||
| base.Dispose(disposing); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,181 @@ | ||
| using OpenTelemetry.Metrics; | ||
| using System.Diagnostics.Metrics; | ||
|
|
||
| namespace Azure.Monitor.OpenTelemetry.Exporter.Internals | ||
| { | ||
| internal sealed class PerformanceCounter : IDisposable | ||
| { | ||
| private readonly MeterProvider _meterProvider; | ||
| private readonly Meter _perfCounterMeter; | ||
|
|
||
| // Gauge instruments | ||
| private readonly ObservableGauge<double> _exceptionRateGauge; | ||
| private readonly ObservableGauge<double> _requestRateGauge; | ||
| private readonly ObservableGauge<double> _processCpuGauge; | ||
| private readonly ObservableGauge<double> _processCpuNormalizedGauge; | ||
| private readonly ObservableGauge<double> _processPrivateBytesGauge; | ||
| private readonly Process _process = Process.GetCurrentProcess(); | ||
|
|
||
| // state for cpu counter calculations | ||
| private DateTimeOffset _cachedCollectedTime = DateTimeOffset.MinValue; | ||
| private long _cachedCollectedValueTicks = 0; | ||
| private double _currentCpuPercentage = 0; | ||
| private readonly int _processorCount = Environment.ProcessorCount; | ||
|
|
||
| // state for telemetry item related counts | ||
| private readonly PerfCounterItemCounts _itemCounts; | ||
| private long _lastExceptionRateCount = 0; | ||
| private DateTimeOffset _lastExceptionRateTime = DateTimeOffset.UtcNow; | ||
| private long _lastRequestRateCount = 0; | ||
| private DateTimeOffset _lastRequestRateTime = DateTimeOffset.UtcNow; | ||
|
|
||
| private bool _disposed; | ||
|
|
||
| public PerformanceCounter(MeterProvider meterProvider, PerfCounterItemCounts itemCounts) | ||
| { | ||
| _meterProvider = meterProvider; | ||
| _perfCounterMeter = new Meter(PerfCounterConstants.PerfCounterMeterName); | ||
| _itemCounts = itemCounts; | ||
|
|
||
| // Create observable gauges for perf counter | ||
| _exceptionRateGauge = _perfCounterMeter.CreateObservableGauge( | ||
| PerfCounterConstants.ExceptionRateInstrumentName, | ||
| () => GetExceptionRate(), | ||
| description: "Exception rate gauge (ex/sec)"); | ||
|
|
||
| _requestRateGauge = _perfCounterMeter.CreateObservableGauge( | ||
| PerfCounterConstants.RequestRateInstrumentName, | ||
| () => GetRequestRate(), | ||
| description: "Request rate gauge (req/sec)"); | ||
|
|
||
| _processCpuGauge = _perfCounterMeter.CreateObservableGauge( | ||
| PerfCounterConstants.ProcessCpuInstrumentName, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| () => GetProcessCpu(), | ||
| description: "Process CPU gauge (percent)"); | ||
|
|
||
| _processCpuNormalizedGauge = _perfCounterMeter.CreateObservableGauge( | ||
| PerfCounterConstants.ProcessCpuNormalizedInstrumentName, | ||
| () => GetProcessCpuNormalized(), | ||
| description: "Process CPU normalized gauge (percent)"); | ||
|
|
||
| _processPrivateBytesGauge = _perfCounterMeter.CreateObservableGauge( | ||
| PerfCounterConstants.ProcessPrivateBytesInstrumentName, | ||
| () => GetProcessPrivateBytes(), | ||
| description: "Process private bytes gauge"); | ||
| } | ||
|
|
||
| // Placeholder methods for gauge callbacks | ||
| private double GetExceptionRate() | ||
| { | ||
| var currentTime = DateTimeOffset.UtcNow; | ||
| var totalExceptions = _itemCounts.ReadExceptionCount(); | ||
| var intervalData = totalExceptions - _lastExceptionRateCount; | ||
| var elapsedSeconds = (currentTime - _lastExceptionRateTime).TotalSeconds; | ||
|
|
||
| double dataPerSec = 0.0; | ||
| if (elapsedSeconds > 0) | ||
| { | ||
| dataPerSec = intervalData / elapsedSeconds; | ||
| } | ||
|
|
||
| _lastExceptionRateCount = totalExceptions; | ||
| _lastExceptionRateTime = currentTime; | ||
|
|
||
| return dataPerSec; | ||
| } | ||
| private double GetRequestRate() | ||
| { | ||
| var currentTime = DateTimeOffset.UtcNow; | ||
| var totalRequests = _itemCounts.ReadRequestCount(); | ||
| var intervalRequests = totalRequests - _lastRequestRateCount; | ||
| var elapsedSec = (currentTime - _lastRequestRateTime).TotalSeconds; | ||
|
|
||
| double requestsPerSec = 0.0; | ||
| if (elapsedSec > 0) | ||
| { | ||
| requestsPerSec = intervalRequests / elapsedSec; | ||
| } | ||
|
|
||
| _lastRequestRateCount = totalRequests; | ||
| _lastRequestRateTime = currentTime; | ||
|
|
||
| return requestsPerSec; | ||
| } | ||
| private double GetProcessCpu() | ||
| { | ||
| UpdateCpuValuesIfNeeded(); | ||
| return _currentCpuPercentage; | ||
| } | ||
| private double GetProcessCpuNormalized() | ||
| { | ||
| UpdateCpuValuesIfNeeded(); | ||
| return _currentCpuPercentage / _processorCount; | ||
| } | ||
| private double GetProcessPrivateBytes() | ||
| { | ||
| return _process.PrivateMemorySize64; | ||
| } | ||
|
|
||
| private void UpdateCpuValuesIfNeeded() | ||
| { | ||
| var now = DateTimeOffset.UtcNow; | ||
| if ((now - _cachedCollectedTime).TotalSeconds > 59) // the default interval is set to a minute | ||
| { | ||
| _process.Refresh(); | ||
| if (TryCalculateProcessCpu(out var cpuValue)) | ||
| { | ||
| _currentCpuPercentage = cpuValue; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private bool TryCalculateProcessCpu(out double calculatedValue) | ||
| { | ||
| var previousCollectedValue = _cachedCollectedValueTicks; | ||
| var previousCollectedTime = _cachedCollectedTime; | ||
|
|
||
| var recentCollectedValue = _cachedCollectedValueTicks = _process.TotalProcessorTime.Ticks; | ||
| var recentCollectedTime = _cachedCollectedTime = DateTimeOffset.UtcNow; | ||
|
|
||
| double calculatedValue; | ||
|
|
||
| if (previousCollectedTime == DateTimeOffset.MinValue) | ||
| { | ||
| Debug.WriteLine($"{nameof(TryCalculateProcessCpu)} DateTimeOffset.MinValue"); | ||
| calculatedValue = default; | ||
| return false; | ||
| } | ||
|
|
||
| var period = recentCollectedTime.Ticks - previousCollectedTime.Ticks; | ||
| if (period < 0) | ||
| { | ||
| Debug.WriteLine($"{nameof(TryCalculateProcessCpu)} period less than zero"); | ||
| calculatedValue = default; | ||
| return false; | ||
| } | ||
|
|
||
| var diff = recentCollectedValue - previousCollectedValue; | ||
| if (diff < 0) | ||
| { | ||
| Debug.WriteLine($"{nameof(TryCalculateProcessCpu)} diff less than zero"); | ||
| calculatedValue = default; | ||
| return false; | ||
| } | ||
|
|
||
| period = period != 0 ? period : 1; | ||
| calculatedValue = diff * 100.0 / period; | ||
| return true; | ||
| } | ||
|
|
||
| public void Dispose() | ||
| { | ||
| if (_disposed) return; | ||
| try | ||
| { | ||
| _perfCounterMeter?.Dispose(); // Do NOT dispose _meterProvider (shared elsewhere) | ||
| } | ||
| catch { } | ||
| _disposed = true; | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Request rate should be derived from
http.server.request.durationmetrics by taking the histogram count and dividing by 60 (the aggregation window) to get requests per second.