From 1f5010280be3f161fd1e4dcd1687f21f61eb195a Mon Sep 17 00:00:00 2001 From: Razvan Goga Date: Tue, 30 Aug 2022 23:19:58 +0200 Subject: [PATCH 1/2] added ServiceState based on replicas state surfaced ServiceState in dashboard made dashboard links more self-evident --- .../Dashboard/Pages/Index.razor | 21 ++++++-- src/Microsoft.Tye.Hosting/Model/Service.cs | 51 ++++++++++++++++++ test/UnitTests/Microsoft.Tye.UnitTests.csproj | 8 +++ test/UnitTests/ServiceUnitTests.cs | 52 +++++++++++++++++++ 4 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 test/UnitTests/ServiceUnitTests.cs diff --git a/src/Microsoft.Tye.Hosting/Dashboard/Pages/Index.razor b/src/Microsoft.Tye.Hosting/Dashboard/Pages/Index.razor index cf06ba18b..c63736df1 100644 --- a/src/Microsoft.Tye.Hosting/Dashboard/Pages/Index.razor +++ b/src/Microsoft.Tye.Hosting/Dashboard/Pages/Index.razor @@ -12,13 +12,14 @@ + - + @@ -26,7 +27,11 @@ { var logsPath = $"logs/{service.Description.Name}"; var servicePath = $"services/{service.Description.Name}"; + var serviceState = service.State; + - + } } @@ -102,6 +107,16 @@ private List _subscriptions = new List(); + string GetServiceStateClass(ServiceState serviceState) => serviceState switch + { + ServiceState.Starting => "badge-secondary", + ServiceState.Started => "badge-success", + ServiceState.Degraded => "badge-danger", + ServiceState.Failed => "badge-warning", + ServiceState.Stopped => "badge-light", + _ => "badge-dark" + }; + string GetUrl(ServiceBinding b) { return $"{(b.Protocol ?? "tcp")}://{b.Host ?? "localhost"}:{b.Port}"; diff --git a/src/Microsoft.Tye.Hosting/Model/Service.cs b/src/Microsoft.Tye.Hosting/Model/Service.cs index 265a1c6c1..d064e475d 100644 --- a/src/Microsoft.Tye.Hosting/Model/Service.cs +++ b/src/Microsoft.Tye.Hosting/Model/Service.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; using System.Reactive.Subjects; namespace Microsoft.Tye.Hosting.Model @@ -83,5 +84,55 @@ public ServiceType ServiceType public Subject Logs { get; } = new Subject(); public Subject ReplicaEvents { get; } = new Subject(); + + public ServiceState State + { + get + { + var replicaStates = Replicas.Values.Select(r => r.State); + int replicaCount = replicaStates.Count(); + + + if (replicaCount == 0) + return ServiceState.Unknown; + + if (replicaStates.Any(r => r == ReplicaState.Added)) + return ServiceState.Starting; + + if (replicaStates.All(r => r == ReplicaState.Started || r == ReplicaState.Ready || r == ReplicaState.Healthy)) + return ServiceState.Started; + + if (replicaCount == 1) + { + ReplicaState? replicaState = replicaStates.Single(); + + if (replicaState == ReplicaState.Removed) + return ServiceState.Failed; + + if (replicaState == ReplicaState.Stopped) + return ServiceState.Stopped; + } + else + { + if (replicaStates.All(r => r == ReplicaState.Stopped)) + return ServiceState.Stopped; + + if (replicaStates.Any(r => r == ReplicaState.Removed || r == ReplicaState.Stopped)) + return ServiceState.Degraded; + } + + return ServiceState.Unknown; + } + } + } + + public enum ServiceState + { + Unknown, + Starting, + Started, + Degraded, + Failed, + Stopped } } diff --git a/test/UnitTests/Microsoft.Tye.UnitTests.csproj b/test/UnitTests/Microsoft.Tye.UnitTests.csproj index d2addfef2..7fbea20f0 100644 --- a/test/UnitTests/Microsoft.Tye.UnitTests.csproj +++ b/test/UnitTests/Microsoft.Tye.UnitTests.csproj @@ -9,6 +9,14 @@ XUnit + + 10.0 + + + + 10.0 + + diff --git a/test/UnitTests/ServiceUnitTests.cs b/test/UnitTests/ServiceUnitTests.cs new file mode 100644 index 000000000..ed64dbba1 --- /dev/null +++ b/test/UnitTests/ServiceUnitTests.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using Microsoft.Tye.Hosting.Model; +using Xunit; + +namespace Microsoft.Tye.UnitTests +{ + public class ServiceUnitTests + { + [Theory] + [MemberData(nameof(ServiceStateTestData))] + public void ServiceStateIsBasedOnReplicaStates(ServiceState expected, List replicaStates) + { + Service service = new(new ServiceDescription("test", null), ServiceSource.Unknown); + + for (int i = 0; i < replicaStates.Count; i++) + { + string replicaName = i.ToString(); + + service.Replicas.TryAdd(replicaName, new ReplicaStatus(service, replicaName) + { + State = replicaStates[i], + }); + } + + Assert.Equal(expected, service.State); + + } + + public static IEnumerable ServiceStateTestData => + new List + { + //no replica - should not happen + new object[] { ServiceState.Unknown, new List() }, + + //one replica + new object[] { ServiceState.Starting, new List() { ReplicaState.Added } }, + new object[] { ServiceState.Started, new List() { ReplicaState.Started } }, + new object[] { ServiceState.Started, new List() { ReplicaState.Ready } }, + new object[] { ServiceState.Started, new List() { ReplicaState.Healthy } }, + new object[] { ServiceState.Failed, new List() { ReplicaState.Removed } }, + new object[] { ServiceState.Stopped, new List() { ReplicaState.Stopped } }, + + //multiple replicas + new object[] { ServiceState.Starting, new List() { ReplicaState.Added, ReplicaState.Started, ReplicaState.Ready, ReplicaState.Healthy } }, + new object[] { ServiceState.Started, new List() { ReplicaState.Started, ReplicaState.Ready, ReplicaState.Healthy } }, + new object[] { ServiceState.Degraded, new List() { ReplicaState.Removed, ReplicaState.Started, ReplicaState.Ready, ReplicaState.Healthy } }, + new object[] { ServiceState.Degraded, new List() { ReplicaState.Stopped, ReplicaState.Started, ReplicaState.Ready, ReplicaState.Healthy } }, + new object[] { ServiceState.Degraded, new List() { ReplicaState.Removed, ReplicaState.Stopped, ReplicaState.Started, ReplicaState.Ready, ReplicaState.Healthy } }, + new object[] { ServiceState.Stopped, new List() { ReplicaState.Stopped, ReplicaState.Stopped, ReplicaState.Stopped } }, + }; + } +} From 017c4ae48698a853a1fb57e342db64408a40687e Mon Sep 17 00:00:00 2001 From: "razvan.goga@gmail.com" Date: Tue, 21 Mar 2023 20:51:19 +0100 Subject: [PATCH 2/2] Ansi2Html converter for logs --- .../Ansi2Html/Constants.cs | 55 +++++++++++++++++++ .../Ansi2Html/Converter.cs | 49 +++++++++++++++++ .../Dashboard/Pages/Logs.razor | 9 ++- src/Microsoft.Tye.Hosting/ProcessRunner.cs | 5 +- src/Microsoft.Tye.Hosting/TyeHost.cs | 2 + test/UnitTests/Ansi2HtmlConverterTests.cs | 21 +++++++ 6 files changed, 134 insertions(+), 7 deletions(-) create mode 100644 src/Microsoft.Tye.Hosting/Ansi2Html/Constants.cs create mode 100644 src/Microsoft.Tye.Hosting/Ansi2Html/Converter.cs create mode 100644 test/UnitTests/Ansi2HtmlConverterTests.cs diff --git a/src/Microsoft.Tye.Hosting/Ansi2Html/Constants.cs b/src/Microsoft.Tye.Hosting/Ansi2Html/Constants.cs new file mode 100644 index 000000000..10a7243f1 --- /dev/null +++ b/src/Microsoft.Tye.Hosting/Ansi2Html/Constants.cs @@ -0,0 +1,55 @@ +using System.Collections.Generic; + +namespace Microsoft.Tye.Hosting.Ansi2Html +{ + public static class Constants + { + public const string Red = "#800000"; + public const string Black = "#000000"; + public const string Green = "#008000"; + public const string Yellow = "#808000"; + public const string Blue = "#000080"; + public const string Purple = "#800080"; + public const string Cyan = "#008080"; + public const string LightGray = "#c0c0c0"; + public const string DarkGray = "#808080"; + public const string BrightRed = "#ff0000"; + public const string BrightGreen = "#00ff00"; + public const string BrightYellow = "#ffff00"; + public const string BrightBlue = "#0000ff"; + public const string BrightPurple = "#ff00ff"; + public const string BrightCyan = "#00ffff"; + public const string White = "#ffffff"; + + public static Dictionary ColorMap = new Dictionary() + { + { "0", Black }, // Black + { "1", Red }, // Red + { "2", Green }, // Green + { "3", Yellow }, // Yellow + { "4", Blue }, // Blue + { "5", Purple }, // Purple + { "6", Cyan }, // Cyan + { "7", LightGray }, // Light Gray + { "8", DarkGray }, // Dark Gray + { "9", BrightRed }, // Bright Red + { "10", BrightGreen }, // Bright Green + { "11", BrightYellow }, // Bright Yellow + { "12", BrightBlue }, // Bright Blue + { "13", BrightPurple }, // Bright Purple + { "14", BrightCyan }, // Bright Cyan + { "15", White } // White + }; + + public static class SelectGraphicRenditionParameters + { + public const int Reset = 0; + + public static HashSet SetForeground = new HashSet() + { + 30, 31, 32, 33, 34, 35, 36, 37,38 + }; + + } + } +} diff --git a/src/Microsoft.Tye.Hosting/Ansi2Html/Converter.cs b/src/Microsoft.Tye.Hosting/Ansi2Html/Converter.cs new file mode 100644 index 000000000..abb600717 --- /dev/null +++ b/src/Microsoft.Tye.Hosting/Ansi2Html/Converter.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; + +namespace Microsoft.Tye.Hosting.Ansi2Html +{ + public class Converter + { + private readonly Regex _rule = new Regex("\u001b\\[(?\\d+)(?;\\d+)*m", RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.IgnoreCase); + + public string Parse(string input) + { + var htmlText = _rule.Replace(input, match => + { + var code = Convert.ToInt32(match.Groups["code"].Value); + var args = match.Groups["args"].Value; + List attributes = args.Split(';').ToList(); + + var tagBuilder = new StringBuilder(); + if (code == Constants.SelectGraphicRenditionParameters.Reset) + { + tagBuilder.Append(""); + } + else + { + var color = Constants.White; + if (attributes.Count > 0) + { + string colorCode = attributes.Last(); + if (Constants.ColorMap.ContainsKey(colorCode)) + { + color = Constants.ColorMap[colorCode]; + } + } + + tagBuilder.Append(""); + } + + return tagBuilder.ToString(); + }); + + return htmlText; + } + } +} diff --git a/src/Microsoft.Tye.Hosting/Dashboard/Pages/Logs.razor b/src/Microsoft.Tye.Hosting/Dashboard/Pages/Logs.razor index 34130514f..f801f10b8 100644 --- a/src/Microsoft.Tye.Hosting/Dashboard/Pages/Logs.razor +++ b/src/Microsoft.Tye.Hosting/Dashboard/Pages/Logs.razor @@ -1,6 +1,8 @@ @page "/logs/{ServiceName}" +@using Microsoft.Tye.Hosting.Ansi2Html; @inject IJSRuntime JS @inject Application application +@inject Converter ansi2HtmlParser @implements IDisposable
Name Type Source Bindings Replicas RestartsLogs
+ @serviceState + @if(service.ServiceType == ServiceType.External) { @@ -34,7 +39,7 @@ } else { - @service.Description.Name + @service.Description.Name } @@ -91,7 +96,7 @@ { @service.Replicas.Count/@service.Description.Replicas @service.RestartsViewLogs | Metrics