summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--LibSystemdCli.Models/SystemdServiceData.cs33
-rw-r--r--SystemdCtl.Client/Pages/Graphs/TimelineGraph.razor70
-rw-r--r--SystemdCtl.Client/Pages/ServiceManage.razor50
-rw-r--r--SystemdCtl/Controllers/UnitController.cs47
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")]