Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public static TracerProviderBuilder AddAzureMonitorTraceExporter(

builder.AddProcessor(new CompositeProcessor<Activity>(new BaseProcessor<Activity>[]
{
new StandardMetricsExtractionProcessor(new AzureMonitorMetricExporter(exporterOptions)),
new StandardMetricsExtractionProcessor(new AzureMonitorMetricExporter(exporterOptions), new PerfCounterItemCounts()),
new BatchActivityExportProcessor(new AzureMonitorTraceExporter(exporterOptions))
}));
});
Expand All @@ -116,6 +116,7 @@ public static MeterProviderBuilder AddAzureMonitorMetricExporter(
TokenCredential credential = null,
string name = null)
{
// TODO: determine if we want to add support for perf counters here
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
Expand Down Expand Up @@ -199,6 +200,7 @@ public static OpenTelemetryLoggerOptions AddAzureMonitorLogExporter(
options.Credential ??= credential;
}

// TODO: determine if we want to add support for perf counters here
return loggerOptions.AddProcessor(new BatchLogRecordExportProcessor(new AzureMonitorLogExporter(options)));
}

Expand Down Expand Up @@ -267,6 +269,7 @@ public static LoggerProviderBuilder AddAzureMonitorLogExporter(
sp.EnsureNoUseAzureMonitorExporterRegistrations();

// TODO: Do we need provide an option to alter BatchExportLogRecordProcessorOptions?
// TODO: Do we need to support perf counters with this?
return new BatchLogRecordExportProcessor(new AzureMonitorLogExporter(exporterOptions));
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ namespace Azure.Monitor.OpenTelemetry.Exporter
internal sealed class ExporterRegistrationHostedService : IHostedService
{
private readonly IServiceProvider _serviceProvider;

public ExporterRegistrationHostedService(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
Expand All @@ -49,6 +48,7 @@ public Task StopAsync(CancellationToken cancellationToken)
private static void Initialize(IServiceProvider serviceProvider)
{
Debug.Assert(serviceProvider != null, "serviceProvider was null");
PerfCounterItemCounts itemCounts = new PerfCounterItemCounts();

var tracerProvider = serviceProvider!.GetService<TracerProvider>();
if (tracerProvider != null)
Expand All @@ -68,7 +68,7 @@ private static void Initialize(IServiceProvider serviceProvider)
// TODO: Add Ai Sampler.
tracerProvider.AddProcessor(new CompositeProcessor<Activity>(new BaseProcessor<Activity>[]
{
new StandardMetricsExtractionProcessor(new AzureMonitorMetricExporter(exporterOptions)),
new StandardMetricsExtractionProcessor(new AzureMonitorMetricExporter(exporterOptions), itemCounts),
new BatchActivityExportProcessor(new AzureMonitorTraceExporter(exporterOptions))
}));
}
Expand All @@ -89,13 +89,18 @@ private static void Initialize(IServiceProvider serviceProvider)

loggerProvider.AddProcessor(new CompositeProcessor<LogRecord>(new BaseProcessor<LogRecord>[]
{
new PerfCounterLogProcessor(itemCounts),
new LiveMetricsLogProcessor(manager),
new BatchLogRecordExportProcessor(exporter)
}));
}
else
{
loggerProvider.AddProcessor(new BatchLogRecordExportProcessor(exporter));
loggerProvider.AddProcessor(new CompositeProcessor<LogRecord>(new BaseProcessor<LogRecord>[]
{
new PerfCounterLogProcessor(itemCounts),
new BatchLogRecordExportProcessor(exporter)
}));
}
}
}
Expand Down
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);
Copy link
Contributor

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.duration metrics by taking the histogram count and dividing by 60 (the aggregation window) to get requests per second.

}

/// <summary>
/// Atomically increments the total exception count.
/// </summary>
/// <returns>The new value after incrementing.</returns>
public long IncrementExceptionCount()
{
return Interlocked.Increment(ref _totalExceptionCount);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exception rate should be derived from process.runtime.dotnet.exceptions.count metrics from the System.Runtime meter by calculating count / 60 to get exceptions per second.

}

/// <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,
Copy link
Contributor

Choose a reason for hiding this comment

The 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;
}
}
}
Loading
Loading