Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -458,5 +458,26 @@ public void ConfigureFailed(System.Exception ex)

[Event(45, Message = "The {0} method received an AzureMonitorExporterOptions with EnableLiveMetrics set to true, which isn't supported. Note that LiveMetrics is only available via the UseAzureMonitorExporter API.", Level = EventLevel.Warning)]
public void LiveMetricsNotSupported(string methodName) => WriteEvent(45, methodName);

[Event(46, Message = "Failure to calculate CPU Counter. Unexpected negative timespan: PreviousCollectedTime: {0}. RecentCollectedTime: {1}. Not user actionable.", Level = EventLevel.Error)]
public void ProcessCountersUnexpectedNegativeTimeSpan(long previousCollectedTime, long recentCollectedTime) => WriteEvent(46, previousCollectedTime, recentCollectedTime);

[Event(47, Message = "Failure to calculate CPU Counter. Unexpected negative value: PreviousCollectedValue: {0}. RecentCollectedValue: {1}. Not user actionable.", Level = EventLevel.Error)]
public void ProcessCountersUnexpectedNegativeValue(long previousCollectedValue, long recentCollectedValue) => WriteEvent(47, previousCollectedValue, recentCollectedValue);

[Event(48, Message = "Calculated Cpu Counter: Period: {0}. DiffValue: {1}. CalculatedValue: {2}. ProcessorCount: {3}. NormalizedValue: {4}", Level = EventLevel.Verbose)]
public void ProcessCountersCpuCounter(long period, long diffValue, double calculatedValue, int processorCount, double normalizedValue) => WriteEvent(48, period, diffValue, calculatedValue, processorCount, normalizedValue);

[NonEvent]
public void FailedToCollectProcessPrivateBytes(System.Exception ex)
{
if (IsEnabled(EventLevel.Error))
{
FailedToCollectProcessPrivateBytes(ex.FlattenException().ToInvariantString());
}
}

[Event(49, Message = "Failed to collect Process Private Bytes due to an exception. {0}", Level = EventLevel.Warning)]
public void FailedToCollectProcessPrivateBytes(string exceptionMessage) => WriteEvent(49, exceptionMessage);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

namespace Azure.Monitor.OpenTelemetry.Exporter.Internals
{
internal static class PerfCounterConstants
{
internal const string PerfCounterMeterName = "PerfCounterMeter";
internal const string RequestRateInstrumentationName = "RequestCounterRate";
internal const string ProcessCpuInstrumentationName = "ProcessCpu";
internal const string ProcessCpuNormalizedInstrumentationName = "ProcessCpuNormalized";
internal const string ProcessPrivateBytesInstrumentationName = "ProcessPrivateBytes";
internal const string ExceptionRateName = "AzureMonitorExceptionRate";

// 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
Expand Up @@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.Metrics;
using Azure.Monitor.OpenTelemetry.Exporter.Internals.Diagnostics;
using Azure.Monitor.OpenTelemetry.Exporter.Models;
using OpenTelemetry;
using OpenTelemetry.Metrics;
Expand All @@ -15,15 +16,39 @@ internal sealed class StandardMetricsExtractionProcessor : BaseProcessor<Activit
{
private bool _disposed;
private AzureMonitorResource? _resource;

internal readonly MeterProvider? _meterProvider;
private readonly Meter _meter;
private readonly Meter _standardMetricMeter;
private readonly Meter _perfCounterMeter;

private readonly Histogram<double> _requestDuration;
private readonly Histogram<double> _dependencyDuration;

private readonly ObservableGauge<long> _processPrivateBytesGauge;
private readonly ObservableGauge<double> _processCpuGauge;
private readonly ObservableGauge<double> _processCpuNormalizedGauge;
private readonly Counter<long> _requestRate;

private readonly Process _process = Process.GetCurrentProcess();
private readonly int _processorCount = Environment.ProcessorCount;
private DateTimeOffset _cachedCollectedTime = DateTimeOffset.MinValue;
private long _cachedCollectedValue = 0;

private DateTimeOffset _lastCpuCalculationTime = DateTimeOffset.MinValue;
private double _cachedRawCpuValue = 0;
private double _cachedNormalizedCpuValue = 0;
private bool _cpuCalculationValid = false;

internal static readonly IReadOnlyDictionary<string, string> s_standardMetricNameMapping = new Dictionary<string, string>()
{
[StandardMetricConstants.RequestDurationInstrumentName] = StandardMetricConstants.RequestDurationMetricIdValue,
[StandardMetricConstants.DependencyDurationInstrumentName] = StandardMetricConstants.DependencyDurationMetricIdValue,

[PerfCounterConstants.RequestRateInstrumentationName] = PerfCounterConstants.RequestRateMetricIdValue,
[PerfCounterConstants.ProcessPrivateBytesInstrumentationName] = PerfCounterConstants.ProcessPrivateBytesMetricIdValue,
[PerfCounterConstants.ExceptionRateName] = PerfCounterConstants.ExceptionRateMetricIdValue,
[PerfCounterConstants.ProcessCpuInstrumentationName] = PerfCounterConstants.ProcessCpuMetricIdValue,
[PerfCounterConstants.ProcessCpuNormalizedInstrumentationName] = PerfCounterConstants.ProcessCpuNormalizedMetricIdValue,
};

internal AzureMonitorResource? StandardMetricResource => _resource ??= ParentProvider?.GetResource().CreateAzureMonitorResource();
Expand All @@ -32,12 +57,28 @@ internal StandardMetricsExtractionProcessor(AzureMonitorMetricExporter metricExp
{
_meterProvider = Sdk.CreateMeterProviderBuilder()
.AddMeter(StandardMetricConstants.StandardMetricMeterName)
.AddMeter(PerfCounterConstants.PerfCounterMeterName)
.AddMeter("System.Runtime")
.AddView("dotnet.exceptions", new MetricStreamConfiguration
{
Name = PerfCounterConstants.ExceptionRateName,
TagKeys = []
})
.AddView("dotnet.*", MetricStreamConfiguration.Drop)
.AddReader(new PeriodicExportingMetricReader(metricExporter)
{ TemporalityPreference = MetricReaderTemporalityPreference.Delta })
.Build();
_meter = new Meter(StandardMetricConstants.StandardMetricMeterName);
_requestDuration = _meter.CreateHistogram<double>(StandardMetricConstants.RequestDurationInstrumentName);
_dependencyDuration = _meter.CreateHistogram<double>(StandardMetricConstants.DependencyDurationInstrumentName);

_standardMetricMeter = new Meter(StandardMetricConstants.StandardMetricMeterName);
_requestDuration = _standardMetricMeter.CreateHistogram<double>(StandardMetricConstants.RequestDurationInstrumentName);
_dependencyDuration = _standardMetricMeter.CreateHistogram<double>(StandardMetricConstants.DependencyDurationInstrumentName);

_perfCounterMeter = new Meter(PerfCounterConstants.PerfCounterMeterName);
_requestRate = _perfCounterMeter.CreateCounter<long>(PerfCounterConstants.RequestRateInstrumentationName);
_processPrivateBytesGauge = _standardMetricMeter.CreateObservableGauge<long>(PerfCounterConstants.ProcessPrivateBytesInstrumentationName, () => GetProcessPrivateBytes());
_processCpuGauge = _perfCounterMeter.CreateObservableGauge<double>(PerfCounterConstants.ProcessCpuInstrumentationName, () => GetProcessCPU());
_processCpuNormalizedGauge = _perfCounterMeter.CreateObservableGauge<double>(PerfCounterConstants.ProcessCpuNormalizedInstrumentationName, () => GetProcessCPUNormalized());
InitializeCpuBaseline();
}

public override void OnEnd(Activity activity)
Expand Down Expand Up @@ -82,6 +123,7 @@ private void ReportRequestDurationMetric(Activity activity)

// Report metric
_requestDuration.Record(activity.Duration.TotalMilliseconds, tags);
_requestRate.Add(1);
}

private void ReportDependencyDurationMetric(Activity activity)
Expand Down Expand Up @@ -133,6 +175,144 @@ private void ReportDependencyDurationMetric(Activity activity)
activityTagsProcessor.Return();
}

private long GetProcessPrivateBytes()
{
try
{
return _process.PrivateMemorySize64;
}
catch (Exception ex)
{
// Log to event source.
AzureMonitorExporterEventSource.Log.FailedToCollectProcessPrivateBytes(ex);
return 0;
}
}

private double GetProcessCPU()
{
EnsureCpuCalculation();
return _cachedRawCpuValue;
}

private double GetProcessCPUNormalized()
{
EnsureCpuCalculation();
return _cachedNormalizedCpuValue;
}

private void EnsureCpuCalculation()
{
var now = DateTimeOffset.UtcNow;

// Check if we need to recalculate (59-second cache to align with ~60-second collection intervals)
if (!_cpuCalculationValid || (now - _lastCpuCalculationTime).TotalSeconds > 59)
{
try
{
if (TryCalculateCPUCounter(out double rawValue, out double normalizedValue))
{
_cachedRawCpuValue = rawValue;
_cachedNormalizedCpuValue = normalizedValue;
_cpuCalculationValid = true;
}
else
{
_cachedRawCpuValue = 0;
_cachedNormalizedCpuValue = 0;
_cpuCalculationValid = false;
}
}
catch
{
_cachedRawCpuValue = 0;
_cachedNormalizedCpuValue = 0;
_cpuCalculationValid = false;
}

_lastCpuCalculationTime = now;
}
}

private bool TryCalculateCPUCounter(out double rawValue, out double normalizedValue)
{
var previousCollectedValue = _cachedCollectedValue;
var previousCollectedTime = _cachedCollectedTime;

// Refresh process data to get current CPU time
_process.Refresh();

var recentCollectedValue = _cachedCollectedValue = _process.TotalProcessorTime.Ticks;
var recentCollectedTime = _cachedCollectedTime = DateTimeOffset.UtcNow;

if (previousCollectedTime == DateTimeOffset.MinValue)
{
Debug.WriteLine($"{nameof(TryCalculateCPUCounter)} DateTimeOffset.MinValue");
rawValue = default;
normalizedValue = default;
return false;
}

var period = recentCollectedTime.Ticks - previousCollectedTime.Ticks;
if (period <= 0)
{
AzureMonitorExporterEventSource.Log.ProcessCountersUnexpectedNegativeTimeSpan(
previousCollectedTime: previousCollectedTime.Ticks,
recentCollectedTime: recentCollectedTime.Ticks);

Debug.WriteLine($"{nameof(TryCalculateCPUCounter)} period less than or equal to zero");
rawValue = default;
normalizedValue = default;
return false;
}

var diff = recentCollectedValue - previousCollectedValue;
if (diff < 0)
{
AzureMonitorExporterEventSource.Log.ProcessCountersUnexpectedNegativeValue(
previousCollectedValue: previousCollectedValue,
recentCollectedValue: recentCollectedValue);

Debug.WriteLine($"{nameof(TryCalculateCPUCounter)} diff less than zero");
rawValue = default;
normalizedValue = default;
return false;
}

// Calculate raw CPU percentage (can exceed 100% on multi-core systems)
rawValue = (double)diff * 100.0 / period;

// Calculate normalized CPU percentage (0-100%)
normalizedValue = rawValue / _processorCount;

// Clamp to reasonable bounds
rawValue = Math.Max(0, Math.Min(100 * _processorCount, rawValue));
normalizedValue = Math.Max(0, Math.Min(100, normalizedValue));

AzureMonitorExporterEventSource.Log.ProcessCountersCpuCounter(
period: period,
diffValue: diff,
calculatedValue: rawValue,
processorCount: _processorCount,
normalizedValue: normalizedValue);

return true;
}

private void InitializeCpuBaseline()
{
try
{
_process.Refresh();
_cachedCollectedValue = _process.TotalProcessorTime.Ticks;
_cachedCollectedTime = DateTimeOffset.UtcNow;
}
catch
{
// If initialization fails, keep defaults and let first measurement be zero
}
}

protected override void Dispose(bool disposing)
{
if (!_disposed)
Expand All @@ -142,7 +322,9 @@ protected override void Dispose(bool disposing)
try
{
_meterProvider?.Dispose();
_meter?.Dispose();
_standardMetricMeter?.Dispose();
_perfCounterMeter?.Dispose();
_process?.Dispose();
}
catch (Exception)
{
Expand Down
Loading
Loading