From 33a14f8925c2c7828f9933aff1a17d36b4b78941 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Sat, 15 Feb 2025 14:39:36 +0100 Subject: [PATCH 1/2] feat(FleetTelemetryWebSocketService): do not disconnect Fleet tElemetry WebSocket on Car settings save --- .../Server/Services/ConfigJsonService.cs | 6 ------ .../IFleetTelemetryWebSocketService.cs | 1 - .../Services/FleetTelemetryWebSocketService.cs | 18 ------------------ 3 files changed, 25 deletions(-) diff --git a/TeslaSolarCharger/Server/Services/ConfigJsonService.cs b/TeslaSolarCharger/Server/Services/ConfigJsonService.cs index c0fdc21a9..876751c27 100644 --- a/TeslaSolarCharger/Server/Services/ConfigJsonService.cs +++ b/TeslaSolarCharger/Server/Services/ConfigJsonService.cs @@ -266,12 +266,6 @@ public async Task UpdateCarBasicConfiguration(int carId, CarBasicConfiguration c settingsCar.ShouldBeManaged = carBasicConfiguration.ShouldBeManaged; settingsCar.UseBle = carBasicConfiguration.UseBle; settingsCar.BleApiBaseUrl = carBasicConfiguration.BleApiBaseUrl; - await fleetTelemetryWebSocketService.DisconnectWebSocketsByVin(carBasicConfiguration.Vin); - } - - public Task UpdateCarConfiguration(int carId, DepricatedCarConfiguration carConfiguration) - { - throw new NotImplementedException(); } public async Task SaveOrUpdateCar(DtoCar car) diff --git a/TeslaSolarCharger/Server/Services/Contracts/IFleetTelemetryWebSocketService.cs b/TeslaSolarCharger/Server/Services/Contracts/IFleetTelemetryWebSocketService.cs index 6d858b861..ffaf15f7d 100644 --- a/TeslaSolarCharger/Server/Services/Contracts/IFleetTelemetryWebSocketService.cs +++ b/TeslaSolarCharger/Server/Services/Contracts/IFleetTelemetryWebSocketService.cs @@ -3,6 +3,5 @@ public interface IFleetTelemetryWebSocketService { Task ReconnectWebSocketsForEnabledCars(); - Task DisconnectWebSocketsByVin(string vin); bool IsClientConnected(string vin); } diff --git a/TeslaSolarCharger/Server/Services/FleetTelemetryWebSocketService.cs b/TeslaSolarCharger/Server/Services/FleetTelemetryWebSocketService.cs index e3379d109..dde94734c 100644 --- a/TeslaSolarCharger/Server/Services/FleetTelemetryWebSocketService.cs +++ b/TeslaSolarCharger/Server/Services/FleetTelemetryWebSocketService.cs @@ -108,24 +108,6 @@ await existingClient.WebSocketClient.SendAsync(segment, WebSocketMessageType.Tex } } - public async Task DisconnectWebSocketsByVin(string vin) - { - logger.LogTrace("{method}({vin})", nameof(DisconnectWebSocketsByVin), vin); - var client = Clients.FirstOrDefault(c => c.Vin == vin); - if (client != default) - { - if (client.WebSocketClient.State == WebSocketState.Open) - { - await client.WebSocketClient - .CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", new CancellationTokenSource(_heartbeatsendTimeout).Token) - .ConfigureAwait(false); - } - - client.WebSocketClient.Dispose(); - Clients.Remove(client); - } - } - private async Task ConnectToFleetTelemetryApi(string vin) { logger.LogTrace("{method}({carId})", nameof(ConnectToFleetTelemetryApi), vin); From dca8ce09e0e4c0d29cbdbf993dfe7c572b1f1cfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20K=C3=BChnel?= Date: Sat, 15 Feb 2025 16:48:50 +0100 Subject: [PATCH 2/2] feat(SupporRazor): can reconfigure memory logs during runtime --- .../IInMemorySink.cs | 12 +++ .../InMemorySink.cs | 31 ++++-- TeslaSolarCharger/Client/Pages/Support.razor | 97 ++++++++++++++++++- .../Server/Controllers/DebugController.cs | 43 ++++---- TeslaSolarCharger/Server/Program.cs | 9 +- .../Server/Services/BackendApiService.cs | 1 + .../Services/Contracts/IDebugService.cs | 5 + .../Server/Services/DebugService.cs | 45 ++++++++- TeslaSolarCharger/Server/appsettings.json | 1 + 9 files changed, 208 insertions(+), 36 deletions(-) create mode 100644 PkSoftwareService.Custom.Backend/IInMemorySink.cs diff --git a/PkSoftwareService.Custom.Backend/IInMemorySink.cs b/PkSoftwareService.Custom.Backend/IInMemorySink.cs new file mode 100644 index 000000000..7d8b1b756 --- /dev/null +++ b/PkSoftwareService.Custom.Backend/IInMemorySink.cs @@ -0,0 +1,12 @@ +namespace PkSoftwareService.Custom.Backend; + +public interface IInMemorySink +{ + /// + /// Returns a snapshot of the current log messages. + /// + List GetLogs(); + + int GetCapacity(); + void UpdateCapacity(int newCapacity); +} diff --git a/PkSoftwareService.Custom.Backend/InMemorySink.cs b/PkSoftwareService.Custom.Backend/InMemorySink.cs index a59c894f5..4f3599180 100644 --- a/PkSoftwareService.Custom.Backend/InMemorySink.cs +++ b/PkSoftwareService.Custom.Backend/InMemorySink.cs @@ -4,10 +4,10 @@ namespace PkSoftwareService.Custom.Backend; -public class InMemorySink : ILogEventSink +public class InMemorySink : ILogEventSink, IInMemorySink { - private readonly int _capacity; - private readonly Queue _logMessages; + private int _capacity; + private Queue _logMessages; private readonly object _syncRoot = new object(); private readonly MessageTemplateTextFormatter _formatter; @@ -17,7 +17,7 @@ public class InMemorySink : ILogEventSink /// The output template (should match your Console sink). /// Optional format provider. /// Max number of messages to store. - public InMemorySink(string outputTemplate, IFormatProvider? formatProvider = null, int capacity = 20000) + public InMemorySink(string outputTemplate, int capacity, IFormatProvider? formatProvider = null) { _capacity = capacity; _logMessages = new Queue(capacity); @@ -53,14 +53,25 @@ public List GetLogs() } } - /// - /// Optionally clear all logs. - /// - public void Clear() + public int GetCapacity() { - lock (_syncRoot) + return _capacity; + } + + public void UpdateCapacity(int newCapacity) + { + if (newCapacity < 1) { - _logMessages.Clear(); + throw new ArgumentException("Log capacity can not be lower than 1"); + } + + if (newCapacity < _capacity) + { + lock (_syncRoot) + { + _logMessages = new(_logMessages.TakeLast(newCapacity)); + } } + _capacity = newCapacity; } } diff --git a/TeslaSolarCharger/Client/Pages/Support.razor b/TeslaSolarCharger/Client/Pages/Support.razor index 5f8cdfe92..545d1f9de 100644 --- a/TeslaSolarCharger/Client/Pages/Support.razor +++ b/TeslaSolarCharger/Client/Pages/Support.razor @@ -9,18 +9,55 @@

Support

+ +

Logging

+
Never share logs publicly
Logs might contain sensitive information like your vehicle's location. Do not share logs publicly.
-

General

+ + Do not change the configuration as this might lead to extremly high memory usage. All Settings will be reset after a restart. + + +
+ @if (_logCapacity == default || _logLevel == default) + { + + } + else + { +
+ +
+
+ +
+ } +
+ +
+

Car Debug Details

@if (_debugCars == default) { @@ -38,7 +75,7 @@ else
Is Available in Tesla account: @car.Value.IsAvailableInTeslaAccount
Should be managed: @car.Value.ShouldBeManaged
- + @if (car.Value.Vin != default && _fleetTelemetryGetConfigs.TryGetValue(car.Value.Vin, out var config)) {

Fleet Telemetry Config

@@ -50,13 +87,13 @@ else IsDisabled="@(car.Value.Vin == default || !car.Value.IsAvailableInTeslaAccount)" DisabledToolTipText="@(car.Value.IsAvailableInTeslaAccount ? null : "Can not check config as car is not part of Tesla account")" OnButtonClicked="@(() => GetFleetTelemetryConfig(car.Value.Vin))"> - + @if (car.Value.Vin != default && _fleetTelemetrySetResults.TryGetValue(car.Value.Vin, out var result)) {

Fleet Telemetry SetResult

@result
} - + ? _logLevel; + private string? _logLevelSetErrorMessage; + private DtoValue? _logCapacity; + private string? _logCapacitySetErrorMessage; + + private Dictionary LogLevelOptions => new() + { + { "Verbose", "Verbose" }, + { "Debug", "Debug" }, + { "Information", "Information" }, + { "Warning", "Warning" }, + { "Error", "Error" }, + { "Fatal", "Fatal" }, + }; protected override async Task OnInitializedAsync() { @@ -91,6 +142,8 @@ else { _debugCars = cars; } + _logLevel = await HttpClientHelper.SendGetRequestWithSnackbarAsync>("api/Debug/GetLogLevel"); + _logCapacity = await HttpClientHelper.SendGetRequestWithSnackbarAsync>("api/Debug/GetLogCapacity"); } @@ -139,4 +192,40 @@ else _fleetTelemetrySetResults[vin] = stringToDisplay; _isFleetTelemetryLoading = false; } + + private async Task UpdateLogLevel() + { + if (_logLevel == default) + { + return; + } + + if (string.IsNullOrEmpty(_logLevel.Value)) + { + return; + } + var result = await HttpClientHelper.SendPostRequestAsync($"api/Debug/SetLogLevel?level={Uri.EscapeDataString(_logLevel.Value)}", null); + if (result.HasError) + { + _logLevelSetErrorMessage = result.ErrorMessage; + return; + } + Snackbar.Add("Log level updated", Severity.Success); + } + + private async Task UpdateLogCapacity() + { + if (_logCapacity == default) + { + return; + } + var result = await HttpClientHelper.SendPostRequestAsync($"api/Debug/SetLogCapacity?capacity={_logCapacity.Value}", null); + if (result.HasError) + { + _logCapacitySetErrorMessage = result.ErrorMessage; + return; + } + Snackbar.Add("Log capacity updated", Severity.Success); + } + } diff --git a/TeslaSolarCharger/Server/Controllers/DebugController.cs b/TeslaSolarCharger/Server/Controllers/DebugController.cs index 8c4268c00..dfb849a41 100644 --- a/TeslaSolarCharger/Server/Controllers/DebugController.cs +++ b/TeslaSolarCharger/Server/Controllers/DebugController.cs @@ -10,28 +10,25 @@ namespace TeslaSolarCharger.Server.Controllers; -public class DebugController(InMemorySink inMemorySink, - Serilog.Core.LoggingLevelSwitch inMemoryLogLevelSwitch, - IFleetTelemetryConfigurationService fleetTelemetryConfigurationService,/*chaning log level switch is not tested*/ - ISettings settings, +public class DebugController(IFleetTelemetryConfigurationService fleetTelemetryConfigurationService, IDebugService debugService) : ApiBaseController { [HttpGet] public IActionResult DownloadLogs() { - // Get the logs from the in-memory sink. - var logs = inMemorySink.GetLogs(); - - // Join the log entries into a single string, separated by new lines. - var content = string.Join(Environment.NewLine, logs); - - // Convert the string content to a byte array (UTF8 encoding). - var bytes = Encoding.UTF8.GetBytes(content); + var bytes = debugService.GetLogBytes(); // Return the file with the appropriate content type and file name. return File(bytes, "text/plain", "logs.log"); } + [HttpGet] + public IActionResult GetLogLevel() + { + var level = debugService.GetLogLevel(); + return Ok(new DtoValue(level)); + } + /// /// Adjusts the minimum log level for the in-memory sink. /// @@ -40,12 +37,22 @@ public IActionResult DownloadLogs() [HttpPost] public IActionResult SetLogLevel([FromQuery] string level) { - if (!Enum.TryParse(level, true, out var newLevel)) - { - return BadRequest("Invalid log level. Use one of: Verbose, Debug, Information, Warning, Error, Fatal"); - } - inMemoryLogLevelSwitch.MinimumLevel = newLevel; - return Ok($"In-memory sink log level changed to {newLevel}"); + debugService.SetLogLevel(level); + return Ok(); + } + + [HttpGet] + public IActionResult GetLogCapacity() + { + var capacity = debugService.GetLogCapacity(); + return Ok(new DtoValue(capacity)); + } + + [HttpPost] + public IActionResult SetLogCapacity([FromQuery] int capacity) + { + debugService.SetLogCapacity(capacity); + return Ok(); } [HttpGet] diff --git a/TeslaSolarCharger/Server/Program.cs b/TeslaSolarCharger/Server/Program.cs index 8719c0f79..98b2600ac 100644 --- a/TeslaSolarCharger/Server/Program.cs +++ b/TeslaSolarCharger/Server/Program.cs @@ -50,12 +50,15 @@ builder.Host.UseSerilog(); const string outputTemplate = "[{Timestamp:dd-MMM-yyyy HH:mm:ss.fff} {Level:u3} {SourceContext}] {Message:lj}{NewLine}{Exception}"; -var inMemoryLevelSwitch = new LoggingLevelSwitch(LogEventLevel.Verbose); -var inMemorySink = new InMemorySink(outputTemplate, capacity: 20000); +var inMemorySink = new InMemorySink(outputTemplate, capacity: configurationManager.GetValue("InMemoryLogDefaultCapacity")); -builder.Services.AddSingleton(inMemoryLevelSwitch); +builder.Services.AddSingleton(inMemorySink); builder.Services.AddSingleton(inMemorySink); +var inMemoryLevelSwitch = new LoggingLevelSwitch(LogEventLevel.Verbose); +builder.Services.AddSingleton(inMemoryLevelSwitch); + + var app = builder.Build(); Log.Logger = new LoggerConfiguration() diff --git a/TeslaSolarCharger/Server/Services/BackendApiService.cs b/TeslaSolarCharger/Server/Services/BackendApiService.cs index 235bc23a6..4c714644f 100644 --- a/TeslaSolarCharger/Server/Services/BackendApiService.cs +++ b/TeslaSolarCharger/Server/Services/BackendApiService.cs @@ -144,6 +144,7 @@ await errorHandlingService.HandleError(nameof(BackendApiService), nameof(Refresh "Could not refresh backend token", result.ErrorMessage ?? string.Empty, issueKeys.BackendTokenNotRefreshable, null, null); logger.LogError("Could not refresh backend token. {errorMessage}", result.ErrorMessage); memoryCache.Remove(constants.BackendTokenStateKey); + logger.LogError("Could not refresh backend token. Error Message: {errorMessage}", result.ErrorMessage); throw new InvalidOperationException($"Could not refresh backend token {result.ErrorMessage}"); } await errorHandlingService.HandleErrorResolved(issueKeys.BackendTokenNotRefreshable, null); diff --git a/TeslaSolarCharger/Server/Services/Contracts/IDebugService.cs b/TeslaSolarCharger/Server/Services/Contracts/IDebugService.cs index ae9da1632..0e65ead21 100644 --- a/TeslaSolarCharger/Server/Services/Contracts/IDebugService.cs +++ b/TeslaSolarCharger/Server/Services/Contracts/IDebugService.cs @@ -5,4 +5,9 @@ namespace TeslaSolarCharger.Server.Services.Contracts; public interface IDebugService { Task> GetCars(); + byte[] GetLogBytes(); + void SetLogLevel(string level); + void SetLogCapacity(int capacity); + string GetLogLevel(); + int GetLogCapacity(); } diff --git a/TeslaSolarCharger/Server/Services/DebugService.cs b/TeslaSolarCharger/Server/Services/DebugService.cs index 3e9fad4e4..e720ed5f7 100644 --- a/TeslaSolarCharger/Server/Services/DebugService.cs +++ b/TeslaSolarCharger/Server/Services/DebugService.cs @@ -1,4 +1,8 @@ using Microsoft.EntityFrameworkCore; +using PkSoftwareService.Custom.Backend; +using Serilog; +using Serilog.Events; +using System.Text; using TeslaSolarCharger.Model.Contracts; using TeslaSolarCharger.Server.Services.Contracts; using TeslaSolarCharger.Shared.Dtos.Support; @@ -6,7 +10,9 @@ namespace TeslaSolarCharger.Server.Services; public class DebugService(ILogger logger, - ITeslaSolarChargerContext context) : IDebugService + ITeslaSolarChargerContext context, + IInMemorySink inMemorySink, + Serilog.Core.LoggingLevelSwitch inMemoryLogLevelSwitch) : IDebugService { public async Task> GetCars() { @@ -23,4 +29,41 @@ public async Task> GetCars() logger.LogDebug("Found {carCount} cars", cars.Count); return cars; } + + public byte[] GetLogBytes() + { + logger.LogTrace("{method}", nameof(GetLogBytes)); + var logEntries = inMemorySink.GetLogs(); + var content = string.Join(Environment.NewLine, logEntries); + var bytes = Encoding.UTF8.GetBytes(content); + return bytes; + } + + public string GetLogLevel() + { + logger.LogTrace("{method}", nameof(GetLogLevel)); + return inMemoryLogLevelSwitch.MinimumLevel.ToString(); + } + + public void SetLogLevel(string level) + { + logger.LogTrace("{method} {level}", nameof(SetLogLevel), level); + if (!Enum.TryParse(level, true, out var newLevel)) + { + throw new ArgumentException("Invalid log level. Use one of: Verbose, Debug, Information, Warning, Error, Fatal", nameof(level)); + } + inMemoryLogLevelSwitch.MinimumLevel = newLevel; + } + + public int GetLogCapacity() + { + logger.LogTrace("{method}", nameof(GetLogCapacity)); + return inMemorySink.GetCapacity(); + } + + public void SetLogCapacity(int capacity) + { + logger.LogTrace("{method} {capacity}", nameof(SetLogCapacity), capacity); + inMemorySink.UpdateCapacity(capacity); + } } diff --git a/TeslaSolarCharger/Server/appsettings.json b/TeslaSolarCharger/Server/appsettings.json index 5ff48ea27..e89a7a704 100644 --- a/TeslaSolarCharger/Server/appsettings.json +++ b/TeslaSolarCharger/Server/appsettings.json @@ -40,6 +40,7 @@ "FleetTelemetryApiUrl": "wss://api.fleet-telemetry.solar4car.com/notokenws?", "BetaFleetTelemetryApiUrl": "wss://beta.api.fleet-telemetry.solar4car.com/notokenws?", "BackendPasswordDefaultLength": 25, + "InMemoryLogDefaultCapacity": 50000, "GridPriceProvider": { "EnergyProvider": "FixedPrice", "Octopus": {