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
64 changes: 64 additions & 0 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 @@ -31,9 +32,18 @@ internal sealed class ProcessMetrics
private double? virtualMemoryUsage;
private double? userProcessorTime;
private double? privilegedProcessorTime;
private int? numberOfThreads;
private IEnumerable<Measurement<double>> cpuUtilization;

// vars for calculating CPU utilization
private DateTime collectionTimeStamp;
Yun-Ting marked this conversation as resolved.
Show resolved Hide resolved
private double collectionUserProcessorTime;
private double collectionPrivilegedProcessorTime;
Yun-Ting marked this conversation as resolved.
Show resolved Hide resolved

public ProcessMetrics(ProcessInstrumentationOptions options)
{
this.collectionTimeStamp = this.currentProcess.StartTime;
Yun-Ting marked this conversation as resolved.
Show resolved Hide resolved

// TODO: change to ObservableUpDownCounter
this.MeterInstance.CreateObservableGauge(
"process.memory.usage",
Expand Down Expand Up @@ -90,6 +100,39 @@ public ProcessMetrics(ProcessInstrumentationOptions options)
},
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()
Expand All @@ -99,5 +142,26 @@ private void Snapshot()
this.virtualMemoryUsage = this.currentProcess.PrivateMemorySize64;
this.userProcessorTime = this.currentProcess.UserProcessorTime.TotalSeconds;
this.privilegedProcessorTime = this.currentProcess.PrivilegedProcessorTime.TotalSeconds;
this.cpuUtilization = this.GetCpuUtilization();
this.numberOfThreads = this.currentProcess.Threads.Count;
}

private IEnumerable<Measurement<double>> GetCpuUtilization()
{
var userProcessorUtilization = (this.currentProcess.UserProcessorTime.TotalSeconds - this.collectionUserProcessorTime) /
Yun-Ting marked this conversation as resolved.
Show resolved Hide resolved
Yun-Ting marked this conversation as resolved.
Show resolved Hide resolved
((DateTime.UtcNow - this.collectionTimeStamp.ToUniversalTime()).TotalSeconds * Environment.ProcessorCount);

var privilegedProcessorUtilization = (this.currentProcess.PrivilegedProcessorTime.TotalSeconds - this.collectionPrivilegedProcessorTime) /
((DateTime.UtcNow - this.collectionTimeStamp.ToUniversalTime()).TotalSeconds * Environment.ProcessorCount);

this.collectionTimeStamp = DateTime.UtcNow;
this.collectionUserProcessorTime = this.currentProcess.UserProcessorTime.TotalSeconds;
this.collectionPrivilegedProcessorTime = this.currentProcess.PrivilegedProcessorTime.TotalSeconds;

return new[]
{
new Measurement<double>(userProcessorUtilization, new KeyValuePair<string, object?>("state", "user")),
new Measurement<double>(privilegedProcessorUtilization, new KeyValuePair<string, object?>("state", "system")),
Yun-Ting marked this conversation as resolved.
Show resolved Hide resolved
};
}
}
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,44 @@ public void CpuTimeMetricsAreCaptured()
Assert.True(systemTimeCaptured);
}

[Fact]
public void CpuUtilizationMetricsAreCapturedAndInRange()
{
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);
Assert.True(GetValue(cpuUtilizationMetric) >= 0 && GetValue(cpuUtilizationMetric) <= 1);

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