using Docker.DotNet; using Docker.DotNet.Models; using MoonlightServers.Daemon.ServerSystem.Abstractions; namespace MoonlightServers.Daemon.ServerSystem.Implementations.Docker; public class DockerStatistics : IRuntimeStatistics, IInstallStatistics { public event Func? OnStatisticsReceived; private readonly string ContainerId; private readonly DockerClient DockerClient; private readonly ILogger Logger; private readonly List Cache = new(102); private readonly SemaphoreSlim CacheLock = new(1, 1); private readonly CancellationTokenSource Cts = new(); public DockerStatistics(string containerId, DockerClient dockerClient, ILogger logger) { ContainerId = containerId; DockerClient = dockerClient; Logger = logger; } public Task AttachAsync() { Task.Run(async () => { Logger.LogTrace("Streaming loop entered"); while (!Cts.IsCancellationRequested) { try { await DockerClient.Containers.GetContainerStatsAsync( ContainerId, new ContainerStatsParameters() { Stream = true }, new Progress(HandleStatsAsync), Cts.Token ); } catch (OperationCanceledException) { // Ignored } catch (Exception e) { Logger.LogError(e, "An unhandled error occured while processing container stats"); } } Logger.LogTrace("Streaming loop exited"); }); return Task.CompletedTask; } private async void HandleStatsAsync(ContainerStatsResponse statsResponse) { try { var convertedStats = ToServerStatistics(statsResponse); await CacheLock.WaitAsync(); try { if(Cache.Count > 100) Cache.RemoveRange(0, 30); Cache.Add(convertedStats); } finally { CacheLock.Release(); } if (OnStatisticsReceived != null) await OnStatisticsReceived.Invoke(convertedStats); } catch (Exception e) { Logger.LogError(e, "An unhandled error occured while handling container stats"); } } public async Task ClearCacheAsync() { await CacheLock.WaitAsync(); try { Cache.Clear(); } finally { CacheLock.Release(); } } public async Task GetCacheAsync() { await CacheLock.WaitAsync(); try { return Cache.ToArray(); } finally { CacheLock.Release(); } } // Helpers public static ServerStatistics ToServerStatistics(ContainerStatsResponse stats) { double cpuUsage = CalculateCpuUsage(stats); ulong usedMemory = stats.MemoryStats?.Usage ?? 0; ulong totalMemory = stats.MemoryStats?.Limit ?? 0; ulong outgoingNetwork = 0; ulong ingoingNetwork = 0; if (stats.Networks != null) { foreach (var network in stats.Networks.Values) { if (network == null) continue; outgoingNetwork += network.TxBytes; ingoingNetwork += network.RxBytes; } } ulong writeDisk = stats.StorageStats?.WriteSizeBytes ?? 0; ulong readDisk = stats.StorageStats?.ReadSizeBytes ?? 0; return new ServerStatistics( CpuUsage: cpuUsage, UsedMemory: usedMemory, TotalMemory: totalMemory, OutgoingNetwork: outgoingNetwork, IngoingNetwork: ingoingNetwork, WriteDisk: writeDisk, ReadDisk: readDisk ); } private static double CalculateCpuUsage(ContainerStatsResponse stats) { var cpuStats = stats.CPUStats; var preCpuStats = stats.PreCPUStats; if (cpuStats == null || preCpuStats == null) return 0; var cpuUsage = cpuStats.CPUUsage; var preCpuUsage = preCpuStats.CPUUsage; if (cpuUsage == null || preCpuUsage == null) return 0; ulong cpuDelta = cpuUsage.TotalUsage - preCpuUsage.TotalUsage; ulong systemDelta = cpuStats.SystemUsage - preCpuStats.SystemUsage; if (systemDelta == 0 || cpuDelta == 0) return 0; uint onlineCpus = cpuStats.OnlineCPUs; if (onlineCpus == 0 && cpuUsage.PercpuUsage != null) { onlineCpus = (uint)cpuUsage.PercpuUsage.Count; } if (onlineCpus == 0) onlineCpus = 1; return (double)cpuDelta / systemDelta * onlineCpus * 100.0; } public async ValueTask DisposeAsync() { await Cts.CancelAsync(); } }