Compare commits

..

2 Commits

Author SHA1 Message Date
f6f8a3bf65 save 2025-12-22 09:48:17 +07:00
Đăng Nguyễn
a8296063f5 update logs 2025-12-22 09:43:00 +07:00
9 changed files with 311 additions and 12 deletions

View File

@ -72,8 +72,9 @@
new(){Icon = "mdi-view-dashboard", Path="/", Label = "Dashboard", Match = NavLinkMatch.All}, new(){Icon = "mdi-view-dashboard", Path="/", Label = "Dashboard", Match = NavLinkMatch.All},
// new(){Icon = "mdi-map-legend", Path="/maps-manager", Label = "Mapping", Match = NavLinkMatch.All}, // new(){Icon = "mdi-map-legend", Path="/maps-manager", Label = "Mapping", Match = NavLinkMatch.All},
new(){Icon = "mdi-monitor", Path="/robot-monitor", Label = "Robot Monitor", Match = NavLinkMatch.All}, new(){Icon = "mdi-monitor", Path="/robot-monitor", Label = "Robot Monitor", Match = NavLinkMatch.All},
new(){Icon = "mdi-application-cog", Path="/robot-config", Label = "Config", Match = NavLinkMatch.All},
new(){Icon = "mdi-state-machine", Path="/robot-order", Label = "order", Match = NavLinkMatch.All}, new(){Icon = "mdi-state-machine", Path="/robot-order", Label = "order", Match = NavLinkMatch.All},
new(){Icon = "mdi-application-cog", Path="/robot-config", Label = "Config", Match = NavLinkMatch.All },
new(){Icon = "mdi-math-log", Path="/logs", Label = "Logs", Match = NavLinkMatch.All},
]; ];
private bool collapseNavMenu = true; private bool collapseNavMenu = true;

View File

@ -0,0 +1,40 @@
using System.Text.Json.Serialization;
namespace RobotApp.Client.Models;
public class LoggerModel
{
[JsonPropertyName("time")]
public string? Time { get; set; }
[JsonPropertyName("level")]
public string? Level { get; set; }
[JsonPropertyName("message")]
public string? Message { get; set; }
[JsonPropertyName("exception")]
public string? Exception { get; set; }
public string ColorClass => Level switch
{
"WARN" => "text-warning",
"INFO" => "text-info",
"DEBUG" => "text-success",
"ERROR" => "text-danger",
"FATAL" => "text-secondary",
_ => "text-muted",
};
public string BackgroundClass => Level switch
{
"WARN" => "bg-warning text-dark",
"INFO" => "bg-info text-dark",
"DEBUG" => "bg-success text-white",
"ERROR" => "bg-danger text-white",
"FATAL" => "bg-secondary text-white",
_ => "bg-dark text-white",
};
public bool HasException => !string.IsNullOrEmpty(Exception);
}

View File

@ -0,0 +1,176 @@
@page "/logs"
@rendermode InteractiveWebAssemblyNoPrerender
@attribute [Authorize]
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@using RobotApp.Client.Models
@inject IJSRuntime JSRuntime
@inject HttpClient Http
@inject IConfiguration Configuration
@inject ISnackbar Snackbar
<PageTitle>Logs</PageTitle>
<div class="w-100 h-100 d-flex flex-column">
<div class="d-flex flex-row align-items-center justify-content-between" style="border-bottom: 1px solid silver">
<MudTextField Class="mt-1 ms-2" T="string" Value="FilterLog" Adornment="Adornment.End" ValueChanged="OnSearch" AdornmentIcon="@Icons.Material.Filled.Search"
IconSize="Size.Medium" Variant="Variant.Outlined" Margin="Margin.Dense" AdornmentColor="Color.Secondary" Label="Search"></MudTextField>
<MudSpacer />
<div class="m-1 d-flex flex-row">
<MudDatePicker Class="mx-4" Label="Date" Date="DateLog" DateChanged="OnDateChanged" MaxDate="DateTime.Today" Variant="Variant.Outlined" Color="Color.Primary"
ShowToolbar="false" Margin="Margin.Dense" AdornmentColor="Color.Primary" />
<MudTooltip Text="Export">
<MudFab Class="mt-2" Color="Color.Info" StartIcon="@Icons.Material.Filled.ImportExport" Size="Size.Small" OnClick="ExportLogs" />
</MudTooltip>
<MudTooltip Text="Refresh">
<MudFab Class="mx-4 mt-2" StartIcon="@Icons.Material.Filled.Refresh" Color="Color.Primary" Size="Size.Small" OnClick="LoadLogs" />
</MudTooltip>
</div>
</div>
<div class="flex-grow-1 mt-2 ms-2 position-relative" style="background-color: rgba(0, 0, 0, 0);">
<MudOverlay Visible="IsLoading" DarkBackground="true" Absolute="true">
<MudProgressCircular Color="Color.Info" Indeterminate="true" />
</MudOverlay>
<div class="h-100 w-100 position-relative">
<div class="log-container" @ref="LogContainerRef">
@if (ShowRawLog)
{
<div class="d-flex justify-content-center my-3">
<div><MudButton Variant="Variant.Outlined" Size="Size.Small" OnClick="@(() => ShowRawLog = false)">Normal</MudButton></div>
</div>
<big style="font-size: 14px;">
@foreach (var log in ShowLogs)
{
@log <br />
}
</big>
}
else
{
@if (SearchLogs.Count < ShowLogs.Count)
{
<div class="d-flex justify-content-center my-3">
<div><MudButton Variant="Variant.Outlined" Size="Size.Small" OnClick="@(() => ShowRawLog = true)">Raw log</MudButton></div>
</div>
}
@foreach (var log in SearchLogs)
{
<div class="log">
<span class="log-head @log.BackgroundClass">
@log.Time <span class="log-level">@log.Level</span>
</span>
<span>@log.Message</span>
@if (log.HasException)
{
<br />
<pre class="log-exception">
@log.Exception
</pre>
}
</div>
}
}
</div>
</div>
</div>
</div>
@code {
private DateTime DateLog = DateTime.Today;
private bool IsLoading;
private readonly List<string> ShowLogs = new();
private readonly List<LoggerModel> SearchLogs = new();
private ElementReference LogContainerRef { get; set; }
private bool ShowRawLog { get; set; }
private string? FilterLog { get; set; }
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (!firstRender) return;
await LoadLogs();
}
private async Task LoadLogs()
{
try
{
IsLoading = true;
ShowLogs.Clear();
StateHasChanged();
var logs = await Http.GetFromJsonAsync<IEnumerable<string>>($"api/LogsManager?date={DateLog}");
ShowLogs.AddRange(logs ?? []);
IsLoading = false;
StateHasChanged();
await ReloadLogs();
}
catch (AccessTokenNotAvailableException ex)
{
ex.Redirect();
return;
}
}
private async Task ReloadLogs()
{
IsLoading = true;
SearchLogs.Clear();
StateHasChanged();
foreach (var line in ShowLogs.Where(log => string.IsNullOrEmpty(FilterLog) || log.Contains(FilterLog)).TakeLast(2000))
{
try
{
var log = System.Text.Json.JsonSerializer.Deserialize<LoggerModel>(line);
if (log is not null) SearchLogs.Add(log);
}
catch (System.Text.Json.JsonException)
{
continue;
}
}
IsLoading = false;
StateHasChanged();
await JSRuntime.InvokeVoidAsync("ScrollToBottom", LogContainerRef);
}
private async Task OnSearch(string text)
{
FilterLog = text;
await ReloadLogs();
}
private async Task OnDateChanged(DateTime? date)
{
if (date is not null && date.HasValue)
{
DateLog = date.Value;
await LoadLogs();
}
}
private async Task ExportLogs()
{
try
{
var fileContent = await Http.GetFromJsonAsync<IEnumerable<string>>($"api/LogsManager?date={DateLog}");
var formattedContent = string.Join("\n", fileContent ?? []);
var fileName = $"LogsManager_{DateLog.ToShortDateString()}.txt";
await JSRuntime.InvokeVoidAsync("downloadFile", fileName, formattedContent, "text/plain");
}
catch (Exception ex)
{
Snackbar.Add($"Lỗi khi tải file: {ex.Message}", Severity.Warning);
}
}
}

View File

@ -0,0 +1,38 @@
.log-container {
height: 100%;
width: 100%;
overflow-x: hidden;
overflow-y: auto;
position: absolute;
top: 0px;
left: 0px;
display: flex;
flex-direction: column;
}
.log {
word-wrap: break-word;
line-height: 18px;
margin-bottom: 12px;
}
.log-logger {
color: rgba(0, 0, 0, 0.3);
font-size: 12px;
}
.log-level {
display: inline-block;
width: 46px;
}
.log-head {
border-radius: 3px;
padding: 2px 5px;
}
.log-exception {
line-height: 16px;
margin-left: 30px;
color: crimson;
}

View File

@ -22,8 +22,4 @@
<ProjectReference Include="..\RobotApp.VDA5050\RobotApp.VDA5050.csproj" /> <ProjectReference Include="..\RobotApp.VDA5050\RobotApp.VDA5050.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Models\" />
</ItemGroup>
</Project> </Project>

View File

@ -0,0 +1,48 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace RobotApp.Controllers;
[Route("api/[controller]")]
[ApiController]
[Authorize]
public class LogsManagerController(Services.Logger<LogsManagerController> Logger) : ControllerBase
{
private readonly string LoggerDirectory = "logs";
[HttpGet]
public async Task<IEnumerable<string>> GetLogs([FromQuery(Name = "date")] DateTime date)
{
string temp = "";
try
{
string fileName = $"{date:yyyy-MM-dd}.log";
string path = Path.Combine(LoggerDirectory, fileName);
if (!Path.GetFullPath(path).StartsWith(Path.GetFullPath(LoggerDirectory)))
{
Logger.Warning($"GetLogs: phát hiện đường dẫn không hợp lệ.");
return [];
}
if (!System.IO.File.Exists(path))
{
Logger.Warning($"GetLogs: không tìm thấy file log của ngày {date.ToShortDateString()} - {path}.");
return [];
}
temp = Path.Combine(LoggerDirectory, $"{Guid.NewGuid()}.log");
System.IO.File.Copy(path, temp);
return await System.IO.File.ReadAllLinesAsync(temp);
}
catch (Exception ex)
{
Logger.Warning($"GetLogs: Hệ thống có lỗi xảy ra - {ex.Message}");
return [];
}
finally
{
if (System.IO.File.Exists(temp)) System.IO.File.Delete(temp);
}
}
}

View File

@ -17,7 +17,7 @@
"dotnetRunMessages": true, "dotnetRunMessages": true,
"launchBrowser": true, "launchBrowser": true,
"workingDirectory": "$(TargetDir)", "workingDirectory": "$(TargetDir)",
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", //"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "https://0.0.0.0:7150;http://localhost:5229", "applicationUrl": "https://0.0.0.0:7150;http://localhost:5229",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"

View File

@ -24,8 +24,8 @@ public class MQTTClient : IAsyncDisposable
public event Action<string>? InstanceActionsChanged; public event Action<string>? InstanceActionsChanged;
public bool IsConnected => !IsDisposed && MqttClient is not null && MqttClient.IsConnected; public bool IsConnected => !IsDisposed && MqttClient is not null && MqttClient.IsConnected;
private string OrderTopic => $"{VDA5050Setting.TopicPrefix}/{ClientId}/{VDA5050Topic.ORDER.ToTopicString()}"; private string OrderTopic => $"{VDA5050Setting.TopicPrefix}/{VDA5050Setting.Manufacturer}/{ClientId}/{VDA5050Topic.ORDER.ToTopicString()}";
private string InstanceActionsTopic => $"{VDA5050Setting.TopicPrefix}/{ClientId}/{VDA5050Topic.INSTANTACTIONS.ToTopicString()}"; private string InstanceActionsTopic => $"{VDA5050Setting.TopicPrefix}/{VDA5050Setting.Manufacturer}/{ClientId}/{VDA5050Topic.INSTANTACTIONS.ToTopicString()}";
public MQTTClient(string clientId, VDA5050Setting setting, Logger<MQTTClient> logger) public MQTTClient(string clientId, VDA5050Setting setting, Logger<MQTTClient> logger)
{ {

View File

@ -22,7 +22,7 @@ public class RobotConnection(RobotConfiguration RobotConfiguration,
{ {
try try
{ {
Logger.Debug($"Nhận Order: {data}"); //Logger.Debug($"Nhận Order: {data}");
var msg = JsonSerializer.Deserialize<OrderMsg>(data, JsonOptionExtends.Read); var msg = JsonSerializer.Deserialize<OrderMsg>(data, JsonOptionExtends.Read);
if (msg is null || string.IsNullOrEmpty(msg.SerialNumber) || msg.SerialNumber != RobotConfiguration.SerialNumber) return; if (msg is null || string.IsNullOrEmpty(msg.SerialNumber) || msg.SerialNumber != RobotConfiguration.SerialNumber) return;
OrderUpdated?.Invoke(msg); OrderUpdated?.Invoke(msg);
@ -37,7 +37,7 @@ public class RobotConnection(RobotConfiguration RobotConfiguration,
{ {
try try
{ {
Logger.Debug($"Nhận InstanceActions: {data}"); //Logger.Debug($"Nhận InstanceActions: {data}");
var msg = JsonSerializer.Deserialize<InstantActionsMsg>(data, JsonOptionExtends.Read); var msg = JsonSerializer.Deserialize<InstantActionsMsg>(data, JsonOptionExtends.Read);
if (msg is null || string.IsNullOrEmpty(msg.SerialNumber) || msg.SerialNumber != RobotConfiguration.SerialNumber) return; if (msg is null || string.IsNullOrEmpty(msg.SerialNumber) || msg.SerialNumber != RobotConfiguration.SerialNumber) return;
ActionUpdated?.Invoke(msg); ActionUpdated?.Invoke(msg);
@ -50,7 +50,7 @@ public class RobotConnection(RobotConfiguration RobotConfiguration,
public async Task<MessageResult> Publish(string topic, string data) public async Task<MessageResult> Publish(string topic, string data)
{ {
if (MqttClient is not null && MqttClient.IsConnected) return await MqttClient.PublishAsync($"{VDA5050Setting.TopicPrefix}/{RobotConfiguration.SerialNumber}/{topic}", data); if (MqttClient is not null && MqttClient.IsConnected) return await MqttClient.PublishAsync($"{VDA5050Setting.TopicPrefix}/{VDA5050Setting.Manufacturer}/{RobotConfiguration.SerialNumber}/{topic}", data);
return new(false, "Chưa có kết nối tới broker"); return new(false, "Chưa có kết nối tới broker");
} }