Skip to content
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

[Instrumentation.Process] Added 2 instruments: CPU Utilization and threads #687

Merged
merged 15 commits into from
Oct 14, 2022
106 changes: 85 additions & 21 deletions src/OpenTelemetry.Instrumentation.Process/ProcessMetrics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
// limitations under the License.
// </copyright>

using System;
using System.Collections.Generic;
using System.Diagnostics.Metrics;
using System.Reflection;
Expand All @@ -27,25 +28,36 @@ internal sealed class ProcessMetrics
internal readonly Meter MeterInstance = new(AssemblyName.Name, AssemblyName.Version.ToString());

private readonly Diagnostics.Process currentProcess = Diagnostics.Process.GetCurrentProcess();
private double? memoryUsage;
private double? virtualMemoryUsage;
private double? userProcessorTime;
private double? privilegedProcessorTime;
private double? memoryUsageBytes;
private double? virtualMemoryUsageBytes;
private double? userProcessorTimeSeconds;
private double? privilegedProcessorTimeSeconds;
private int? numberOfThreads;
private IEnumerable<Measurement<double>> cpuUtilization;

// vars for calculating CPU utilization
private DateTime lastCollectedTimeStampUtc;
private double lastCollectedUserProcessorTime;
private double lastCollectedPrivilegedProcessorTime;

public ProcessMetrics(ProcessInstrumentationOptions options)
{
this.lastCollectedTimeStampUtc = DateTime.UtcNow;
this.lastCollectedUserProcessorTime = this.currentProcess.UserProcessorTime.TotalSeconds;
this.lastCollectedUserProcessorTime = this.currentProcess.PrivilegedProcessorTime.TotalSeconds;
utpilla marked this conversation as resolved.
Show resolved Hide resolved

// TODO: change to ObservableUpDownCounter
this.MeterInstance.CreateObservableGauge(
"process.memory.usage",
() =>
{
if (!this.memoryUsage.HasValue)
if (!this.memoryUsageBytes.HasValue)
{
this.Snapshot();
}

var value = this.memoryUsage.Value;
this.memoryUsage = null;
var value = this.memoryUsageBytes.Value;
this.memoryUsageBytes = null;
return value;
},
unit: "By",
Expand All @@ -56,13 +68,13 @@ public ProcessMetrics(ProcessInstrumentationOptions options)
"process.memory.virtual",
() =>
{
if (!this.virtualMemoryUsage.HasValue)
if (!this.virtualMemoryUsageBytes.HasValue)
{
this.Snapshot();
}

var value = this.virtualMemoryUsage.Value;
this.virtualMemoryUsage = null;
var value = this.virtualMemoryUsageBytes.Value;
this.virtualMemoryUsageBytes = null;
return value;
},
unit: "By",
Expand All @@ -72,32 +84,84 @@ public ProcessMetrics(ProcessInstrumentationOptions options)
"process.cpu.time",
() =>
{
if (!this.userProcessorTime.HasValue || !this.privilegedProcessorTime.HasValue)
if (!this.userProcessorTimeSeconds.HasValue || !this.privilegedProcessorTimeSeconds.HasValue)
{
this.Snapshot();
}

var userProcessorTimeValue = this.userProcessorTime.Value;
var privilegedProcessorTimeValue = this.privilegedProcessorTime.Value;
this.userProcessorTime = null;
this.privilegedProcessorTime = null;
var userProcessorTimeSecondsValue = this.userProcessorTimeSeconds.Value;
var privilegedProcessorTimeSecondsValue = this.privilegedProcessorTimeSeconds.Value;
this.userProcessorTimeSeconds = null;
this.privilegedProcessorTimeSeconds = null;

return new[]
{
new Measurement<double>(userProcessorTimeValue, new KeyValuePair<string, object?>("state", "user")),
new Measurement<double>(privilegedProcessorTimeValue, new KeyValuePair<string, object?>("state", "system")),
new Measurement<double>(userProcessorTimeSecondsValue, new KeyValuePair<string, object?>("state", "user")),
new Measurement<double>(privilegedProcessorTimeSecondsValue, new KeyValuePair<string, object?>("state", "system")),
};
},
unit: "s",
description: "Total CPU seconds broken down by different states.");

this.MeterInstance.CreateObservableGauge(
"process.cpu.utilization",
() =>
{
if (this.cpuUtilization == null)
{
this.Snapshot();
}

var value = this.cpuUtilization;
this.cpuUtilization = null;
return value;
},
unit: "1",
description: "Difference in process.cpu.time since the last measurement, divided by the elapsed time and number of CPUs available to the process.");

// TODO: change to ObservableUpDownCounter
this.MeterInstance.CreateObservableGauge(
"process.threads",
() =>
{
if (!this.numberOfThreads.HasValue)
{
this.Snapshot();
}

var value = this.numberOfThreads.Value;
this.numberOfThreads = null;
return value;
},
unit: "{threads}",
description: "Process threads count.");
}

private void Snapshot()
{
this.currentProcess.Refresh();
this.memoryUsage = this.currentProcess.WorkingSet64;
this.virtualMemoryUsage = this.currentProcess.PrivateMemorySize64;
this.userProcessorTime = this.currentProcess.UserProcessorTime.TotalSeconds;
this.privilegedProcessorTime = this.currentProcess.PrivilegedProcessorTime.TotalSeconds;
this.memoryUsageBytes = this.currentProcess.WorkingSet64;
this.virtualMemoryUsageBytes = this.currentProcess.PrivateMemorySize64;
this.userProcessorTimeSeconds = this.currentProcess.UserProcessorTime.TotalSeconds;
this.privilegedProcessorTimeSeconds = this.currentProcess.PrivilegedProcessorTime.TotalSeconds;
this.cpuUtilization = this.GetCpuUtilization();
this.numberOfThreads = this.currentProcess.Threads.Count;
}

private IEnumerable<Measurement<double>> GetCpuUtilization()
{
var temp = (DateTime.UtcNow - this.lastCollectedTimeStampUtc).TotalSeconds * Environment.ProcessorCount;
var userProcessorUtilization = (this.userProcessorTimeSeconds - this.lastCollectedUserProcessorTime) / temp;
var privilegedProcessorUtilization = (this.privilegedProcessorTimeSeconds - this.lastCollectedPrivilegedProcessorTime) / temp;

this.lastCollectedTimeStampUtc = DateTime.UtcNow;
this.lastCollectedUserProcessorTime = this.currentProcess.UserProcessorTime.TotalSeconds;
this.lastCollectedPrivilegedProcessorTime = this.currentProcess.PrivilegedProcessorTime.TotalSeconds;

return new[]
{
new Measurement<double>(Math.Min(userProcessorUtilization.Value, 1D), new KeyValuePair<string, object?>("state", "user")),
new Measurement<double>(Math.Min(privilegedProcessorUtilization.Value, 1D), new KeyValuePair<string, object?>("state", "system")),
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
// </copyright>

using System.Collections.Generic;
using System.Diagnostics.Metrics;
using System.Linq;
using OpenTelemetry.Metrics;
using Xunit;
Expand All @@ -37,13 +36,17 @@ public void ProcessMetricsAreCaptured()

meterProvider.ForceFlush(MaxTimeToAllowForFlush);

Assert.True(exportedItems.Count == 3);
Assert.True(exportedItems.Count == 5);
var physicalMemoryMetric = exportedItems.FirstOrDefault(i => i.Name == "process.memory.usage");
Assert.NotNull(physicalMemoryMetric);
var virtualMemoryMetric = exportedItems.FirstOrDefault(i => i.Name == "process.memory.virtual");
Assert.NotNull(virtualMemoryMetric);
var cpuTimeMetric = exportedItems.FirstOrDefault(i => i.Name == "process.cpu.time");
Assert.NotNull(cpuTimeMetric);
var cpuUtilizationMetric = exportedItems.FirstOrDefault(i => i.Name == "process.cpu.utilization");
Assert.NotNull(cpuUtilizationMetric);
var threadMetric = exportedItems.FirstOrDefault(i => i.Name == "process.threads");
Assert.NotNull(cpuTimeMetric);
}

[Fact]
Expand Down Expand Up @@ -83,6 +86,43 @@ public void CpuTimeMetricsAreCaptured()
Assert.True(systemTimeCaptured);
}

[Fact]
public void CpuUtilizationMetricsAreCaptured()
{
var exportedItems = new List<Metric>();
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.AddProcessInstrumentation()
.AddInMemoryExporter(exportedItems)
.Build();

meterProvider.ForceFlush(MaxTimeToAllowForFlush);

var cpuUtilizationMetric = exportedItems.FirstOrDefault(i => i.Name == "process.cpu.utilization");
Assert.NotNull(cpuUtilizationMetric);

var userCpuUtilizationCaptured = false;
var systemCpuUtilizationCaptured = false;

var iter = cpuUtilizationMetric.GetMetricPoints().GetEnumerator();
while (iter.MoveNext() && (!userCpuUtilizationCaptured || !systemCpuUtilizationCaptured))
{
foreach (var tag in iter.Current.Tags)
{
if (tag.Key == "state" && tag.Value.ToString() == "user")
{
userCpuUtilizationCaptured = true;
}
else if (tag.Key == "state" && tag.Value.ToString() == "system")
{
systemCpuUtilizationCaptured = true;
}
}
}

Assert.True(userCpuUtilizationCaptured);
Assert.True(systemCpuUtilizationCaptured);
}

[Fact]
public void CheckValidGaugeValueWhen2MeterProviderInstancesHaveTheSameMeterName()
{
Expand Down