diff options
-rw-r--r-- | LibSystemdCli.Models/SystemdServiceData.cs | 33 | ||||
-rw-r--r-- | SystemdCtl.Client/Pages/Graphs/TimelineGraph.razor | 70 | ||||
-rw-r--r-- | SystemdCtl.Client/Pages/ServiceManage.razor | 50 | ||||
-rw-r--r-- | SystemdCtl/Controllers/UnitController.cs | 47 |
4 files changed, 186 insertions, 14 deletions
diff --git a/LibSystemdCli.Models/SystemdServiceData.cs b/LibSystemdCli.Models/SystemdServiceData.cs index 1efcfd7..8d35c26 100644 --- a/LibSystemdCli.Models/SystemdServiceData.cs +++ b/LibSystemdCli.Models/SystemdServiceData.cs @@ -9,14 +9,30 @@ public class SystemdServiceData { [JsonPropertyName("multiData")] public Dictionary<string, List<string>> MultiData { get; set; } = new(); - public bool IsRunning => Status is "active" or "reloading"; + public bool IsRunning => Status is "active" or "reloading" or "deactating" or "activating"; public string Status => GetSingleData("ActiveState") ?? "unknown"; + + public long MemoryCurrent { + get { + if (long.TryParse(GetSingleData("MemoryCurrent") ?? "0", out long mem)) return mem; + return 0; + } + } + + public long MemoryPeak { + get { + if (long.TryParse(GetSingleData("MemoryPeak") ?? "0", out long mem)) return mem; + return 0; + } + } + + public void AddData(string key, string value) { if (MultiData.ContainsKey(key)) { MultiData[key].Add(value); } else if (SingleData.ContainsKey(key)) { - MultiData[key] = new List<string> { SingleData[key], value }; + MultiData[key] = [SingleData[key], value]; SingleData.Remove(key); } else { @@ -43,4 +59,17 @@ public class SystemdServiceData { return null; } + + public void SetOldData(SystemdServiceData oldData) { + var now = DateTime.Now; + + var cpuDiff = CpuUsageNs - oldData.CpuUsageNs; + var timeDiff = now - oldData.Timestamp; + CpuUsagePercent = cpuDiff / timeDiff.TotalMilliseconds / 1_000_000d; + } + + public DateTime Timestamp { get; set; } = DateTime.Now; + public long CpuUsageNs => long.Parse(GetSingleData("CPUUsageNSec") ?? "0"); + + public double CpuUsagePercent { get; set; } } \ No newline at end of file diff --git a/SystemdCtl.Client/Pages/Graphs/TimelineGraph.razor b/SystemdCtl.Client/Pages/Graphs/TimelineGraph.razor new file mode 100644 index 0000000..9c53986 --- /dev/null +++ b/SystemdCtl.Client/Pages/Graphs/TimelineGraph.razor @@ -0,0 +1,70 @@ +@using ArcaneLibs +<svg width="@Width" height="@Height"> + @if (Data.Count > 0) { + <polyline points="@string.Join(" ", _points.Select(x => $"{x.Key},{x.Value}"))" style="fill:none;stroke:black;stroke-width:3"/> + @* Y min/max labels *@ + <text> + <text x="0" y="@Height" fill="black">@(ValueFormatter.Invoke(_minValue))</text> + <text x="0" y="15" fill="black">@(ValueFormatter.Invoke(_maxValue))</text> + </text> + @* outline *@ + <rect x="0" y="0" width="@Width" height="@Height" style="fill:none;stroke:black;stroke-width:1"/> + } +</svg> + +@code { + + [Parameter] + public Dictionary<DateTime, double> Data { get; set; } + + [Parameter] + public int Width { get; set; } = 100; + + [Parameter] + public int Height { get; set; } = 100; + + [Parameter] + public double? MinValue { get; set; } + + [Parameter] + public double? MaxValue { get; set; } + + //value formatter + [Parameter] + public Func<double, string> ValueFormatter { get; set; } = x => x.ToString("X2"); + + private double _minValue => MinValue ?? (Data.Count > 0 ? Data.Values.Min() : 0); + private double _maxValue => MaxValue ?? (Data.Count > 0 ? Data.Values.Max() : 0); + + private Dictionary<double, double> _points = []; + + protected override async Task OnParametersSetAsync() { + await RebuildGraph(); + await base.OnParametersSetAsync(); + } + + private async Task RebuildGraph() { + if (Data.Count == 0) return; + _points.Clear(); + var startTime = Data.Keys.Min(x => x).Ticks; + var endTime = Data.Keys.Max(x => x).Ticks; + var minValue = _minValue; + var maxValue = _maxValue; + Console.WriteLine($"Start: {startTime}, End: {endTime}, Min: {minValue}, Max: {maxValue}"); + foreach (var item in Data) { + _points.Add(Map(item.Key.Ticks, startTime, endTime, 0, Width), + Map(item.Value, minValue, maxValue, Height, 0)); + } + } + + public static double Map( + double value, + double originalStart, + double originalEnd, + double newStart, + double newEnd) { + double num = (newEnd - newStart) / (originalEnd - originalStart); + return newStart + (value - originalStart) * num; + } + +} \ No newline at end of file diff --git a/SystemdCtl.Client/Pages/ServiceManage.razor b/SystemdCtl.Client/Pages/ServiceManage.razor index 851f1b6..39be89e 100644 --- a/SystemdCtl.Client/Pages/ServiceManage.razor +++ b/SystemdCtl.Client/Pages/ServiceManage.razor @@ -6,6 +6,8 @@ @using SystemdCtl.Client.Abstractions @using System.Diagnostics.Metrics @using System.Text.Json +@using ArcaneLibs +@using SystemdCtl.Client.Pages.Graphs @* @attribute [StreamRendering] *@ @rendermode InteractiveWebAssembly @inject NavigationManager NavigationManager @@ -33,6 +35,19 @@ </div> </div> +<div class="row"> + <div class="col-12"> + <h3>Statistics</h3> + <div class="card"> + <div class="card-body"> + <p>CPU usage: @ServiceInfo?.CpuUsagePercent.ToString("P2")</p> + <TimelineGraph Data="@CpuUsageHistory" Height="50" Width="500" MinValue="0" ValueFormatter="@(value => value.ToString("P2"))"/> + <p>Memory usage: @Util.SI_BytesToString(ServiceInfo?.MemoryCurrent ?? 0) (peak: @Util.SI_BytesToString(ServiceInfo?.MemoryPeak ?? 0))</p> + <TimelineGraph Data="@MemoryUsageHistory" Height="50" Width="500" ValueFormatter="@(value => Util.SI_BytesToString((long)value))"/> + </div> + </div> + </div> +</div> @* //simple log view *@ <div class="row"> @@ -72,17 +87,22 @@ private async Task Run() { if (!IsClient) return; - int history = 100; - LogLines.Clear(); GetServiceDataTask(); + GetLogs(); + } + + private JsonSerializerOptions jso = new() { + DefaultBufferSize = 1, + AllowTrailingCommas = true + }; + + private async Task GetLogs() { + int history = 100; var Http = new StreamingHttpClient() { BaseAddress = new Uri(NavigationManager.BaseUri) }; - var jso = new JsonSerializerOptions() { - DefaultBufferSize = 1, - AllowTrailingCommas = true - }; + + LogLines.Clear(); var _items = Http.GetAsyncEnumerableFromJsonAsync<SystemdJournalLogItem>($"/api/unit/{ServiceName}/logs?contextLines={history}", jso); await foreach (var item in _items) { - // LogLines.RemoveAll(x=>x.SystemdInvocationId != item.SystemdInvocationId); LogLines.Add(item); if (LogLines.Count > history) { LogLines.RemoveAt(0); @@ -95,10 +115,17 @@ private async Task GetServiceDataTask() { if (!IsClient) return; var Http = new StreamingHttpClient() { BaseAddress = new Uri(NavigationManager.BaseUri) }; - while (true) { - ServiceInfo = await Http.GetFromJsonAsync<SystemdServiceData>($"/api/unit/{ServiceName}/data"); + + var stream = Http.GetAsyncEnumerableFromJsonAsync<SystemdServiceData>($"/api/unit/{ServiceName}/dataStream", jso); + await foreach (var item in stream) { + ServiceInfo = item; + CpuUsageHistory.Add(ServiceInfo.Timestamp, ServiceInfo.CpuUsagePercent); + MemoryUsageHistory.Add(ServiceInfo.Timestamp, ServiceInfo.MemoryCurrent); + CpuUsageHistory = CpuUsageHistory.Where((x, y) => x.Key > DateTime.Now.AddMinutes(-1)) + .OrderBy(x => x.Key).ToDictionary(); + MemoryUsageHistory = MemoryUsageHistory.Where((x, y) => x.Key > DateTime.Now.AddMinutes(-1)).OrderBy(x => x.Key).ToDictionary(); + StateHasChanged(); - await Task.Delay(TimeSpan.FromSeconds(5)); } } @@ -130,4 +157,7 @@ await Http.GetAsync($"/api/unit/{ServiceName}/kill", null); } + public Dictionary<DateTime, double> CpuUsageHistory { get; set; } = []; + public Dictionary<DateTime, double> MemoryUsageHistory { get; set; } = []; + } \ No newline at end of file diff --git a/SystemdCtl/Controllers/UnitController.cs b/SystemdCtl/Controllers/UnitController.cs index a128584..da1c534 100644 --- a/SystemdCtl/Controllers/UnitController.cs +++ b/SystemdCtl/Controllers/UnitController.cs @@ -1,3 +1,7 @@ +using System.Runtime.InteropServices; +using System.Text; +using ArcaneLibs.Extensions; +using ArcaneLibs.Extensions.Streams; using LibSystemdCli; using LibSystemdCli.Models; using Microsoft.AspNetCore.Mvc; @@ -17,15 +21,54 @@ public class UnitController : ControllerBase } } + // [HttpGet("unit/{serviceName}/logs")] + // public async IAsyncEnumerable<SystemdJournalLogItem> GetUnitLogs(string serviceName, [FromQuery] int contextLines = 100) + // { + // await foreach (var log in SystemdExecutor.GetUnitLogs(serviceName, contextLines: contextLines)) + // { + // Console.WriteLine(log.Message); + // yield return log; + // await Response.Body.FlushAsync(); + // } + // } [HttpGet("unit/{serviceName}/logs")] - public async IAsyncEnumerable<SystemdJournalLogItem> GetUnitLogs(string serviceName, [FromQuery] int contextLines = 100) + public async Task GetUnitLogs(string serviceName, [FromQuery] int contextLines = 100) { + Response.ContentType = "application/json"; + await Response.StartAsync(); + await Response.Body.WriteAsync("[\n"u8.ToArray()); await foreach (var log in SystemdExecutor.GetUnitLogs(serviceName, contextLines: contextLines)) { Console.WriteLine(log.Message); - yield return log; + var bytes = Encoding.UTF8.GetBytes($" {log.ToJson(indent: false)},\n"); + await Response.Body.WriteAsync(bytes); await Response.Body.FlushAsync(); } + await Response.Body.WriteAsync("]\n"u8.ToArray()); + await Response.Body.FlushAsync(); + await Response.CompleteAsync(); + } + + [HttpGet("unit/{serviceName}/dataStream")] + public async Task GetUnitDataStream(string serviceName, [FromQuery] int contextLines = 100) + { + Response.ContentType = "application/json"; + await Response.StartAsync(); + await Response.Body.WriteAsync("[\n"u8.ToArray()); + var oldData = await SystemdExecutor.GetUnitData(serviceName); + while(true) + { + var data = await SystemdExecutor.GetUnitData(serviceName); + data.SetOldData(oldData); + var bytes = Encoding.UTF8.GetBytes($" {data.ToJson(indent: false)},\n"); + await Response.Body.WriteAsync(bytes); + await Response.Body.FlushAsync(); + oldData = data; + await Task.Delay(1000); + } + await Response.Body.WriteAsync("]\n"u8.ToArray()); + await Response.Body.FlushAsync(); + await Response.CompleteAsync(); } [HttpGet("unit/{serviceName}/data")] |