diff --git a/src/OpenTelemetry.Instrumentation.Process/ProcessMetrics.cs b/src/OpenTelemetry.Instrumentation.Process/ProcessMetrics.cs index 30c9b2a7d8..cd4e743149 100644 --- a/src/OpenTelemetry.Instrumentation.Process/ProcessMetrics.cs +++ b/src/OpenTelemetry.Instrumentation.Process/ProcessMetrics.cs @@ -14,6 +14,7 @@ // limitations under the License. // +using System; using System.Collections.Generic; using System.Diagnostics.Metrics; using System.Reflection; @@ -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> cpuUtilization; + + // vars for calculating CPU utilization + private DateTime lastCollectionTimeUtc; + private double lastCollectedUserProcessorTime; + private double lastCollectedPrivilegedProcessorTime; public ProcessMetrics(ProcessInstrumentationOptions options) { + this.lastCollectionTimeUtc = DateTime.UtcNow; + this.lastCollectedUserProcessorTime = this.currentProcess.UserProcessorTime.TotalSeconds; + this.lastCollectedPrivilegedProcessorTime = this.currentProcess.PrivilegedProcessorTime.TotalSeconds; + // 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", @@ -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", @@ -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(userProcessorTimeValue, new KeyValuePair("state", "user")), - new Measurement(privilegedProcessorTimeValue, new KeyValuePair("state", "system")), + new Measurement(userProcessorTimeSecondsValue, new KeyValuePair("state", "user")), + new Measurement(privilegedProcessorTimeSecondsValue, new KeyValuePair("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> GetCpuUtilization() + { + var elapsedTimeForAllCpus = (DateTime.UtcNow - this.lastCollectionTimeUtc).TotalSeconds * Environment.ProcessorCount; + var userProcessorUtilization = (this.userProcessorTimeSeconds - this.lastCollectedUserProcessorTime) / elapsedTimeForAllCpus; + var privilegedProcessorUtilization = (this.privilegedProcessorTimeSeconds - this.lastCollectedPrivilegedProcessorTime) / elapsedTimeForAllCpus; + + this.lastCollectionTimeUtc = DateTime.UtcNow; + this.lastCollectedUserProcessorTime = this.currentProcess.UserProcessorTime.TotalSeconds; + this.lastCollectedPrivilegedProcessorTime = this.currentProcess.PrivilegedProcessorTime.TotalSeconds; + + return new[] + { + new Measurement(Math.Min(userProcessorUtilization.Value, 1D), new KeyValuePair("state", "user")), + new Measurement(Math.Min(privilegedProcessorUtilization.Value, 1D), new KeyValuePair("state", "system")), + }; } } diff --git a/test/OpenTelemetry.Instrumentation.Process.Tests/ProcessMetricsTests.cs b/test/OpenTelemetry.Instrumentation.Process.Tests/ProcessMetricsTests.cs index 82631a34ab..c269f58aa8 100644 --- a/test/OpenTelemetry.Instrumentation.Process.Tests/ProcessMetricsTests.cs +++ b/test/OpenTelemetry.Instrumentation.Process.Tests/ProcessMetricsTests.cs @@ -15,7 +15,6 @@ // using System.Collections.Generic; -using System.Diagnostics.Metrics; using System.Linq; using OpenTelemetry.Metrics; using Xunit; @@ -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] @@ -83,6 +86,43 @@ public void CpuTimeMetricsAreCaptured() Assert.True(systemTimeCaptured); } + [Fact] + public void CpuUtilizationMetricsAreCaptured() + { + var exportedItems = new List(); + 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() {