From 2933211933c1fc6cfb5c3fcb7e4cc0c5dead3ad8 Mon Sep 17 00:00:00 2001 From: dalibor Date: Sat, 18 Jun 2022 17:16:44 +0200 Subject: [PATCH 1/2] Add CPU metrics collector and instrumentation --- .../RuntimeMetrics.cs | 12 +++++++++ .../RuntimeMetricsOptions.cs | 16 ++++++++++++ .../RuntimeMetricsOptionsTests.cs | 26 +++++++++++++++++++ .../RuntimeMetricsTests.cs | 3 +++ 4 files changed, 57 insertions(+) diff --git a/src/OpenTelemetry.Instrumentation.Runtime/RuntimeMetrics.cs b/src/OpenTelemetry.Instrumentation.Runtime/RuntimeMetrics.cs index ebae341a47..0e3d5225c8 100644 --- a/src/OpenTelemetry.Instrumentation.Runtime/RuntimeMetrics.cs +++ b/src/OpenTelemetry.Instrumentation.Runtime/RuntimeMetrics.cs @@ -25,6 +25,7 @@ namespace OpenTelemetry.Instrumentation.Runtime { + /// /// .NET runtime instrumentation. /// @@ -38,6 +39,7 @@ internal class RuntimeMetrics : IDisposable private static readonly int NumberOfGenerations = 3; private static string metricPrefix = "process.runtime.dotnet."; private readonly Meter meter; + private CpuCollector CpuCollector { get; set;} /// /// Initializes a new instance of the class. @@ -47,6 +49,14 @@ public RuntimeMetrics(RuntimeMetricsOptions options) { this.meter = new Meter(InstrumentationName, InstrumentationVersion); + if(options.IsCpuEnabled){ + CpuCollector = new CpuCollector(options); + + this.meter.CreateObservableGauge($"{metricPrefix}cpu.total", () => CpuCollector.Record.TotalCpuUsed, "%", "Total CPU Percentage Used"); + this.meter.CreateObservableGauge($"{metricPrefix}cpu.privilage", () => CpuCollector.Record.PrivilegedCpuUsed, "%", "Privileged CPU Percentage Used"); + this.meter.CreateObservableGauge($"{metricPrefix}cpu.user", () => CpuCollector.Record.UserCpuUsed, "%", "User CPU Percentage Used"); + } + if (options.IsGcEnabled) { // TODO: Almost all the ObservableGauge should be ObservableUpDownCounter (except for CPU utilization and fragmentation.ratio. @@ -105,6 +115,8 @@ public RuntimeMetrics(RuntimeMetricsOptions options) public void Dispose() { this.meter?.Dispose(); + + CpuCollector?.Dispose(); } private static IEnumerable> GetGarbageCollectionCounts() diff --git a/src/OpenTelemetry.Instrumentation.Runtime/RuntimeMetricsOptions.cs b/src/OpenTelemetry.Instrumentation.Runtime/RuntimeMetricsOptions.cs index 03075c5e75..3533d6461d 100644 --- a/src/OpenTelemetry.Instrumentation.Runtime/RuntimeMetricsOptions.cs +++ b/src/OpenTelemetry.Instrumentation.Runtime/RuntimeMetricsOptions.cs @@ -40,6 +40,16 @@ public class RuntimeMetricsOptions public bool? ThreadingEnabled { get; set; } #endif + /// + /// Gets or sets a value indicating whether CPU metrics should be collected. + /// + public bool? CpuEnabled { get; set; } + + /// + /// Set CPU collect interval, default value is 1s + /// + public int? CpuCollectInterval { get; set; } + /// /// Gets or sets a value indicating whether process metrics should be collected. /// @@ -61,8 +71,14 @@ public class RuntimeMetricsOptions && this.ThreadingEnabled == null #endif && this.ProcessEnabled == null + && this.CpuEnabled == null && this.AssembliesEnabled == null; + /// + /// Gets a value indicating whether CPU collection metrics is enabled. + /// + internal bool IsCpuEnabled => this.CpuEnabled == true || this.IsAllEnabled; + /// /// Gets a value indicating whether garbage collection metrics is enabled. /// diff --git a/test/OpenTelemetry.Instrumentation.Runtime.Tests/RuntimeMetricsOptionsTests.cs b/test/OpenTelemetry.Instrumentation.Runtime.Tests/RuntimeMetricsOptionsTests.cs index 78dca4b563..0dda421ab5 100644 --- a/test/OpenTelemetry.Instrumentation.Runtime.Tests/RuntimeMetricsOptionsTests.cs +++ b/test/OpenTelemetry.Instrumentation.Runtime.Tests/RuntimeMetricsOptionsTests.cs @@ -32,6 +32,7 @@ public void Enable_All_If_Nothing_Was_Defined() #if NETCOREAPP3_1_OR_GREATER Assert.True(options.IsThreadingEnabled); #endif + Assert.True(options.IsCpuEnabled); Assert.True(options.IsProcessEnabled); Assert.True(options.IsAssembliesEnabled); Assert.True(options.IsAllEnabled); @@ -46,6 +47,26 @@ public void Enable_Gc_Only() #if NET6_0_OR_GREATER Assert.False(options.IsJitEnabled); #endif +#if NETCOREAPP3_1_OR_GREATER + Assert.False(options.IsThreadingEnabled); +#endif + Assert.False(options.IsCpuEnabled); + Assert.False(options.IsProcessEnabled); + Assert.False(options.IsAssembliesEnabled); + Assert.False(options.IsAllEnabled); + } + + + [Fact] + public void Enable_Cpu_Only() + { + var options = new RuntimeMetricsOptions { CpuEnabled = true }; + + Assert.False(options.IsGcEnabled); +#if NET6_0_OR_GREATER + Assert.False(options.IsJitEnabled); +#endif + Assert.True(options.IsCpuEnabled); #if NETCOREAPP3_1_OR_GREATER Assert.False(options.IsThreadingEnabled); #endif @@ -62,6 +83,7 @@ public void Enable_Jit_Only() Assert.False(options.IsGcEnabled); Assert.True(options.IsJitEnabled); + Assert.False(options.IsCpuEnabled); Assert.False(options.IsThreadingEnabled); Assert.False(options.IsProcessEnabled); Assert.False(options.IsAssembliesEnabled); @@ -81,6 +103,7 @@ public void Enable_Threading_Only() #endif Assert.True(options.IsThreadingEnabled); Assert.False(options.IsProcessEnabled); + Assert.False(options.IsCpuEnabled); Assert.False(options.IsAssembliesEnabled); Assert.False(options.IsAllEnabled); } @@ -99,6 +122,7 @@ public void Enable_Process_Only() Assert.False(options.IsThreadingEnabled); #endif Assert.True(options.IsProcessEnabled); + Assert.False(options.IsCpuEnabled); Assert.False(options.IsAssembliesEnabled); Assert.False(options.IsAllEnabled); } @@ -116,6 +140,7 @@ public void Enable_Assemblies_Only() Assert.False(options.IsThreadingEnabled); #endif Assert.False(options.IsProcessEnabled); + Assert.False(options.IsCpuEnabled); Assert.True(options.IsAssembliesEnabled); Assert.False(options.IsAllEnabled); } @@ -133,6 +158,7 @@ public void Enable_Multiple() Assert.False(options.IsThreadingEnabled); #endif Assert.True(options.IsProcessEnabled); + Assert.False(options.IsCpuEnabled); Assert.False(options.IsAssembliesEnabled); Assert.False(options.IsAllEnabled); } diff --git a/test/OpenTelemetry.Instrumentation.Runtime.Tests/RuntimeMetricsTests.cs b/test/OpenTelemetry.Instrumentation.Runtime.Tests/RuntimeMetricsTests.cs index 997a62f6d6..f1bbce01ca 100644 --- a/test/OpenTelemetry.Instrumentation.Runtime.Tests/RuntimeMetricsTests.cs +++ b/test/OpenTelemetry.Instrumentation.Runtime.Tests/RuntimeMetricsTests.cs @@ -43,6 +43,9 @@ public void RuntimeMetricsAreCaptured() options.JitEnabled = true; #endif + + options.CpuEnabled = true; + options.AssembliesEnabled = true; }) .AddInMemoryExporter(exportedItems) From 3d4bb7cc6e5c26d658184de95a34279b018ac524 Mon Sep 17 00:00:00 2001 From: dalibor Date: Sat, 18 Jun 2022 17:21:50 +0200 Subject: [PATCH 2/2] Add CPU metrics collector --- .../CpuCollector.cs | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 src/OpenTelemetry.Instrumentation.Runtime/CpuCollector.cs diff --git a/src/OpenTelemetry.Instrumentation.Runtime/CpuCollector.cs b/src/OpenTelemetry.Instrumentation.Runtime/CpuCollector.cs new file mode 100644 index 0000000000..8fb3b4d41f --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.Runtime/CpuCollector.cs @@ -0,0 +1,115 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Diagnostics; + +namespace OpenTelemetry.Instrumentation.Runtime +{ + + /// + /// .NET cpu metrics collector + /// + internal class CpuCollector : IDisposable + { + private long TRIGER_PERIOD {get; set;} = 1000; + + private const short DUE_TIME = 1000; + + private readonly Process _process = Process.GetCurrentProcess(); + private System.Threading.Timer _timer; + public static MetricsRecord Record { get; private set; } = new MetricsRecord(); + + private DateTime _lastTimeStamp; + private TimeSpan _lastTotalProcTime = TimeSpan.Zero; + private TimeSpan _lastUserProcTime = TimeSpan.Zero; + private TimeSpan _lastPrivilegedProcTime = TimeSpan.Zero; + + public struct MetricsRecord + { + public MetricsRecord(double totalCpuUsed, double privilegedCpuUsed, double userCpuUsed) + { + TotalCpuUsed = totalCpuUsed; + + PrivilegedCpuUsed = privilegedCpuUsed; + + UserCpuUsed = userCpuUsed; + } + + public double TotalCpuUsed { get; internal set;} + public double PrivilegedCpuUsed { get; internal set;} + public double UserCpuUsed { get; internal set;} + } + + /// + /// Initializes a new instance of the class. + /// + public CpuCollector(RuntimeMetricsOptions options) + { + _lastTimeStamp = _process.StartTime; + + if(options.CpuCollectInterval.HasValue && options.CpuCollectInterval.Value < 1000){ + TRIGER_PERIOD = 1000; + } + + _timer = new System.Threading.Timer(CollectData!, null, DUE_TIME, TRIGER_PERIOD); + } + + /// + /// Collects CPU metrics. + /// + private void CollectData(object state) + { + _process.Refresh(); + + var totalCpuTimeUsed = _process.TotalProcessorTime.TotalMilliseconds - _lastTotalProcTime.TotalMilliseconds; + var privilegedCpuTimeUsed = _process.PrivilegedProcessorTime.TotalMilliseconds - _lastPrivilegedProcTime.TotalMilliseconds; + var userCpuTimeUsed = _process.UserProcessorTime.TotalMilliseconds - _lastUserProcTime.TotalMilliseconds; + + _lastTotalProcTime = _process.TotalProcessorTime; + _lastPrivilegedProcTime = _process.PrivilegedProcessorTime; + _lastUserProcTime = _process.UserProcessorTime; + + try + { + var cpuTimeElapsed = (DateTime.UtcNow - _lastTimeStamp).TotalMilliseconds * Environment.ProcessorCount; + _lastTimeStamp = DateTime.UtcNow; + + var totla_cpu = totalCpuTimeUsed * 100 / cpuTimeElapsed; + var privilaged_cpu = privilegedCpuTimeUsed * 100 / cpuTimeElapsed; + var user_cpu = userCpuTimeUsed * 100 / cpuTimeElapsed; + + Record = new MetricsRecord() + { + TotalCpuUsed = totalCpuTimeUsed * 100 / cpuTimeElapsed, + PrivilegedCpuUsed = privilegedCpuTimeUsed * 100 / cpuTimeElapsed, + UserCpuUsed = userCpuTimeUsed * 100 / cpuTimeElapsed, + }; + + } + catch + { + + } + } + + /// + public void Dispose() + { + this._timer?.Dispose(); + } + } +}