diff --git a/LTEMvcApp/Controllers/ClientController.cs b/LTEMvcApp/Controllers/ClientController.cs new file mode 100644 index 0000000..9591325 --- /dev/null +++ b/LTEMvcApp/Controllers/ClientController.cs @@ -0,0 +1,173 @@ +using Microsoft.AspNetCore.Mvc; +using LTEMvcApp.Models; +using LTEMvcApp.Services; +using Microsoft.Extensions.Logging; + +namespace LTEMvcApp.Controllers +{ + /// + /// 客户端管理控制器 - 负责客户端的基本管理功能 + /// + [ApiController] + [Route("api/[controller]")] + public class ClientController : ControllerBase + { + private readonly WebSocketManagerService _webSocketManager; + private readonly ILogger _logger; + + public ClientController(WebSocketManagerService webSocketManager, ILogger logger) + { + _webSocketManager = webSocketManager; + _logger = logger; + } + + /// + /// 获取所有客户端状态 + /// + /// 客户端状态列表 + [HttpGet("states")] + public ActionResult> GetClientStates() + { + _logger.LogInformation("获取所有客户端状态"); + var states = _webSocketManager.GetAllClientStates(); + return Ok(states); + } + + /// + /// 启动客户端 + /// + /// 客户端名称 + /// 操作结果 + [HttpPost("{clientName}/start")] + public ActionResult StartClient(string clientName) + { + _logger.LogInformation($"API请求: 启动客户端 {clientName}"); + var success = _webSocketManager.StartClient(clientName); + if (success) + { + _logger.LogInformation($"客户端 {clientName} 启动成功"); + return Ok(new { message = $"客户端 '{clientName}' 已启动" }); + } + else + { + _logger.LogWarning($"客户端 {clientName} 启动失败"); + return BadRequest($"启动客户端 '{clientName}' 失败"); + } + } + + /// + /// 停止客户端 + /// + /// 客户端名称 + /// 操作结果 + [HttpPost("{clientName}/stop")] + public ActionResult StopClient(string clientName) + { + var success = _webSocketManager.StopClient(clientName); + if (success) + return Ok(new { message = $"客户端 '{clientName}' 已停止" }); + else + return BadRequest($"停止客户端 '{clientName}' 失败"); + } + + /// + /// 播放/暂停客户端 + /// + /// 客户端名称 + /// 操作结果 + [HttpPost("{clientName}/playpause")] + public ActionResult PlayPauseClient(string clientName) + { + var success = _webSocketManager.PlayPauseClient(clientName); + if (success) + return Ok(new { message = $"客户端 '{clientName}' 播放/暂停状态已切换" }); + else + return BadRequest($"切换客户端 '{clientName}' 播放/暂停状态失败"); + } + + /// + /// 重置客户端日志 + /// + /// 客户端名称 + /// 操作结果 + [HttpPost("{clientName}/reset-logs")] + public ActionResult ResetClientLogs(string clientName) + { + var success = _webSocketManager.ResetClientLogs(clientName); + if (success) + return Ok(new { message = $"客户端 '{clientName}' 日志已重置" }); + else + return BadRequest($"重置客户端 '{clientName}' 日志失败"); + } + + /// + /// 获取客户端日志 + /// + /// 客户端名称 + /// 日志数量限制 + /// 日志列表 + [HttpGet("{clientName}/logs")] + public ActionResult?> GetClientLogs(string clientName, [FromQuery] int limit = 100) + { + var logs = _webSocketManager.GetClientLogs(clientName); + if (logs == null) + return NotFound($"客户端 '{clientName}' 不存在或未连接"); + + // 限制返回的日志数量 + var limitedLogs = logs.TakeLast(limit).ToList(); + return Ok(limitedLogs); + } + + /// + /// 设置客户端日志配置 + /// + /// 客户端名称 + /// 请求体 + [HttpPost("{clientName}/logs-config")] + public ActionResult SetClientLogsConfig(string clientName, [FromBody] ClientLogsConfig request) + { + var success = _webSocketManager.SetClientLogsConfig(clientName, request); + if (success) + { + return Ok(new { message = "日志配置已更新" }); + } + else + { + return NotFound(new { message = $"客户端 '{clientName}' 未找到或更新失败" }); + } + } + + /// + /// 获取连接统计信息 + /// + /// 统计信息 + [HttpGet("statistics")] + public ActionResult GetStatistics() + { + var stats = _webSocketManager.GetConnectionStatistics(); + return Ok(stats); + } + + /// + /// 启动所有已配置的客户端 + /// + /// 操作结果 + [HttpPost("start-all")] + public ActionResult StartAllClients() + { + _webSocketManager.StartAllConfiguredClients(); + return Ok(new { message = "所有已配置的客户端已启动" }); + } + + /// + /// 停止所有客户端 + /// + /// 操作结果 + [HttpPost("stop-all")] + public ActionResult StopAllClients() + { + _webSocketManager.StopAllClients(); + return Ok(new { message = "所有客户端已停止" }); + } + } +} \ No newline at end of file diff --git a/LTEMvcApp/Controllers/ConfigController.cs b/LTEMvcApp/Controllers/ConfigController.cs new file mode 100644 index 0000000..b11f7d9 --- /dev/null +++ b/LTEMvcApp/Controllers/ConfigController.cs @@ -0,0 +1,83 @@ +using Microsoft.AspNetCore.Mvc; +using LTEMvcApp.Models; +using LTEMvcApp.Services; +using Microsoft.Extensions.Logging; + +namespace LTEMvcApp.Controllers +{ + /// + /// 配置管理控制器 - 负责普通客户端配置管理 + /// + [ApiController] + [Route("api/[controller]")] + public class ConfigController : ControllerBase + { + private readonly WebSocketManagerService _webSocketManager; + private readonly ILogger _logger; + + public ConfigController(WebSocketManagerService webSocketManager, ILogger logger) + { + _webSocketManager = webSocketManager; + _logger = logger; + } + + /// + /// 获取客户端配置 + /// + /// 客户端名称 + /// 客户端配置 + [HttpGet("{clientName}")] + public ActionResult GetClientConfig(string clientName) + { + var config = _webSocketManager.GetClientConfig(clientName); + if (config == null) + return NotFound($"客户端 '{clientName}' 不存在"); + + return Ok(config); + } + + /// + /// 获取所有客户端配置 + /// + /// 客户端配置列表 + [HttpGet] + public ActionResult> GetAllConfigs() + { + var configs = _webSocketManager.GetAllClientConfigs(); + return Ok(configs); + } + + /// + /// 添加客户端配置 + /// + /// 客户端配置 + /// 操作结果 + [HttpPost] + public ActionResult AddClientConfig([FromBody] ClientConfig config) + { + if (string.IsNullOrEmpty(config.Name)) + return BadRequest("客户端名称不能为空"); + + var success = _webSocketManager.AddClientConfig(config); + if (success) + return Ok(new { message = $"客户端 '{config.Name}' 配置已添加" }); + else + return BadRequest("添加客户端配置失败"); + } + + /// + /// 移除客户端配置 + /// + /// 客户端名称 + /// 操作结果 + [HttpDelete("{clientName}")] + public ActionResult RemoveClientConfig(string clientName) + { + var success = _webSocketManager.RemoveClientConfig(clientName); + if (success) + return Ok(new { message = $"客户端 '{clientName}' 配置已移除" }); + else + return BadRequest($"移除客户端 '{clientName}' 配置失败"); + } + } +} \ No newline at end of file diff --git a/LTEMvcApp/Controllers/HomeController.cs b/LTEMvcApp/Controllers/HomeController.cs index fc254c5..4847cd0 100644 --- a/LTEMvcApp/Controllers/HomeController.cs +++ b/LTEMvcApp/Controllers/HomeController.cs @@ -28,7 +28,7 @@ public class HomeController : Controller ViewBag.ClientConfigs = configs; // 获取测试客户端配置 - var testConfig = _webSocketManager.GetTestClientConfig(); + var testConfig = _webSocketManager.GetDefaultTestClientConfig(); var testClient = _webSocketManager.GetTestClient(); var clientState = testClient?.State ?? LTEMvcApp.Models.ClientState.Stop; @@ -147,8 +147,11 @@ public class HomeController : Controller /// public IActionResult TestClientConfig() { - var testConfig = _webSocketManager.GetTestClientConfig(); + var testConfig = _webSocketManager.GetDefaultTestClientConfig(); + var allTestConfigs = _webSocketManager.GetAllTestClientConfigs(); + ViewBag.TestConfig = testConfig; + ViewBag.AllTestConfigs = allTestConfigs; return View(); } diff --git a/LTEMvcApp/Controllers/LogController.cs b/LTEMvcApp/Controllers/LogController.cs new file mode 100644 index 0000000..0395920 --- /dev/null +++ b/LTEMvcApp/Controllers/LogController.cs @@ -0,0 +1,487 @@ +using Microsoft.AspNetCore.Mvc; +using LTEMvcApp.Models; +using LTEMvcApp.Services; +using Microsoft.Extensions.Logging; +using System.Threading.Tasks; +using System.Threading; +using System.IO; +using System.Text; + +namespace LTEMvcApp.Controllers +{ + /// + /// 日志管理控制器 - 负责日志相关的管理功能 + /// + [ApiController] + [Route("api/[controller]")] + public class LogController : ControllerBase + { + private readonly WebSocketManagerService _webSocketManager; + private readonly ILogger _logger; + private readonly string _logsDirectory = "ClientMessageLogs"; + + public LogController(WebSocketManagerService webSocketManager, ILogger logger) + { + _webSocketManager = webSocketManager; + _logger = logger; + + // 确保日志目录存在 + if (!Directory.Exists(_logsDirectory)) + { + Directory.CreateDirectory(_logsDirectory); + } + } + + /// + /// 获取日志缓存统计信息 + /// + /// 统计信息 + [HttpGet("stats")] + public ActionResult GetLogCacheStats() + { + try + { + var stats = new + { + totalLogs = _webSocketManager.GetLogCacheCount(), + cacheSize = 10000, // LogCacheSize + timestamp = DateTime.UtcNow + }; + return Ok(stats); + } + catch (Exception ex) + { + _logger.LogError(ex, "获取日志缓存统计信息时发生错误"); + return StatusCode(500, new { message = "获取统计信息失败", error = ex.Message }); + } + } + + /// + /// 清空全局日志缓存 + /// + /// 操作结果 + [HttpPost("clear")] + public ActionResult ClearLogCache() + { + try + { + _webSocketManager.ClearLogCache(); + return Ok(new { message = "日志缓存已清空" }); + } + catch (Exception ex) + { + _logger.LogError(ex, "清空日志缓存时发生错误"); + return StatusCode(500, new { message = "清空日志缓存失败", error = ex.Message }); + } + } + + /// + /// 重置全局日志缓存 + /// + /// 操作结果 + [HttpPost("reset")] + public ActionResult ResetLogCache() + { + try + { + _webSocketManager.ResetLogCache(); + return Ok(new { message = "日志缓存已重置" }); + } + catch (Exception ex) + { + _logger.LogError(ex, "重置日志缓存时发生错误"); + return StatusCode(500, new { message = "重置日志缓存失败", error = ex.Message }); + } + } + + /// + /// 添加测试日志数据 + /// + /// 操作结果 + [HttpPost("add-test-data")] + public ActionResult AddTestLogData() + { + try + { + var testLogs = new List(); + _webSocketManager.AddLogsToCache(testLogs); + return Ok(new { message = $"已添加 {testLogs.Count} 条测试日志" }); + } + catch (Exception ex) + { + _logger.LogError(ex, "添加测试日志数据时发生错误"); + return StatusCode(500, new { message = "添加测试日志失败", error = ex.Message }); + } + } + + /// + /// 使用 Server-Sent Events (SSE) 实时推送全局日志 + /// + [HttpGet("stream")] + public async Task StreamLogs(CancellationToken cancellationToken) + { + try + { + Response.ContentType = "text/event-stream"; + Response.Headers.Append("Cache-Control", "no-cache"); + Response.Headers.Append("Connection", "keep-alive"); + Response.Headers.Append("Access-Control-Allow-Origin", "*"); + + // 发送连接成功事件 + await SendSseEvent("connected", new { message = "日志流连接已建立", timestamp = DateTime.UtcNow }); + await Response.Body.FlushAsync(cancellationToken); + + int lastLogCount = 0; + var lastLogs = new List(); + var lastLogHash = string.Empty; + + // 首先,一次性推送所有已缓存的日志 + try + { + var initialLogs = _webSocketManager.GetLogCache()?.ToList() ?? new List(); + _logger.LogInformation("StreamLogs: 获取到初始日志 {Count} 条", initialLogs.Count); + + if (initialLogs.Any()) + { + _logger.LogInformation("StreamLogs: 发送历史日志事件,日志数量: {Count}", initialLogs.Count); + await SendSseEvent("history", new { logs = initialLogs, totalCount = initialLogs.Count }); + await Response.Body.FlushAsync(cancellationToken); + lastLogCount = initialLogs.Count; + lastLogs = initialLogs.ToList(); + lastLogHash = CalculateLogsHash(initialLogs); + } + else + { + _logger.LogInformation("StreamLogs: 没有历史日志数据"); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "获取初始日志时发生错误"); + await SendSseEvent("error", new { message = "获取初始日志失败", error = ex.Message }); + await Response.Body.FlushAsync(cancellationToken); + } + + while (!cancellationToken.IsCancellationRequested) + { + try + { + var currentLogs = _webSocketManager.GetLogCache()?.ToList() ?? new List(); + var currentLogHash = CalculateLogsHash(currentLogs); + + bool hasNewLogs = false; + List newLogs = new List(); + + if (currentLogs.Count > lastLogCount) + { + newLogs = currentLogs.Skip(lastLogCount).ToList(); + hasNewLogs = newLogs.Any(); + } + else if (currentLogs.Count == lastLogCount && currentLogHash != lastLogHash) + { + newLogs = GetChangedLogs(lastLogs, currentLogs); + hasNewLogs = newLogs.Any(); + } + else if (currentLogs.Count < lastLogCount) + { + _logger.LogInformation("检测到日志缓存被重置,重新同步"); + await SendSseEvent("reset", new { + message = "日志缓存已重置", + totalCount = currentLogs.Count + }); + await Response.Body.FlushAsync(cancellationToken); + + lastLogCount = currentLogs.Count; + lastLogs = currentLogs.ToList(); + lastLogHash = currentLogHash; + continue; + } + + if (hasNewLogs && newLogs.Any()) + { + _logger.LogInformation("StreamLogs: 发送新日志事件,新增日志数量: {NewCount}, 总日志数量: {TotalCount}", newLogs.Count, currentLogs.Count); + + var eventData = new { + logs = newLogs, + totalCount = currentLogs.Count, + newCount = newLogs.Count + }; + + await SendSseEvent("new_logs", eventData); + await Response.Body.FlushAsync(cancellationToken); + + lastLogCount = currentLogs.Count; + lastLogs = currentLogs.ToList(); + lastLogHash = currentLogHash; + } + + await Task.Delay(250, cancellationToken); + } + catch (OperationCanceledException) + { + break; + } + catch (Exception ex) + { + _logger.LogError(ex, "StreamLogs 循环中发生错误"); + await SendSseEvent("error", new { + message = "处理日志流时发生错误", + error = ex.Message, + timestamp = DateTime.UtcNow + }); + await Response.Body.FlushAsync(cancellationToken); + + await Task.Delay(1000, cancellationToken); + } + } + + await SendSseEvent("disconnected", new { + message = "日志流连接已断开", + timestamp = DateTime.UtcNow + }); + await Response.Body.FlushAsync(cancellationToken); + } + catch (OperationCanceledException) + { + _logger.LogInformation("StreamLogs 连接被客户端取消"); + } + catch (Exception ex) + { + _logger.LogError(ex, "StreamLogs 方法执行时发生未处理的异常"); + try + { + await SendSseEvent("fatal_error", new { + message = "服务器内部错误", + error = ex.Message, + timestamp = DateTime.UtcNow + }); + await Response.Body.FlushAsync(); + } + catch + { + // 忽略发送错误事件时的异常 + } + } + } + + /// + /// 获取日志缓存详细状态(调试用) + /// + /// 日志缓存详细状态 + [HttpGet("debug")] + public ActionResult GetLogCacheDebugInfo() + { + try + { + var logs = _webSocketManager.GetLogCache()?.ToList() ?? new List(); + var logCount = _webSocketManager.GetLogCacheCount(); + var cacheStatus = _webSocketManager.GetLogCacheStatus(); + + var debugInfo = new + { + totalLogs = logCount, + actualLogsCount = logs.Count, + cacheSize = 10000, + timestamp = DateTime.UtcNow, + sampleLogs = logs.TakeLast(5).Select(log => new + { + timestamp = log.Timestamp, + layer = log.Layer, + message = log.Message?.Substring(0, Math.Min(100, log.Message?.Length ?? 0)) + "..." + }).ToList(), + logHash = CalculateLogsHash(logs), + cacheStatus = cacheStatus + }; + + return Ok(debugInfo); + } + catch (Exception ex) + { + _logger.LogError(ex, "获取日志缓存调试信息时发生错误"); + return StatusCode(500, new { message = "获取调试信息失败", error = ex.Message }); + } + } + + /// + /// 获取SSE连接状态(调试用) + /// + /// 连接状态信息 + [HttpGet("connection-status")] + public ActionResult GetSseConnectionStatus() + { + try + { + var status = new + { + timestamp = DateTime.UtcNow, + requestHeaders = Request.Headers.ToDictionary(h => h.Key, h => h.Value.ToString()), + userAgent = Request.Headers["User-Agent"].ToString(), + accept = Request.Headers["Accept"].ToString(), + cacheControl = Request.Headers["Cache-Control"].ToString(), + connection = Request.Headers["Connection"].ToString(), + isHttps = Request.IsHttps, + host = Request.Host.ToString(), + path = Request.Path.ToString(), + queryString = Request.QueryString.ToString(), + method = Request.Method, + contentType = Request.ContentType, + contentLength = Request.ContentLength + }; + + return Ok(status); + } + catch (Exception ex) + { + _logger.LogError(ex, "获取SSE连接状态时发生错误"); + return StatusCode(500, new { message = "获取连接状态失败", error = ex.Message }); + } + } + + /// + /// 测试SSE连接(调试用) + /// + /// 测试结果 + [HttpGet("test-connection")] + public ActionResult TestSseConnection() + { + try + { + var testResult = new + { + message = "SSE连接测试成功", + timestamp = DateTime.UtcNow, + serverTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"), + logCacheCount = _webSocketManager.GetLogCacheCount(), + testData = new { test = true, message = "这是一个测试消息" } + }; + + return Ok(testResult); + } + catch (Exception ex) + { + _logger.LogError(ex, "测试SSE连接时发生错误"); + return StatusCode(500, new { message = "测试连接失败", error = ex.Message }); + } + } + + /// + /// 强制推送测试日志(调试用) + /// + /// 操作结果 + [HttpPost("force-push-test")] + public ActionResult ForcePushTestLogs() + { + try + { + var testLogs = new List(); + _webSocketManager.AddLogsToCache(testLogs); + + _logger.LogInformation("强制推送测试日志: {Message}", testLogs[0].Message); + + return Ok(new { + message = $"已强制推送测试日志: {testLogs[0].Message}", + timestamp = testLogs[0].Timestamp + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "强制推送测试日志时发生错误"); + return StatusCode(500, new { message = "强制推送失败", error = ex.Message }); + } + } + + private async Task SendSseEvent(string eventName, object data) + { + try + { + if (string.IsNullOrEmpty(eventName)) + { + _logger.LogWarning("尝试发送空事件名称的SSE事件"); + return; + } + + if (data == null) + { + _logger.LogWarning("尝试发送空数据的SSE事件: {EventName}", eventName); + return; + } + + var json = Newtonsoft.Json.JsonConvert.SerializeObject(data); + var eventData = $"event: {eventName}\ndata: {json}\n\n"; + + await Response.WriteAsync(eventData); + } + catch (Exception ex) + { + _logger.LogError(ex, "发送SSE事件时发生错误: {EventName}", eventName); + } + } + + /// + /// 计算日志列表的哈希值,用于检测内容变化 + /// + /// 日志列表 + /// 哈希值字符串 + private string CalculateLogsHash(List logs) + { + if (logs == null || !logs.Any()) + return string.Empty; + + try + { + var hashInput = string.Join("|", logs.Select(log => + $"{log.Timestamp}_{log.Layer}_{log.Message}")); + + using (var sha256 = System.Security.Cryptography.SHA256.Create()) + { + var bytes = System.Text.Encoding.UTF8.GetBytes(hashInput); + var hash = sha256.ComputeHash(bytes); + return Convert.ToBase64String(hash); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "计算日志哈希值时发生错误"); + return string.Empty; + } + } + + /// + /// 获取变化的日志项 + /// + /// 旧日志列表 + /// 新日志列表 + /// 变化的日志列表 + private List GetChangedLogs(List oldLogs, List newLogs) + { + var changedLogs = new List(); + + if (oldLogs.Count != newLogs.Count) + { + return newLogs; + } + + for (int i = 0; i < newLogs.Count; i++) + { + if (i < oldLogs.Count) + { + var oldLog = oldLogs[i]; + var newLog = newLogs[i]; + + if (oldLog.Timestamp != newLog.Timestamp || + oldLog.Layer != newLog.Layer || + oldLog.Message != newLog.Message) + { + changedLogs.Add(newLog); + } + } + else + { + changedLogs.Add(newLogs[i]); + } + } + + return changedLogs; + } + } +} \ No newline at end of file diff --git a/LTEMvcApp/Controllers/MessageController.cs b/LTEMvcApp/Controllers/MessageController.cs new file mode 100644 index 0000000..7243724 --- /dev/null +++ b/LTEMvcApp/Controllers/MessageController.cs @@ -0,0 +1,368 @@ +using Microsoft.AspNetCore.Mvc; +using LTEMvcApp.Models; +using LTEMvcApp.Services; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json.Linq; +using System.Threading.Tasks; +using System.Threading; +using System.IO; +using System.Text; + +namespace LTEMvcApp.Controllers +{ + /// + /// 客户端消息管理控制器 + /// + [ApiController] + [Route("api/[controller]")] + public class MessageController : ControllerBase + { + private readonly WebSocketManagerService _webSocketManager; + private readonly ILogger _logger; + private readonly string _logsDirectory = "ClientMessageLogs"; + + public MessageController(WebSocketManagerService webSocketManager, ILogger logger) + { + _webSocketManager = webSocketManager; + _logger = logger; + if (!Directory.Exists(_logsDirectory)) + { + Directory.CreateDirectory(_logsDirectory); + } + } + + /// + /// SSE推送客户端消息流 + /// + [HttpGet("{clientName}/stream")] + public async Task StreamClientMessages(string clientName) + { + try + { + Response.ContentType = "text/event-stream"; + Response.Headers.Append("Cache-Control", "no-cache"); + Response.Headers.Append("Connection", "keep-alive"); + Response.Headers.Append("Access-Control-Allow-Origin", "*"); + + var client = _webSocketManager.GetClientInstance(clientName); + if (client == null) + { + await SendSseEvent("error", new { message = "客户端未连接或不存在", clientName }); + return; + } + + await SendSseEvent("open", new { + message = "成功连接到服务器事件流", + clientName, + timestamp = DateTime.UtcNow + }); + await Response.Body.FlushAsync(HttpContext.RequestAborted); + + int lastSentCount = 0; + int lastReceivedCount = 0; + var cancellationToken = HttpContext.RequestAborted; + + var sentLogFilePath = Path.Combine(_logsDirectory, $"{clientName}_sent_messages.log"); + var receivedLogFilePath = Path.Combine(_logsDirectory, $"{clientName}_received_messages.log"); + + while (!cancellationToken.IsCancellationRequested) + { + try + { + bool hasNewMessages = false; + var currentSentCount = client.SentMessagesCount; + if (currentSentCount > lastSentCount) + { + var sentMessages = client.SentMessages?.ToList() ?? new List(); + if (sentMessages.Count > lastSentCount) + { + var newMessages = sentMessages.Skip(lastSentCount).ToList(); + if (newMessages.Any()) + { + await LogMessagesToFile(sentLogFilePath, newMessages, "SENT", clientName); + await SendSseEvent("update", new { + type = "sent", + messages = newMessages, + totalCount = currentSentCount, + newCount = newMessages.Count + }); + lastSentCount = currentSentCount; + hasNewMessages = true; + } + } + } + var currentReceivedCount = client.ReceivedMessagesCount; + if (currentReceivedCount > lastReceivedCount) + { + var receivedMessages = client.ReceivedMessages?.ToList() ?? new List(); + if (receivedMessages.Count > lastReceivedCount) + { + var newMessages = receivedMessages.Skip(lastReceivedCount).ToList(); + if (newMessages.Any()) + { + await LogMessagesToFile(receivedLogFilePath, newMessages, "RECEIVED", clientName); + await SendSseEvent("update", new { + type = "received", + messages = newMessages, + totalCount = currentReceivedCount, + newCount = newMessages.Count + }); + lastReceivedCount = currentReceivedCount; + hasNewMessages = true; + } + } + } + if (hasNewMessages) + { + await Response.Body.FlushAsync(cancellationToken); + } + await Task.Delay(250, cancellationToken); + } + catch (OperationCanceledException) + { + break; + } + catch (Exception ex) + { + _logger.LogError(ex, "StreamClientMessages 循环中发生错误,客户端: {ClientName}", clientName); + await SendSseEvent("error", new { + message = "处理消息流时发生错误", + error = ex.Message, + clientName, + timestamp = DateTime.UtcNow + }); + await Response.Body.FlushAsync(cancellationToken); + await Task.Delay(1000, cancellationToken); + } + } + await SendSseEvent("disconnected", new { + message = "客户端消息流连接已断开", + clientName, + timestamp = DateTime.UtcNow + }); + await Response.Body.FlushAsync(cancellationToken); + } + catch (OperationCanceledException) + { + _logger.LogInformation("StreamClientMessages 连接被客户端取消,客户端: {ClientName}", clientName); + } + catch (Exception ex) + { + _logger.LogError(ex, "StreamClientMessages 方法执行时发生未处理的异常,客户端: {ClientName}", clientName); + try + { + await SendSseEvent("fatal_error", new { + message = "服务器内部错误", + error = ex.Message, + clientName, + timestamp = DateTime.UtcNow + }); + await Response.Body.FlushAsync(); + } + catch { } + } + } + + /// + /// 发送消息到客户端 + /// + [HttpPost("{clientName}/send")] + public ActionResult SendMessage(string clientName, [FromBody] JObject message) + { + var messageId = _webSocketManager.SendMessageToClient(clientName, message); + if (messageId >= 0) + return Ok(new { messageId, message = $"消息已发送到客户端 '{clientName}'" }); + else + return BadRequest($"发送消息到客户端 '{clientName}' 失败"); + } + + /// + /// 获取客户端消息日志文件列表 + /// + [HttpGet("logs")] + public ActionResult GetClientMessageLogFiles() + { + try + { + if (!Directory.Exists(_logsDirectory)) + { + return Ok(new { files = new List(), message = "日志目录不存在" }); + } + var logFiles = Directory.GetFiles(_logsDirectory, "*.log") + .Select(filePath => new + { + fileName = Path.GetFileName(filePath), + filePath = filePath, + size = new FileInfo(filePath).Length, + lastModified = System.IO.File.GetLastWriteTime(filePath), + clientName = Path.GetFileNameWithoutExtension(filePath).Replace("_sent_messages", "").Replace("_received_messages", ""), + type = filePath.Contains("_sent_messages") ? "发送消息" : "接收消息" + }) + .OrderByDescending(f => f.lastModified) + .ToList(); + return Ok(new { files = logFiles, totalCount = logFiles.Count }); + } + catch (Exception ex) + { + _logger.LogError(ex, "获取客户端消息日志文件列表时发生错误"); + return StatusCode(500, new { message = "获取日志文件列表失败", error = ex.Message }); + } + } + + /// + /// 获取客户端消息日志文件内容 + /// + [HttpGet("logs/{fileName}")] + public ActionResult GetClientMessageLogContent(string fileName, [FromQuery] int lines = 100) + { + try + { + var filePath = Path.Combine(_logsDirectory, fileName); + if (!System.IO.File.Exists(filePath)) + { + return NotFound(new { message = $"日志文件 '{fileName}' 不存在" }); + } + var fileInfo = new System.IO.FileInfo(filePath); + var allLines = System.IO.File.ReadAllLines(filePath, Encoding.UTF8); + var lastLines = allLines.TakeLast(lines).ToList(); + return Ok(new + { + fileName = fileName, + filePath = filePath, + totalLines = allLines.Length, + returnedLines = lastLines.Count, + fileSize = fileInfo.Length, + lastModified = fileInfo.LastWriteTime, + content = lastLines + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "获取客户端消息日志文件内容时发生错误: {FileName}", fileName); + return StatusCode(500, new { message = "获取日志文件内容失败", error = ex.Message }); + } + } + + /// + /// 清空客户端消息日志文件 + /// + [HttpDelete("logs")] + public ActionResult ClearClientMessageLogs([FromQuery] string? fileName = null) + { + try + { + if (!Directory.Exists(_logsDirectory)) + { + return Ok(new { message = "日志目录不存在,无需清空" }); + } + int clearedCount = 0; + if (string.IsNullOrEmpty(fileName)) + { + var logFiles = Directory.GetFiles(_logsDirectory, "*.log"); + foreach (var filePath in logFiles) + { + System.IO.File.WriteAllText(filePath, string.Empty); + clearedCount++; + } + _logger.LogInformation("已清空 {Count} 个客户端消息日志文件", clearedCount); + return Ok(new { message = $"已清空 {clearedCount} 个客户端消息日志文件" }); + } + else + { + var filePath = Path.Combine(_logsDirectory, fileName); + if (!System.IO.File.Exists(filePath)) + { + return NotFound(new { message = $"日志文件 '{fileName}' 不存在" }); + } + System.IO.File.WriteAllText(filePath, string.Empty); + _logger.LogInformation("已清空客户端消息日志文件: {FileName}", fileName); + return Ok(new { message = $"已清空客户端消息日志文件 '{fileName}'" }); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "清空客户端消息日志文件时发生错误"); + return StatusCode(500, new { message = "清空日志文件失败", error = ex.Message }); + } + } + + /// + /// 删除客户端消息日志文件 + /// + [HttpDelete("logs/delete")] + public ActionResult DeleteClientMessageLogs([FromQuery] string? fileName = null) + { + try + { + if (!Directory.Exists(_logsDirectory)) + { + return Ok(new { message = "日志目录不存在,无需删除" }); + } + int deletedCount = 0; + if (string.IsNullOrEmpty(fileName)) + { + var logFiles = Directory.GetFiles(_logsDirectory, "*.log"); + foreach (var filePath in logFiles) + { + System.IO.File.Delete(filePath); + deletedCount++; + } + _logger.LogInformation("已删除 {Count} 个客户端消息日志文件", deletedCount); + return Ok(new { message = $"已删除 {deletedCount} 个客户端消息日志文件" }); + } + else + { + var filePath = Path.Combine(_logsDirectory, fileName); + if (!System.IO.File.Exists(filePath)) + { + return NotFound(new { message = $"日志文件 '{fileName}' 不存在" }); + } + System.IO.File.Delete(filePath); + _logger.LogInformation("已删除客户端消息日志文件: {FileName}", fileName); + return Ok(new { message = $"已删除客户端消息日志文件 '{fileName}'" }); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "删除客户端消息日志文件时发生错误"); + return StatusCode(500, new { message = "删除日志文件失败", error = ex.Message }); + } + } + + private async Task LogMessagesToFile(string logFilePath, List messages, string messageType, string clientName) + { + try + { + var logBuilder = new StringBuilder(); + var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"); + foreach (var message in messages) + { + logBuilder.AppendLine($"[{timestamp}] [{clientName}] [{messageType}] {message}"); + logBuilder.AppendLine(new string('-', 80)); + } + await System.IO.File.AppendAllTextAsync(logFilePath, logBuilder.ToString(), Encoding.UTF8); + _logger.LogDebug("已记录 {Count} 条 {MessageType} 消息到文件: {FilePath}", messages.Count, messageType, logFilePath); + } + catch (Exception ex) + { + _logger.LogError(ex, "记录消息到文件时发生错误: {FilePath}", logFilePath); + } + } + + private async Task SendSseEvent(string eventName, object data) + { + try + { + if (string.IsNullOrEmpty(eventName) || data == null) + return; + var json = Newtonsoft.Json.JsonConvert.SerializeObject(data); + var eventData = $"event: {eventName}\ndata: {json}\n\n"; + await Response.WriteAsync(eventData); + } + catch (Exception ex) + { + _logger.LogError(ex, "发送SSE事件时发生错误: {EventName}", eventName); + } + } + } +} \ No newline at end of file diff --git a/LTEMvcApp/Controllers/TestConfigController.cs b/LTEMvcApp/Controllers/TestConfigController.cs new file mode 100644 index 0000000..83ffb9a --- /dev/null +++ b/LTEMvcApp/Controllers/TestConfigController.cs @@ -0,0 +1,175 @@ +using Microsoft.AspNetCore.Mvc; +using LTEMvcApp.Models; +using LTEMvcApp.Services; +using Microsoft.Extensions.Logging; + +namespace LTEMvcApp.Controllers +{ + /// + /// 测试配置管理控制器 - 负责测试客户端配置管理 + /// + [ApiController] + [Route("api/[controller]")] + public class TestConfigController : ControllerBase + { + private readonly WebSocketManagerService _webSocketManager; + private readonly ILogger _logger; + + public TestConfigController(WebSocketManagerService webSocketManager, ILogger logger) + { + _webSocketManager = webSocketManager; + _logger = logger; + } + + /// + /// 获取默认测试客户端配置 + /// + /// 测试客户端配置 + [HttpGet("default")] + public ActionResult GetDefaultTestClientConfig() + { + var testConfig = _webSocketManager.GetDefaultTestClientConfig(); + return Ok(testConfig); + } + + /// + /// 设置测试客户端配置 + /// + /// 测试客户端配置 + /// 操作结果 + [HttpPost("default")] + public ActionResult SetTestClientConfig([FromBody] ClientConfig config) + { + if (string.IsNullOrEmpty(config.Name)) + return BadRequest("客户端名称不能为空"); + + var success = _webSocketManager.SetTestClientConfig(config); + if (success) + return Ok(new { message = "测试客户端配置已更新" }); + else + return BadRequest("更新测试客户端配置失败"); + } + + /// + /// 获取所有测试客户端配置 + /// + /// 测试客户端配置列表 + [HttpGet] + public ActionResult> GetAllTestClientConfigs() + { + var configs = _webSocketManager.GetAllTestClientConfigs(); + return Ok(configs); + } + + /// + /// 根据地址获取测试客户端配置 + /// + /// 服务器地址 + /// 测试客户端配置 + [HttpGet("address/{address}")] + public ActionResult GetTestClientConfigByAddress(string address) + { + var config = _webSocketManager.GetTestClientConfigByAddress(address); + if (config == null) + return NotFound($"测试客户端配置 (地址: {address}) 不存在"); + + return Ok(config); + } + + /// + /// 删除测试客户端配置 + /// + /// 服务器地址 + /// 操作结果 + [HttpDelete("address/{address}")] + public ActionResult RemoveTestClientConfig(string address) + { + var success = _webSocketManager.RemoveTestClientConfig(address); + if (success) + return Ok(new { message = $"测试客户端配置 (地址: {address}) 已删除" }); + else + return NotFound($"测试客户端配置 (地址: {address}) 不存在"); + } + + /// + /// 启动测试客户端 + /// + /// 启动请求 + /// 操作结果 + [HttpPost("start")] + public ActionResult StartTestClient([FromBody] StartStopRequest? request = null) + { + string? address = request?.Address; + + if (string.IsNullOrEmpty(address)) + { + // 使用默认配置启动 + var success = _webSocketManager.StartTestClient(); + if (success) + return Ok(new { message = "测试客户端已启动" }); + else + return BadRequest("启动测试客户端失败"); + } + else + { + // 根据地址查找配置并启动 + var config = _webSocketManager.GetTestClientConfigByAddress(address); + if (config == null) + return NotFound($"未找到地址为 {address} 的测试客户端配置"); + + var success = _webSocketManager.StartClient(config.Name); + if (success) + return Ok(new { message = $"测试客户端 {config.Name} 已启动" }); + else + return BadRequest($"启动测试客户端 {config.Name} 失败"); + } + } + + /// + /// 停止测试客户端 + /// + /// 停止请求 + /// 操作结果 + [HttpPost("stop")] + public ActionResult StopTestClient([FromBody] StartStopRequest? request = null) + { + string? address = request?.Address; + + if (string.IsNullOrEmpty(address)) + { + // 使用默认配置停止 + _logger.LogInformation("API 请求: 停止测试客户端"); + var success = _webSocketManager.StopTestClient(); + if (success) + return Ok(new { message = "测试客户端停止成功" }); + else + return BadRequest("停止测试客户端失败"); + } + else + { + // 根据地址查找配置并停止 + var config = _webSocketManager.GetTestClientConfigByAddress(address); + if (config == null) + return NotFound($"未找到地址为 {address} 的测试客户端配置"); + + _logger.LogInformation($"API 请求: 停止测试客户端 {config.Name}"); + var success = _webSocketManager.StopClient(config.Name); + if (success) + return Ok(new { message = $"测试客户端 {config.Name} 停止成功" }); + else + return BadRequest($"停止测试客户端 {config.Name} 失败"); + } + } + } + + /// + /// 启动/停止请求 + /// + public class StartStopRequest + { + /// + /// 服务器地址 + /// + public string? Address { get; set; } + } +} \ No newline at end of file diff --git a/LTEMvcApp/Controllers/WebSocketController.cs b/LTEMvcApp/Controllers/WebSocketController.cs index 399b715..63bfdeb 100644 --- a/LTEMvcApp/Controllers/WebSocketController.cs +++ b/LTEMvcApp/Controllers/WebSocketController.cs @@ -1,18 +1,13 @@ using Microsoft.AspNetCore.Mvc; using LTEMvcApp.Models; using LTEMvcApp.Services; -using Newtonsoft.Json.Linq; using Microsoft.Extensions.Logging; -using System.Threading.Tasks; -using System.Linq; -using System.Threading; using System.IO; -using System.Text; namespace LTEMvcApp.Controllers { /// - /// WebSocket控制器 - 提供LTE客户端WebSocket管理API + /// WebSocket控制器(已拆分,原方法已迁移至更专注的控制器) /// [ApiController] [Route("api/[controller]")] @@ -26,1167 +21,11 @@ namespace LTEMvcApp.Controllers { _webSocketManager = webSocketManager; _logger = logger; - - // 确保日志目录存在 if (!Directory.Exists(_logsDirectory)) { Directory.CreateDirectory(_logsDirectory); } } - - /// - /// 获取所有客户端状态 - /// - /// 客户端状态列表 - [HttpGet("clients")] - public ActionResult> GetClientStates() - { - _logger.LogInformation("获取所有客户端状态"); - var states = _webSocketManager.GetAllClientStates(); - return Ok(states); - } - - /// - /// 获取客户端配置 - /// - /// 客户端名称 - /// 客户端配置 - [HttpGet("clients/{clientName}/config")] - public ActionResult GetClientConfig(string clientName) - { - var config = _webSocketManager.GetClientConfig(clientName); - if (config == null) - return NotFound($"客户端 '{clientName}' 不存在"); - - return Ok(config); - } - - /// - /// 获取所有客户端配置 - /// - /// 客户端配置列表 - [HttpGet("configs")] - public ActionResult> GetAllConfigs() - { - var configs = _webSocketManager.GetAllClientConfigs(); - return Ok(configs); - } - - /// - /// 添加客户端配置 - /// - /// 客户端配置 - /// 操作结果 - [HttpPost("configs")] - public ActionResult AddClientConfig([FromBody] ClientConfig config) - { - if (string.IsNullOrEmpty(config.Name)) - return BadRequest("客户端名称不能为空"); - - var success = _webSocketManager.AddClientConfig(config); - if (success) - return Ok(new { message = $"客户端 '{config.Name}' 配置已添加" }); - else - return BadRequest("添加客户端配置失败"); - } - - /// - /// 启动客户端 - /// - /// 客户端名称 - /// 操作结果 - [HttpPost("clients/{clientName}/start")] - public ActionResult StartClient(string clientName) - { - _logger.LogInformation($"API请求: 启动客户端 {clientName}"); - var success = _webSocketManager.StartClient(clientName); - if (success) - { - _logger.LogInformation($"客户端 {clientName} 启动成功"); - return Ok(new { message = $"客户端 '{clientName}' 已启动" }); - } - else - { - _logger.LogWarning($"客户端 {clientName} 启动失败"); - return BadRequest($"启动客户端 '{clientName}' 失败"); - } - } - - /// - /// 停止客户端 - /// - /// 客户端名称 - /// 操作结果 - [HttpPost("clients/{clientName}/stop")] - public ActionResult StopClient(string clientName) - { - var success = _webSocketManager.StopClient(clientName); - if (success) - return Ok(new { message = $"客户端 '{clientName}' 已停止" }); - else - return BadRequest($"停止客户端 '{clientName}' 失败"); - } - - /// - /// 播放/暂停客户端 - /// - /// 客户端名称 - /// 操作结果 - [HttpPost("clients/{clientName}/playpause")] - public ActionResult PlayPauseClient(string clientName) - { - var success = _webSocketManager.PlayPauseClient(clientName); - if (success) - return Ok(new { message = $"客户端 '{clientName}' 播放/暂停状态已切换" }); - else - return BadRequest($"切换客户端 '{clientName}' 播放/暂停状态失败"); - } - - /// - /// 重置客户端日志 - /// - /// 客户端名称 - /// 操作结果 - [HttpPost("clients/{clientName}/reset-logs")] - public ActionResult ResetClientLogs(string clientName) - { - var success = _webSocketManager.ResetClientLogs(clientName); - if (success) - return Ok(new { message = $"客户端 '{clientName}' 日志已重置" }); - else - return BadRequest($"重置客户端 '{clientName}' 日志失败"); - } - - /// - /// 获取客户端日志 - /// - /// 客户端名称 - /// 日志数量限制 - /// 日志列表 - [HttpGet("clients/{clientName}/logs")] - public ActionResult?> GetClientLogs(string clientName, [FromQuery] int limit = 100) - { - var logs = _webSocketManager.GetClientLogs(clientName); - if (logs == null) - return NotFound($"客户端 '{clientName}' 不存在或未连接"); - - // 限制返回的日志数量 - var limitedLogs = logs.TakeLast(limit).ToList(); - return Ok(limitedLogs); - } - - /// - /// 设置客户端日志配置 - /// - /// 客户端名称 - /// 请求体 - [HttpPost("clients/{clientName}/logs-config")] - public ActionResult SetClientLogsConfig(string clientName, [FromBody] ClientLogsConfig request) - { - var success = _webSocketManager.SetClientLogsConfig(clientName, request); - if (success) - { - return Ok(new { message = "日志配置已更新" }); - } - else - { - return NotFound(new { message = $"客户端 '{clientName}' 未找到或更新失败" }); - } - } - - /// - /// 发送消息到客户端 - /// - /// 客户端名称 - /// 消息内容 - /// 操作结果 - [HttpPost("clients/{clientName}/send-message")] - public ActionResult SendMessage(string clientName, [FromBody] JObject message) - { - var messageId = _webSocketManager.SendMessageToClient(clientName, message); - if (messageId >= 0) - return Ok(new { messageId, message = $"消息已发送到客户端 '{clientName}'" }); - else - return BadRequest($"发送消息到客户端 '{clientName}' 失败"); - } - - /// - /// 获取连接统计信息 - /// - /// 统计信息 - [HttpGet("statistics")] - public ActionResult GetStatistics() - { - var stats = _webSocketManager.GetConnectionStatistics(); - return Ok(stats); - } - - /// - /// 启动所有已配置的客户端 - /// - /// 操作结果 - [HttpPost("start-all")] - public ActionResult StartAllClients() - { - _webSocketManager.StartAllConfiguredClients(); - return Ok(new { message = "所有已配置的客户端已启动" }); - } - - /// - /// 停止所有客户端 - /// - /// 操作结果 - [HttpPost("stop-all")] - public ActionResult StopAllClients() - { - _webSocketManager.StopAllClients(); - return Ok(new { message = "所有客户端已停止" }); - } - - /// - /// 移除客户端配置 - /// - /// 客户端名称 - /// 操作结果 - [HttpDelete("configs/{clientName}")] - public ActionResult RemoveClientConfig(string clientName) - { - var success = _webSocketManager.RemoveClientConfig(clientName); - if (success) - return Ok(new { message = $"客户端 '{clientName}' 配置已移除" }); - else - return BadRequest($"移除客户端 '{clientName}' 配置失败"); - } - - /// - /// 使用 Server-Sent Events (SSE) 实时推送客户端消息 - /// - /// 客户端名称 - [HttpGet("clients/{clientName}/messages/stream")] - public async Task StreamClientMessages(string clientName) - { - try - { - Response.ContentType = "text/event-stream"; - Response.Headers.Append("Cache-Control", "no-cache"); - Response.Headers.Append("Connection", "keep-alive"); - Response.Headers.Append("Access-Control-Allow-Origin", "*"); - - var client = _webSocketManager.GetClientInstance(clientName); - if (client == null) - { - // 发送一个错误事件然后关闭 - await SendSseEvent("error", new { message = "客户端未连接或不存在", clientName }); - return; - } - - // 发送一个连接成功事件 - await SendSseEvent("open", new { - message = "成功连接到服务器事件流", - clientName, - timestamp = DateTime.UtcNow - }); - await Response.Body.FlushAsync(HttpContext.RequestAborted); - - int lastSentCount = 0; - int lastReceivedCount = 0; - var cancellationToken = HttpContext.RequestAborted; - - // 创建日志文件路径 - var sentLogFilePath = Path.Combine(_logsDirectory, $"{clientName}_sent_messages.log"); - var receivedLogFilePath = Path.Combine(_logsDirectory, $"{clientName}_received_messages.log"); - - while (!cancellationToken.IsCancellationRequested) - { - try - { - bool hasNewMessages = false; - - // 检查并高效地发送新的"已发送"消息 - var currentSentCount = client.SentMessagesCount; - if (currentSentCount > lastSentCount) - { - var sentMessages = client.SentMessages?.ToList() ?? new List(); - if (sentMessages.Count > lastSentCount) - { - var newMessages = sentMessages.Skip(lastSentCount).ToList(); - if (newMessages.Any()) - { - // 记录发送的消息到日志文件 - await LogMessagesToFile(sentLogFilePath, newMessages, "SENT", clientName); - - await SendSseEvent("update", new { - type = "sent", - messages = newMessages, - totalCount = currentSentCount, - newCount = newMessages.Count - }); - lastSentCount = currentSentCount; - hasNewMessages = true; - } - } - } - - // 检查并高效地发送新的"已接收"消息 - var currentReceivedCount = client.ReceivedMessagesCount; - if (currentReceivedCount > lastReceivedCount) - { - var receivedMessages = client.ReceivedMessages?.ToList() ?? new List(); - if (receivedMessages.Count > lastReceivedCount) - { - var newMessages = receivedMessages.Skip(lastReceivedCount).ToList(); - if (newMessages.Any()) - { - // 记录接收的消息到日志文件 - await LogMessagesToFile(receivedLogFilePath, newMessages, "RECEIVED", clientName); - - await SendSseEvent("update", new { - type = "received", - messages = newMessages, - totalCount = currentReceivedCount, - newCount = newMessages.Count - }); - lastReceivedCount = currentReceivedCount; - hasNewMessages = true; - } - } - } - - if (hasNewMessages) - { - await Response.Body.FlushAsync(cancellationToken); - } - - await Task.Delay(250, cancellationToken); // 每250毫秒检查一次新消息 - } - catch (OperationCanceledException) - { - // 正常的取消操作,退出循环 - break; - } - catch (Exception ex) - { - _logger.LogError(ex, "StreamClientMessages 循环中发生错误,客户端: {ClientName}", clientName); - await SendSseEvent("error", new { - message = "处理消息流时发生错误", - error = ex.Message, - clientName, - timestamp = DateTime.UtcNow - }); - await Response.Body.FlushAsync(cancellationToken); - - // 等待一段时间后继续,避免频繁错误 - await Task.Delay(1000, cancellationToken); - } - } - - // 发送断开连接事件 - await SendSseEvent("disconnected", new { - message = "客户端消息流连接已断开", - clientName, - timestamp = DateTime.UtcNow - }); - await Response.Body.FlushAsync(cancellationToken); - } - catch (OperationCanceledException) - { - _logger.LogInformation("StreamClientMessages 连接被客户端取消,客户端: {ClientName}", clientName); - } - catch (Exception ex) - { - _logger.LogError(ex, "StreamClientMessages 方法执行时发生未处理的异常,客户端: {ClientName}", clientName); - try - { - await SendSseEvent("fatal_error", new { - message = "服务器内部错误", - error = ex.Message, - clientName, - timestamp = DateTime.UtcNow - }); - await Response.Body.FlushAsync(); - } - catch - { - // 忽略发送错误事件时的异常 - } - } - } - - /// - /// 将消息记录到日志文件 - /// - /// 日志文件路径 - /// 消息列表 - /// 消息类型(SENT/RECEIVED) - /// 客户端名称 - private async Task LogMessagesToFile(string logFilePath, List messages, string messageType, string clientName) - { - try - { - var logBuilder = new StringBuilder(); - var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"); - - foreach (var message in messages) - { - logBuilder.AppendLine($"[{timestamp}] [{clientName}] [{messageType}] {message}"); - logBuilder.AppendLine(new string('-', 80)); // 分隔线 - } - - await System.IO.File.AppendAllTextAsync(logFilePath, logBuilder.ToString(), Encoding.UTF8); - _logger.LogDebug("已记录 {Count} 条 {MessageType} 消息到文件: {FilePath}", messages.Count, messageType, logFilePath); - } - catch (Exception ex) - { - _logger.LogError(ex, "记录消息到文件时发生错误: {FilePath}", logFilePath); - } - } - - private async Task SendSseEvent(string eventName, object data) - { - try - { - if (string.IsNullOrEmpty(eventName)) - { - _logger.LogWarning("尝试发送空事件名称的SSE事件"); - return; - } - - if (data == null) - { - _logger.LogWarning("尝试发送空数据的SSE事件: {EventName}", eventName); - return; - } - - var json = Newtonsoft.Json.JsonConvert.SerializeObject(data); - var eventData = $"event: {eventName}\ndata: {json}\n\n"; - - _logger.LogDebug("SendSseEvent: 发送事件 {EventName}, 数据长度: {DataLength}, 事件数据长度: {EventDataLength}", - eventName, json.Length, eventData.Length); - - await Response.WriteAsync(eventData); - - _logger.LogDebug("SendSseEvent: 事件 {EventName} 发送成功", eventName); - } - catch (Exception ex) - { - _logger.LogError(ex, "发送SSE事件时发生错误: {EventName}", eventName); - // 不重新抛出异常,避免影响整个流 - } - } - - /// - /// 获取测试客户端配置 - /// - /// 测试客户端配置 - [HttpGet("test-client-config")] - public ActionResult GetTestClientConfig() - { - var testConfig = _webSocketManager.GetTestClientConfig(); - return Ok(testConfig); - } - - /// - /// 设置测试客户端配置 - /// - /// 测试客户端配置 - /// 操作结果 - [HttpPost("test-client-config")] - public ActionResult SetTestClientConfig([FromBody] ClientConfig config) - { - if (string.IsNullOrEmpty(config.Name)) - return BadRequest("客户端名称不能为空"); - - var success = _webSocketManager.SetTestClientConfig(config); - if (success) - return Ok(new { message = "测试客户端配置已更新" }); - else - return BadRequest("更新测试客户端配置失败"); - } - - /// - /// 启动测试客户端 - /// - /// 操作结果 - [HttpPost("test-client/start")] - public ActionResult StartTestClient() - { - var success = _webSocketManager.StartTestClient(); - if (success) - return Ok(new { message = "测试客户端已启动" }); - else - return BadRequest("启动测试客户端失败"); - } - - /// - /// 停止测试客户端 - /// - /// 操作结果 - [HttpPost("test-client/stop")] - public ActionResult StopTestClient() - { - _logger.LogInformation("API 请求: 停止测试客户端"); - var success = _webSocketManager.StopTestClient(); - if (success) - return Ok(new { message = "测试客户端停止成功" }); - else - return BadRequest("停止测试客户端失败"); - } - - /// - /// 获取日志缓存统计信息 - /// - /// 统计信息 - [HttpGet("logs/stats")] - public ActionResult GetLogCacheStats() - { - try - { - var stats = new - { - totalLogs = _webSocketManager.GetLogCacheCount(), - cacheSize = 10000, // LogCacheSize - timestamp = DateTime.UtcNow - }; - return Ok(stats); - } - catch (Exception ex) - { - _logger.LogError(ex, "获取日志缓存统计信息时发生错误"); - return StatusCode(500, new { message = "获取统计信息失败", error = ex.Message }); - } - } - - /// - /// 清空全局日志缓存 - /// - /// 操作结果 - [HttpPost("logs/clear")] - public ActionResult ClearLogCache() - { - try - { - _webSocketManager.ClearLogCache(); - return Ok(new { message = "日志缓存已清空" }); - } - catch (Exception ex) - { - _logger.LogError(ex, "清空日志缓存时发生错误"); - return StatusCode(500, new { message = "清空日志缓存失败", error = ex.Message }); - } - } - - /// - /// 重置全局日志缓存 - /// - /// 操作结果 - [HttpPost("logs/reset")] - public ActionResult ResetLogCache() - { - try - { - _webSocketManager.ResetLogCache(); - return Ok(new { message = "日志缓存已重置" }); - } - catch (Exception ex) - { - _logger.LogError(ex, "重置日志缓存时发生错误"); - return StatusCode(500, new { message = "重置日志缓存失败", error = ex.Message }); - } - } - - /// - /// 添加测试日志数据 - /// - /// 操作结果 - [HttpPost("logs/add-test-data")] - public ActionResult AddTestLogData() - { - try - { - var testLogs = new List - { - - }; - - // 手动添加到日志缓存 - _webSocketManager.AddLogsToCache(testLogs); - - return Ok(new { message = $"已添加 {testLogs.Count} 条测试日志" }); - } - catch (Exception ex) - { - _logger.LogError(ex, "添加测试日志数据时发生错误"); - return StatusCode(500, new { message = "添加测试日志失败", error = ex.Message }); - } - } - - /// - /// 使用 Server-Sent Events (SSE) 实时推送全局日志 - /// - [HttpGet("logs/stream")] - public async Task StreamLogs(CancellationToken cancellationToken) - { - try - { - Response.ContentType = "text/event-stream"; - Response.Headers.Append("Cache-Control", "no-cache"); - Response.Headers.Append("Connection", "keep-alive"); - Response.Headers.Append("Access-Control-Allow-Origin", "*"); - - // 发送连接成功事件 - await SendSseEvent("connected", new { message = "日志流连接已建立", timestamp = DateTime.UtcNow }); - await Response.Body.FlushAsync(cancellationToken); - - int lastLogCount = 0; - var lastLogs = new List(); - var lastLogHash = string.Empty; // 添加日志内容哈希值用于比较 - - // 首先,一次性推送所有已缓存的日志 - try - { - var initialLogs = _webSocketManager.GetLogCache()?.ToList() ?? new List(); - _logger.LogInformation("StreamLogs: 获取到初始日志 {Count} 条", initialLogs.Count); - - if (initialLogs.Any()) - { - _logger.LogInformation("StreamLogs: 发送历史日志事件,日志数量: {Count}", initialLogs.Count); - await SendSseEvent("history", new { logs = initialLogs, totalCount = initialLogs.Count }); - await Response.Body.FlushAsync(cancellationToken); - lastLogCount = initialLogs.Count; - lastLogs = initialLogs.ToList(); // 保存副本用于比较 - lastLogHash = CalculateLogsHash(initialLogs); // 计算初始日志哈希值 - } - else - { - _logger.LogInformation("StreamLogs: 没有历史日志数据"); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "获取初始日志时发生错误"); - await SendSseEvent("error", new { message = "获取初始日志失败", error = ex.Message }); - await Response.Body.FlushAsync(cancellationToken); - } - - while (!cancellationToken.IsCancellationRequested) - { - try - { - var currentLogs = _webSocketManager.GetLogCache()?.ToList() ?? new List(); - var currentLogHash = CalculateLogsHash(currentLogs); - - _logger.LogDebug("StreamLogs: 当前日志数量: {CurrentCount}, 上次日志数量: {LastCount}, 哈希值: {CurrentHash}", - currentLogs.Count, lastLogCount, currentLogHash); - - // 检查是否有新日志(通过数量和内容哈希双重检查) - bool hasNewLogs = false; - List newLogs = new List(); - - if (currentLogs.Count > lastLogCount) - { - // 数量增加,计算新增的日志 - newLogs = currentLogs.Skip(lastLogCount).ToList(); - hasNewLogs = newLogs.Any(); - _logger.LogDebug("StreamLogs: 检测到数量增加,新增 {NewCount} 条日志", newLogs.Count); - } - else if (currentLogs.Count == lastLogCount && currentLogHash != lastLogHash) - { - // 数量相同但内容变化,可能是日志被替换或更新 - // 比较每个日志项,找出变化的日志 - newLogs = GetChangedLogs(lastLogs, currentLogs); - hasNewLogs = newLogs.Any(); - _logger.LogDebug("StreamLogs: 检测到内容变化,变化 {ChangedCount} 条日志", newLogs.Count); - } - else if (currentLogs.Count < lastLogCount) - { - // 日志被清空或重置的情况 - _logger.LogInformation("检测到日志缓存被重置,重新同步"); - await SendSseEvent("reset", new { - message = "日志缓存已重置", - totalCount = currentLogs.Count - }); - await Response.Body.FlushAsync(cancellationToken); - - lastLogCount = currentLogs.Count; - lastLogs = currentLogs.ToList(); - lastLogHash = currentLogHash; - continue; // 跳过本次循环,等待下次检查 - } - else - { - // 数量和内容都没有变化 - _logger.LogDebug("StreamLogs: 没有检测到变化,当前数量: {CurrentCount}, 上次数量: {LastCount}", - currentLogs.Count, lastLogCount); - } - - if (hasNewLogs && newLogs.Any()) - { - _logger.LogInformation("StreamLogs: 发送新日志事件,新增日志数量: {NewCount}, 总日志数量: {TotalCount}", newLogs.Count, currentLogs.Count); - _logger.LogDebug("StreamLogs: 新日志详情 - 第一条: {FirstLog}, 最后一条: {LastLog}", - newLogs.FirstOrDefault()?.Message, - newLogs.LastOrDefault()?.Message); - - var eventData = new { - logs = newLogs, - totalCount = currentLogs.Count, - newCount = newLogs.Count - }; - - _logger.LogDebug("StreamLogs: 准备发送事件数据: {EventData}", - Newtonsoft.Json.JsonConvert.SerializeObject(eventData)); - - await SendSseEvent("new_logs", eventData); - await Response.Body.FlushAsync(cancellationToken); - - _logger.LogInformation("StreamLogs: 新日志事件发送完成"); - - // 更新索引和缓存 - lastLogCount = currentLogs.Count; - lastLogs = currentLogs.ToList(); - lastLogHash = currentLogHash; - } - - await Task.Delay(250, cancellationToken); - } - catch (OperationCanceledException) - { - // 正常的取消操作,退出循环 - break; - } - catch (Exception ex) - { - _logger.LogError(ex, "StreamLogs 循环中发生错误"); - await SendSseEvent("error", new { - message = "处理日志流时发生错误", - error = ex.Message, - timestamp = DateTime.UtcNow - }); - await Response.Body.FlushAsync(cancellationToken); - - // 等待一段时间后继续,避免频繁错误 - await Task.Delay(1000, cancellationToken); - } - } - - // 发送断开连接事件 - await SendSseEvent("disconnected", new { - message = "日志流连接已断开", - timestamp = DateTime.UtcNow - }); - await Response.Body.FlushAsync(cancellationToken); - } - catch (OperationCanceledException) - { - _logger.LogInformation("StreamLogs 连接被客户端取消"); - } - catch (Exception ex) - { - _logger.LogError(ex, "StreamLogs 方法执行时发生未处理的异常"); - try - { - await SendSseEvent("fatal_error", new { - message = "服务器内部错误", - error = ex.Message, - timestamp = DateTime.UtcNow - }); - await Response.Body.FlushAsync(); - } - catch - { - // 忽略发送错误事件时的异常 - } - } - } - - /// - /// 计算日志列表的哈希值,用于检测内容变化 - /// - /// 日志列表 - /// 哈希值字符串 - private string CalculateLogsHash(List logs) - { - if (logs == null || !logs.Any()) - return string.Empty; - - try - { - // 使用日志的关键字段计算哈希值 - var hashInput = string.Join("|", logs.Select(log => - $"{log.Timestamp}_{log.Layer}_{log.Message}")); - - using (var sha256 = System.Security.Cryptography.SHA256.Create()) - { - var bytes = System.Text.Encoding.UTF8.GetBytes(hashInput); - var hash = sha256.ComputeHash(bytes); - return Convert.ToBase64String(hash); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "计算日志哈希值时发生错误"); - return string.Empty; - } - } - - /// - /// 获取变化的日志项 - /// - /// 旧日志列表 - /// 新日志列表 - /// 变化的日志列表 - private List GetChangedLogs(List oldLogs, List newLogs) - { - var changedLogs = new List(); - - if (oldLogs.Count != newLogs.Count) - { - // 数量不同,返回所有新日志 - return newLogs; - } - - // 比较每个日志项 - for (int i = 0; i < newLogs.Count; i++) - { - if (i < oldLogs.Count) - { - var oldLog = oldLogs[i]; - var newLog = newLogs[i]; - - // 比较关键字段 - if (oldLog.Timestamp != newLog.Timestamp || - oldLog.Layer != newLog.Layer || - oldLog.Message != newLog.Message) - { - changedLogs.Add(newLog); - } - } - else - { - // 新增的日志 - changedLogs.Add(newLogs[i]); - } - } - - return changedLogs; - } - - /// - /// 获取客户端消息日志文件列表 - /// - /// 日志文件列表 - [HttpGet("clients/message-logs")] - public ActionResult GetClientMessageLogFiles() - { - try - { - if (!Directory.Exists(_logsDirectory)) - { - return Ok(new { files = new List(), message = "日志目录不存在" }); - } - - var logFiles = Directory.GetFiles(_logsDirectory, "*.log") - .Select(filePath => new - { - fileName = Path.GetFileName(filePath), - filePath = filePath, - size = new System.IO.FileInfo(filePath).Length, - lastModified = System.IO.File.GetLastWriteTime(filePath), - clientName = Path.GetFileNameWithoutExtension(filePath).Replace("_sent_messages", "").Replace("_received_messages", ""), - type = filePath.Contains("_sent_messages") ? "发送消息" : "接收消息" - }) - .OrderByDescending(f => f.lastModified) - .ToList(); - - return Ok(new { files = logFiles, totalCount = logFiles.Count }); - } - catch (Exception ex) - { - _logger.LogError(ex, "获取客户端消息日志文件列表时发生错误"); - return StatusCode(500, new { message = "获取日志文件列表失败", error = ex.Message }); - } - } - - /// - /// 获取客户端消息日志文件内容 - /// - /// 文件名 - /// 返回的行数(默认100行) - /// 日志文件内容 - [HttpGet("clients/message-logs/{fileName}")] - public ActionResult GetClientMessageLogContent(string fileName, [FromQuery] int lines = 100) - { - try - { - var filePath = Path.Combine(_logsDirectory, fileName); - if (!System.IO.File.Exists(filePath)) - { - return NotFound(new { message = $"日志文件 '{fileName}' 不存在" }); - } - - var fileInfo = new System.IO.FileInfo(filePath); - var allLines = System.IO.File.ReadAllLines(filePath, Encoding.UTF8); - var lastLines = allLines.TakeLast(lines).ToList(); - - return Ok(new - { - fileName = fileName, - filePath = filePath, - totalLines = allLines.Length, - returnedLines = lastLines.Count, - fileSize = fileInfo.Length, - lastModified = fileInfo.LastWriteTime, - content = lastLines - }); - } - catch (Exception ex) - { - _logger.LogError(ex, "获取客户端消息日志文件内容时发生错误: {FileName}", fileName); - return StatusCode(500, new { message = "获取日志文件内容失败", error = ex.Message }); - } - } - - /// - /// 清空客户端消息日志文件 - /// - /// 文件名(可选,如果不提供则清空所有日志文件) - /// 操作结果 - [HttpDelete("clients/message-logs")] - public ActionResult ClearClientMessageLogs([FromQuery] string? fileName = null) - { - try - { - if (!Directory.Exists(_logsDirectory)) - { - return Ok(new { message = "日志目录不存在,无需清空" }); - } - - int clearedCount = 0; - if (string.IsNullOrEmpty(fileName)) - { - // 清空所有日志文件 - var logFiles = Directory.GetFiles(_logsDirectory, "*.log"); - foreach (var filePath in logFiles) - { - System.IO.File.WriteAllText(filePath, string.Empty); - clearedCount++; - } - _logger.LogInformation("已清空 {Count} 个客户端消息日志文件", clearedCount); - return Ok(new { message = $"已清空 {clearedCount} 个客户端消息日志文件" }); - } - else - { - // 清空指定日志文件 - var filePath = Path.Combine(_logsDirectory, fileName); - if (!System.IO.File.Exists(filePath)) - { - return NotFound(new { message = $"日志文件 '{fileName}' 不存在" }); - } - - System.IO.File.WriteAllText(filePath, string.Empty); - _logger.LogInformation("已清空客户端消息日志文件: {FileName}", fileName); - return Ok(new { message = $"已清空客户端消息日志文件 '{fileName}'" }); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "清空客户端消息日志文件时发生错误"); - return StatusCode(500, new { message = "清空日志文件失败", error = ex.Message }); - } - } - - /// - /// 删除客户端消息日志文件 - /// - /// 文件名(可选,如果不提供则删除所有日志文件) - /// 操作结果 - [HttpDelete("clients/message-logs/delete")] - public ActionResult DeleteClientMessageLogs([FromQuery] string? fileName = null) - { - try - { - if (!Directory.Exists(_logsDirectory)) - { - return Ok(new { message = "日志目录不存在,无需删除" }); - } - - int deletedCount = 0; - if (string.IsNullOrEmpty(fileName)) - { - // 删除所有日志文件 - var logFiles = Directory.GetFiles(_logsDirectory, "*.log"); - foreach (var filePath in logFiles) - { - System.IO.File.Delete(filePath); - deletedCount++; - } - _logger.LogInformation("已删除 {Count} 个客户端消息日志文件", deletedCount); - return Ok(new { message = $"已删除 {deletedCount} 个客户端消息日志文件" }); - } - else - { - // 删除指定日志文件 - var filePath = Path.Combine(_logsDirectory, fileName); - if (!System.IO.File.Exists(filePath)) - { - return NotFound(new { message = $"日志文件 '{fileName}' 不存在" }); - } - - System.IO.File.Delete(filePath); - _logger.LogInformation("已删除客户端消息日志文件: {FileName}", fileName); - return Ok(new { message = $"已删除客户端消息日志文件 '{fileName}'" }); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "删除客户端消息日志文件时发生错误"); - return StatusCode(500, new { message = "删除日志文件失败", error = ex.Message }); - } - } - - /// - /// 获取日志缓存详细状态(调试用) - /// - /// 日志缓存详细状态 - [HttpGet("logs/debug")] - public ActionResult GetLogCacheDebugInfo() - { - try - { - var logs = _webSocketManager.GetLogCache()?.ToList() ?? new List(); - var logCount = _webSocketManager.GetLogCacheCount(); - var cacheStatus = _webSocketManager.GetLogCacheStatus(); - - var debugInfo = new - { - totalLogs = logCount, - actualLogsCount = logs.Count, - cacheSize = 10000, // LogCacheSize - timestamp = DateTime.UtcNow, - sampleLogs = logs.TakeLast(5).Select(log => new - { - timestamp = log.Timestamp, - layer = log.Layer, - message = log.Message?.Substring(0, Math.Min(100, log.Message?.Length ?? 0)) + "..." - }).ToList(), - logHash = CalculateLogsHash(logs), - cacheStatus = cacheStatus - }; - - return Ok(debugInfo); - } - catch (Exception ex) - { - _logger.LogError(ex, "获取日志缓存调试信息时发生错误"); - return StatusCode(500, new { message = "获取调试信息失败", error = ex.Message }); - } - } - - /// - /// 获取SSE连接状态(调试用) - /// - /// 连接状态信息 - [HttpGet("logs/connection-status")] - public ActionResult GetSseConnectionStatus() - { - try - { - var status = new - { - timestamp = DateTime.UtcNow, - requestHeaders = Request.Headers.ToDictionary(h => h.Key, h => h.Value.ToString()), - userAgent = Request.Headers["User-Agent"].ToString(), - accept = Request.Headers["Accept"].ToString(), - cacheControl = Request.Headers["Cache-Control"].ToString(), - connection = Request.Headers["Connection"].ToString(), - isHttps = Request.IsHttps, - host = Request.Host.ToString(), - path = Request.Path.ToString(), - queryString = Request.QueryString.ToString(), - method = Request.Method, - contentType = Request.ContentType, - contentLength = Request.ContentLength - }; - - return Ok(status); - } - catch (Exception ex) - { - _logger.LogError(ex, "获取SSE连接状态时发生错误"); - return StatusCode(500, new { message = "获取连接状态失败", error = ex.Message }); - } - } - - /// - /// 测试SSE连接(调试用) - /// - /// 测试结果 - [HttpGet("logs/test-connection")] - public ActionResult TestSseConnection() - { - try - { - var testResult = new - { - message = "SSE连接测试成功", - timestamp = DateTime.UtcNow, - serverTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"), - logCacheCount = _webSocketManager.GetLogCacheCount(), - testData = new { test = true, message = "这是一个测试消息" } - }; - - return Ok(testResult); - } - catch (Exception ex) - { - _logger.LogError(ex, "测试SSE连接时发生错误"); - return StatusCode(500, new { message = "测试连接失败", error = ex.Message }); - } - } - - /// - /// 强制推送测试日志(调试用) - /// - /// 操作结果 - [HttpPost("logs/force-push-test")] - public ActionResult ForcePushTestLogs() - { - try - { - var testLogs = new List - { - }; - - // 手动添加到日志缓存 - _webSocketManager.AddLogsToCache(testLogs); - - _logger.LogInformation("强制推送测试日志: {Message}", testLogs[0].Message); - - return Ok(new { - message = $"已强制推送测试日志: {testLogs[0].Message}", - timestamp = testLogs[0].Timestamp - }); - } - catch (Exception ex) - { - _logger.LogError(ex, "强制推送测试日志时发生错误"); - return StatusCode(500, new { message = "强制推送失败", error = ex.Message }); - } - } - } - - /// - /// 日志配置请求 - /// - public class LogsConfigRequest - { - /// - /// 日志配置 - /// - public Dictionary Config { get; set; } = new(); - - /// - /// 是否保存配置 - /// - public bool Save { get; set; } = false; + // 该控制器已拆分,原有API请参见 ClientController、ConfigController、TestConfigController、LogController、MessageController } } \ No newline at end of file diff --git a/LTEMvcApp/README_ClientMessageLogs.md b/LTEMvcApp/README_ClientMessageLogs.md index 95c4f23..03e18bc 100644 --- a/LTEMvcApp/README_ClientMessageLogs.md +++ b/LTEMvcApp/README_ClientMessageLogs.md @@ -20,35 +20,45 @@ -------------------------------------------------------------------------------- ``` -### 3. 日志文件管理 API +## API 接口 -#### 获取日志文件列表 -```http -GET /api/websocket/clients/message-logs +### 获取客户端消息日志文件列表 +``` +GET /api/message/logs ``` -#### 获取日志文件内容 -```http -GET /api/websocket/clients/message-logs/{fileName}?lines=100 +### 获取客户端消息日志文件内容 +``` +GET /api/message/logs/{fileName}?lines=100 ``` -#### 清空日志文件 -```http -DELETE /api/websocket/clients/message-logs?fileName={fileName} +### 清空客户端消息日志文件 +``` +DELETE /api/message/logs?fileName={fileName} ``` -#### 删除日志文件 -```http -DELETE /api/websocket/clients/message-logs/delete?fileName={fileName} +### 删除客户端消息日志文件 +``` +DELETE /api/message/logs/delete?fileName={fileName} ``` -### 4. Web 界面管理 -在 `ClientMessages.cshtml` 页面中添加了日志文件管理面板,提供: -- 日志文件列表显示 -- 文件大小和修改时间信息 -- 查看日志内容(支持不同行数显示) -- 清空和删除日志文件功能 -- 实时刷新日志文件列表 +### 客户端消息流(SSE) +``` +GET /api/message/{clientName}/stream +``` + +### 发送消息到客户端 +``` +POST /api/message/{clientName}/send +Content-Type: application/json + +{ + "type": "command", + "data": { + "action": "test" + } +} +``` ## 技术实现 diff --git a/LTEMvcApp/README_MultipleTestConfigs.md b/LTEMvcApp/README_MultipleTestConfigs.md new file mode 100644 index 0000000..dde4f7f --- /dev/null +++ b/LTEMvcApp/README_MultipleTestConfigs.md @@ -0,0 +1,174 @@ +# 多个测试客户端配置功能 + +## 功能概述 + +本功能允许系统保存和管理多个测试客户端配置,使用服务器地址(Address)作为唯一标识符。系统已简化为只保留多个配置的管理功能。 + +## 主要特性 + +### 1. 基于Address的唯一性管理 +- 使用服务器地址(Address)作为配置的唯一key +- 保存前自动检查Address是否存在 +- 存在则更新,不存在则添加新配置 + +### 2. 自动配置加载 +- 页面加载时显示默认配置(第一个配置) +- 输入Address时自动查询并加载对应配置 +- 支持实时配置切换 + +### 3. 简化的配置文件管理 +- 只使用 `test_client_configs.json` 存储所有配置 +- 移除了单个配置文件的管理 +- 自动创建默认配置(如果没有配置存在) + +## API接口 + +### 获取默认测试客户端配置 +``` +GET /api/testconfig/default +``` + +### 获取所有测试客户端配置 +``` +GET /api/testconfig +``` + +### 根据地址获取测试客户端配置 +``` +GET /api/testconfig/address/{address} +``` + +### 设置默认测试客户端配置 +``` +POST /api/testconfig/default +``` + +### 删除测试客户端配置 +``` +DELETE /api/testconfig/address/{address} +``` + +### 启动测试客户端 +``` +POST /api/testconfig/start +Content-Type: application/json + +{ + "address": "192.168.13.12:9001" // 可选,如果不提供则使用默认配置 +} +``` + +### 停止测试客户端 +``` +POST /api/testconfig/stop +Content-Type: application/json + +{ + "address": "192.168.13.12:9001" // 可选,如果不提供则使用默认配置 +} +``` + +## 前端功能 + +### 自动配置查询 +- 在地址输入框失去焦点时自动查询配置 +- 如果找到配置则自动填充表单 +- 如果未找到则准备创建新配置 + +### 智能保存 +- 保存时自动检查Address是否存在 +- 存在则更新现有配置 +- 不存在则创建新配置 + +## 使用示例 + +### 1. 创建新配置 +1. 在配置页面输入新的服务器地址 +2. 填写其他配置信息 +3. 点击保存,系统会自动创建新配置 + +### 2. 更新现有配置 +1. 输入已存在的服务器地址 +2. 系统会自动加载现有配置 +3. 修改配置信息 +4. 点击保存,系统会更新现有配置 + +### 3. 切换配置 +1. 在地址输入框中输入不同的地址 +2. 系统会自动加载对应的配置 +3. 可以查看和编辑不同地址的配置 + +## 技术实现 + +### 后端实现 +- `WebSocketManagerService` 简化为只管理多个配置 +- 使用 `List` 存储所有配置 +- 基于Address的唯一性检查 +- 自动文件持久化 +- 移除了单个配置的管理逻辑 + +### 前端实现 +- JavaScript自动查询功能 +- 表单自动填充 +- 实时配置切换 + +## 文件结构 + +``` +Services/ +├── WebSocketManagerService.cs # 简化的多配置管理逻辑 +Controllers/ +├── WebSocketController.cs # API接口 +Views/Home/ +├── TestClientConfig.cshtml # 配置管理页面 +``` + +## 配置文件格式 + +### test_client_configs.json +```json +[ + { + "name": "TestClient1", + "address": "192.168.13.12:9001", + "enabled": true, + "password": "test123", + "reconnectDelay": 15000, + "ssl": false, + "readonly": false, + "mode": "ran", + "logs": { + "layers": { + "PHY": { + "level": "debug", + "filter": "debug", + "maxSize": 1000, + "payload": true + } + } + } + } +] +``` + +## 简化设计说明 + +### 移除的功能 +- 单个测试配置文件 (`test_client_config.json`) +- 单个配置的独立管理方法 +- 复杂的配置切换逻辑 + +### 保留的功能 +- 多个配置的管理 +- 基于Address的唯一性检查 +- 自动配置加载和保存 +- 默认配置的自动创建 + +## 注意事项 + +1. Address必须是唯一的,不能重复 +2. 系统会自动创建默认配置(如果没有配置存在) +3. 删除配置时使用Address作为标识 +4. 页面刷新时会加载默认配置(第一个配置) +5. 所有配置都会持久化到 `test_client_configs.json` 文件中 +6. 系统已简化为只管理多个配置,不再维护单个配置文件 \ No newline at end of file diff --git a/LTEMvcApp/README_StreamLogs_Optimization.md b/LTEMvcApp/README_StreamLogs_Optimization.md index 39ad21b..5b7a815 100644 --- a/LTEMvcApp/README_StreamLogs_Optimization.md +++ b/LTEMvcApp/README_StreamLogs_Optimization.md @@ -65,17 +65,35 @@ ### 1. 获取日志缓存统计信息 ``` -GET /api/websocket/logs/stats +GET /api/log/stats ``` ### 2. 清空日志缓存 ``` -POST /api/websocket/logs/clear +POST /api/log/clear ``` ### 3. 重置日志缓存 ``` -POST /api/websocket/logs/reset +POST /api/log/reset +``` + +### 4. 添加测试日志数据 +``` +POST /api/log/add-test-data +``` + +### 5. 实时日志流(SSE) +``` +GET /api/log/stream +``` + +### 6. 调试信息 +``` +GET /api/log/debug +GET /api/log/connection-status +GET /api/log/test-connection +POST /api/log/force-push-test ``` ## 事件类型 @@ -102,7 +120,7 @@ POST /api/websocket/logs/reset ```javascript // 连接日志流 -const eventSource = new EventSource('/api/websocket/logs/stream'); +const eventSource = new EventSource('/api/log/stream'); eventSource.addEventListener('connected', (event) => { console.log('日志流连接已建立'); diff --git a/LTEMvcApp/Services/WebSocketManagerService.cs b/LTEMvcApp/Services/WebSocketManagerService.cs index 6765eec..82b256d 100644 --- a/LTEMvcApp/Services/WebSocketManagerService.cs +++ b/LTEMvcApp/Services/WebSocketManagerService.cs @@ -23,10 +23,10 @@ namespace LTEMvcApp.Services private readonly ConcurrentDictionary _configs; private readonly ILogger _logger; private readonly IServiceProvider _serviceProvider; - private ClientConfig _testClientConfig; + private List _testClientConfigs; // 只保留多个测试配置 private const int LogCacheSize = 10000; // 服务器最多缓存10000条最新日志 private readonly ConcurrentQueue _logCache = new ConcurrentQueue(); - private readonly string _configFilePath = "test_client_config.json"; + private readonly string _configsFilePath = "test_client_configs.json"; // 只保留多个配置文件路径 #endregion @@ -65,88 +65,57 @@ namespace LTEMvcApp.Services _configs = new ConcurrentDictionary(); _logger = logger; _serviceProvider = serviceProvider; + _testClientConfigs = new List(); // 初始化测试配置列表 - LoadTestClientConfig(); + LoadTestClientConfigs(); // 加载多个测试配置 _logger.LogInformation("WebSocketManagerService 初始化"); } /// - /// 加载测试客户端配置 + /// 加载多个测试客户端配置 /// - private void LoadTestClientConfig() + private void LoadTestClientConfigs() { try { - if (File.Exists(_configFilePath)) + if (File.Exists(_configsFilePath)) { - var json = File.ReadAllText(_configFilePath); - _testClientConfig = JsonSerializer.Deserialize(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true })!; - _logger.LogInformation("成功从 {FilePath} 加载测试客户端配置。", _configFilePath); + var json = File.ReadAllText(_configsFilePath); + _testClientConfigs = JsonSerializer.Deserialize>(json, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }) ?? new List(); + _logger.LogInformation("成功从 {FilePath} 加载 {Count} 个测试客户端配置。", _configsFilePath, _testClientConfigs.Count); } else { - _logger.LogWarning("配置文件 {FilePath} 未找到,将创建并使用默认配置。", _configFilePath); - _testClientConfig = GetDefaultTestConfig(); - SaveTestClientConfig(); + _logger.LogWarning("多个配置文件 {FilePath} 未找到,将创建空配置列表。", _configsFilePath); + _testClientConfigs = new List(); + SaveTestClientConfigs(); } } catch (Exception ex) { - _logger.LogError(ex, "加载或创建测试客户端配置文件时出错。将使用默认配置。"); - _testClientConfig = GetDefaultTestConfig(); + _logger.LogError(ex, "加载多个测试客户端配置文件时出错。将使用空配置列表。"); + _testClientConfigs = new List(); } } /// - /// 保存测试客户端配置到文件 + /// 保存多个测试客户端配置到文件 /// - private void SaveTestClientConfig() + private void SaveTestClientConfigs() { try { var options = new JsonSerializerOptions { WriteIndented = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; - var json = JsonSerializer.Serialize(_testClientConfig, options); - File.WriteAllText(_configFilePath, json); - _logger.LogInformation("测试客户端配置已成功保存到 {FilePath}。", _configFilePath); + var json = JsonSerializer.Serialize(_testClientConfigs, options); + File.WriteAllText(_configsFilePath, json); + _logger.LogInformation("多个测试客户端配置已成功保存到 {FilePath},共 {Count} 个配置。", _configsFilePath, _testClientConfigs.Count); } catch (Exception ex) { - _logger.LogError(ex, "保存测试客户端配置文件失败。"); + _logger.LogError(ex, "保存多个测试客户端配置文件失败。"); } } - - private ClientConfig GetDefaultTestConfig() - { - var layers = new Dictionary(); - foreach(var layerName in LogLayerTypes.AllLayers.Where(l => l != "EVENT")) - { - layers[layerName] = new LogLayerConfig { Level = LogLayerTypes.GetDefaultLevel(layerName), Filter = "warn", MaxSize = 1000, Payload = false }; - } - - // Set some specific payloads to true - if(layers.ContainsKey("PHY")) layers["PHY"].Payload = true; - if(layers.ContainsKey("MAC")) layers["MAC"].Payload = true; - if(layers.ContainsKey("RRC")) layers["RRC"].Payload = true; - if(layers.ContainsKey("NAS")) layers["NAS"].Payload = true; - - - return new ClientConfig - { - Name = "TestClient", - Enabled = true, - Address = "192.168.13.12:9001", - Ssl = false, - ReconnectDelay = 15000, - Password = "test123", - Logs = new ClientLogsConfig - { - Layers = layers, - Signal = true, - Cch = true - } - }; - } #endregion @@ -200,10 +169,12 @@ namespace LTEMvcApp.Services _logger.LogInformation($"启动客户端: {clientName}"); ClientConfig config; - if (clientName == _testClientConfig.Name) + + // 检查是否是测试客户端 + var testConfig = _testClientConfigs.FirstOrDefault(c => c.Name == clientName); + if (testConfig != null) { - // 使用测试客户端配置 - config = _testClientConfig; + config = testConfig; _logger.LogInformation($"使用测试客户端配置: {config.Name}"); } else if (!_configs.TryGetValue(clientName, out config)) @@ -430,12 +401,30 @@ namespace LTEMvcApp.Services } /// - /// 获取测试客户端配置 + /// 获取所有测试客户端配置 /// - /// 测试客户端配置 - public ClientConfig GetTestClientConfig() + /// 测试客户端配置列表 + public List GetAllTestClientConfigs() + { + return _testClientConfigs.ToList(); + } + + /// + /// 获取默认测试客户端配置(第一个配置或创建默认配置) + /// + /// 默认测试客户端配置 + public ClientConfig GetDefaultTestClientConfig() { - return _testClientConfig; + if (_testClientConfigs.Any()) + { + return _testClientConfigs.First(); + } + + // 如果没有配置,创建默认配置 + var defaultConfig = CreateDefaultTestConfig(); + _testClientConfigs.Add(defaultConfig); + SaveTestClientConfigs(); + return defaultConfig; } /// @@ -451,9 +440,32 @@ namespace LTEMvcApp.Services return false; } - _logger.LogInformation($"更新测试客户端配置: {config.Name}"); - _testClientConfig = config; - SaveTestClientConfig(); + if (string.IsNullOrEmpty(config.Address)) + { + _logger.LogWarning("尝试设置空地址的测试客户端配置"); + return false; + } + + _logger.LogInformation($"更新测试客户端配置: {config.Name} (地址: {config.Address})"); + + // 使用Address作为唯一key来检查是否存在 + var existingConfigIndex = _testClientConfigs.FindIndex(c => c.Address == config.Address); + if (existingConfigIndex >= 0) + { + // 更新现有配置 + _testClientConfigs[existingConfigIndex] = config; + _logger.LogInformation($"更新现有测试配置 (地址: {config.Address})"); + } + else + { + // 添加新配置 + _testClientConfigs.Add(config); + _logger.LogInformation($"添加新测试配置 (地址: {config.Address})"); + } + + // 保存到文件 + SaveTestClientConfigs(); + return true; } @@ -463,8 +475,9 @@ namespace LTEMvcApp.Services /// 是否成功启动 public bool StartTestClient() { - _logger.LogInformation("启动测试客户端"); - return StartClient(_testClientConfig.Name); + var defaultConfig = GetDefaultTestClientConfig(); + _logger.LogInformation("启动测试客户端: " + defaultConfig.Name); + return StartClient(defaultConfig.Name); } /// @@ -473,8 +486,9 @@ namespace LTEMvcApp.Services /// 是否成功停止 public bool StopTestClient() { - _logger.LogInformation("停止测试客户端"); - return StopClient(_testClientConfig.Name); + var defaultConfig = GetDefaultTestClientConfig(); + _logger.LogInformation("停止测试客户端: " + defaultConfig.Name); + return StopClient(defaultConfig.Name); } /// @@ -483,7 +497,71 @@ namespace LTEMvcApp.Services /// 测试客户端的WebSocket实例 public LTEClientWebSocket? GetTestClient() { - return GetClientInstance(_testClientConfig.Name); + var defaultConfig = GetDefaultTestClientConfig(); + return GetClientInstance(defaultConfig.Name); + } + + /// + /// 创建默认测试配置 + /// + /// 默认测试配置 + private ClientConfig CreateDefaultTestConfig() + { + var layers = new Dictionary(); + foreach(var layerName in LogLayerTypes.AllLayers.Where(l => l != "EVENT")) + { + layers[layerName] = new LogLayerConfig { Level = LogLayerTypes.GetDefaultLevel(layerName), Filter = "warn", MaxSize = 1000, Payload = false }; + } + + // Set some specific payloads to true + if(layers.ContainsKey("PHY")) layers["PHY"].Payload = true; + if(layers.ContainsKey("MAC")) layers["MAC"].Payload = true; + if(layers.ContainsKey("RRC")) layers["RRC"].Payload = true; + if(layers.ContainsKey("NAS")) layers["NAS"].Payload = true; + + return new ClientConfig + { + Name = "TestClient", + Enabled = true, + Address = "192.168.13.12:9001", + Ssl = false, + ReconnectDelay = 15000, + Password = "test123", + Logs = new ClientLogsConfig + { + Layers = layers, + Signal = true, + Cch = true + } + }; + } + + /// + /// 根据地址获取测试客户端配置 + /// + /// 服务器地址 + /// 测试客户端配置 + public ClientConfig? GetTestClientConfigByAddress(string address) + { + return _testClientConfigs.FirstOrDefault(c => c.Address == address); + } + + /// + /// 删除测试客户端配置 + /// + /// 服务器地址 + /// 是否成功删除 + public bool RemoveTestClientConfig(string address) + { + var config = _testClientConfigs.FirstOrDefault(c => c.Address == address); + if (config != null) + { + _testClientConfigs.Remove(config); + SaveTestClientConfigs(); + _logger.LogInformation($"删除测试客户端配置 (地址: {address})"); + return true; + } + return false; } /// diff --git a/LTEMvcApp/Views/Home/ClientMessages.cshtml b/LTEMvcApp/Views/Home/ClientMessages.cshtml index 466e99a..cb40f0e 100644 --- a/LTEMvcApp/Views/Home/ClientMessages.cshtml +++ b/LTEMvcApp/Views/Home/ClientMessages.cshtml @@ -315,7 +315,7 @@ function loadLogFiles() { $('#logFilesContainer').html('
正在加载日志文件列表...
'); - $.get('/api/websocket/clients/message-logs') + $.get('/api/message/logs') .done(function(response) { if (response.files && response.files.length > 0) { const filesHtml = response.files.map(file => createLogFileItemHtml(file)).join(''); @@ -372,7 +372,7 @@ function loadLogContent(fileName, lines) { $('#logContentContainer').html('
正在加载日志内容...
'); - $.get(`/api/websocket/clients/message-logs/${encodeURIComponent(fileName)}?lines=${lines}`) + $.get(`/api/message/logs/${encodeURIComponent(fileName)}?lines=${lines}`) .done(function(response) { if (response.content && response.content.length > 0) { const contentHtml = response.content.map(line => @@ -402,7 +402,7 @@ } $.ajax({ - url: '/api/websocket/clients/message-logs', + url: '/api/message/logs', method: 'DELETE', data: { fileName: fileName } }) @@ -422,7 +422,7 @@ } $.ajax({ - url: '/api/websocket/clients/message-logs/delete', + url: '/api/message/logs/delete', method: 'DELETE', data: { fileName: fileName } }) @@ -436,7 +436,7 @@ } function initializeEventSource() { - const source = new EventSource(`/api/websocket/clients/${encodeURIComponent(clientName)}/messages/stream`); + const source = new EventSource(`/api/message/${encodeURIComponent(clientName)}/stream`); const statusBadge = $('#connection-status'); source.addEventListener('open', function(e) { diff --git a/LTEMvcApp/Views/Home/Index.cshtml b/LTEMvcApp/Views/Home/Index.cshtml index af0bc49..cb344a1 100644 --- a/LTEMvcApp/Views/Home/Index.cshtml +++ b/LTEMvcApp/Views/Home/Index.cshtml @@ -167,11 +167,11 @@ } - + 启动 - + 停止 @@ -198,8 +198,8 @@ @section Scripts { } \ No newline at end of file diff --git a/LTEMvcApp/test_client_config.json b/LTEMvcApp/test_client_config.json deleted file mode 100644 index 53bccdd..0000000 --- a/LTEMvcApp/test_client_config.json +++ /dev/null @@ -1,153 +0,0 @@ -{ - "name": "TestClient", - "address": "192.168.13.12:9001", - "enabled": true, - "password": "test123", - "reconnectDelay": 15000, - "ssl": false, - "logs": { - "layers": { - "PHY": { - "level": "info", - "maxSize": 1000, - "payload": true, - "filter": "warn", - "color": "#000000", - "direction": {}, - "epc": false, - "debug": null, - "max": null - }, - "MAC": { - "level": "warn", - "maxSize": 1000, - "payload": true, - "filter": "warn", - "color": "#000000", - "direction": {}, - "epc": false, - "debug": null, - "max": null - }, - "RLC": { - "level": "warn", - "maxSize": 1000, - "payload": false, - "filter": "warn", - "color": "#000000", - "direction": {}, - "epc": false, - "debug": null, - "max": null - }, - "PDCP": { - "level": "warn", - "maxSize": 1000, - "payload": false, - "filter": "warn", - "color": "#000000", - "direction": {}, - "epc": false, - "debug": null, - "max": null - }, - "RRC": { - "level": "debug", - "maxSize": 1000, - "payload": true, - "filter": "warn", - "color": "#000000", - "direction": {}, - "epc": false, - "debug": null, - "max": null - }, - "NAS": { - "level": "debug", - "maxSize": 1000, - "payload": true, - "filter": "warn", - "color": "#000000", - "direction": {}, - "epc": false, - "debug": null, - "max": null - }, - "S1AP": { - "level": "debug", - "maxSize": 1000, - "payload": false, - "filter": "warn", - "color": "#000000", - "direction": {}, - "epc": false, - "debug": null, - "max": null - }, - "NGAP": { - "level": "debug", - "maxSize": 1000, - "payload": false, - "filter": "warn", - "color": "#000000", - "direction": {}, - "epc": false, - "debug": null, - "max": null - }, - "GTPU": { - "level": "warn", - "maxSize": 1000, - "payload": false, - "filter": "warn", - "color": "#000000", - "direction": {}, - "epc": false, - "debug": null, - "max": null - }, - "X2AP": { - "level": "debug", - "maxSize": 1000, - "payload": false, - "filter": "warn", - "color": "#000000", - "direction": {}, - "epc": false, - "debug": null, - "max": null - }, - "XnAP": { - "level": "warn", - "maxSize": 1000, - "payload": false, - "filter": "warn", - "color": "#000000", - "direction": {}, - "epc": false, - "debug": null, - "max": null - }, - "M2AP": { - "level": "info", - "maxSize": 1000, - "payload": false, - "filter": "warn", - "color": "#000000", - "direction": {}, - "epc": false, - "debug": null, - "max": null - } - }, - "signal": null, - "cch": null, - "extensionData": null - }, - "pause": false, - "readonly": false, - "skipLogMenu": false, - "locked": false, - "active": false, - "model": null -} \ No newline at end of file diff --git a/LTEMvcApp/test_client_configs.json b/LTEMvcApp/test_client_configs.json new file mode 100644 index 0000000..3ebd238 --- /dev/null +++ b/LTEMvcApp/test_client_configs.json @@ -0,0 +1,310 @@ +[ + { + "name": "TestClient", + "address": "192.168.13.12:9001", + "enabled": true, + "password": "test123", + "reconnectDelay": 15000, + "ssl": false, + "logs": { + "layers": { + "PHY": { + "level": "warn", + "maxSize": 1, + "payload": false, + "filter": "warn", + "color": "#000000", + "direction": {}, + "epc": false, + "debug": null, + "max": null + }, + "MAC": { + "level": "warn", + "maxSize": 1, + "payload": false, + "filter": "warn", + "color": "#000000", + "direction": {}, + "epc": false, + "debug": null, + "max": null + }, + "RLC": { + "level": "warn", + "maxSize": 1, + "payload": false, + "filter": "warn", + "color": "#000000", + "direction": {}, + "epc": false, + "debug": null, + "max": null + }, + "PDCP": { + "level": "warn", + "maxSize": 1, + "payload": false, + "filter": "warn", + "color": "#000000", + "direction": {}, + "epc": false, + "debug": null, + "max": null + }, + "RRC": { + "level": "warn", + "maxSize": 1, + "payload": false, + "filter": "warn", + "color": "#000000", + "direction": {}, + "epc": false, + "debug": null, + "max": null + }, + "NAS": { + "level": "warn", + "maxSize": 1, + "payload": false, + "filter": "warn", + "color": "#000000", + "direction": {}, + "epc": false, + "debug": null, + "max": null + }, + "S1AP": { + "level": "warn", + "maxSize": 1, + "payload": false, + "filter": "warn", + "color": "#000000", + "direction": {}, + "epc": false, + "debug": null, + "max": null + }, + "NGAP": { + "level": "warn", + "maxSize": 1, + "payload": false, + "filter": "warn", + "color": "#000000", + "direction": {}, + "epc": false, + "debug": null, + "max": null + }, + "GTPU": { + "level": "warn", + "maxSize": 1, + "payload": false, + "filter": "warn", + "color": "#000000", + "direction": {}, + "epc": false, + "debug": null, + "max": null + }, + "X2AP": { + "level": "warn", + "maxSize": 1, + "payload": false, + "filter": "warn", + "color": "#000000", + "direction": {}, + "epc": false, + "debug": null, + "max": null + }, + "XnAP": { + "level": "warn", + "maxSize": 1, + "payload": false, + "filter": "warn", + "color": "#000000", + "direction": {}, + "epc": false, + "debug": null, + "max": null + }, + "M2AP": { + "level": "warn", + "maxSize": 1, + "payload": false, + "filter": "warn", + "color": "#000000", + "direction": {}, + "epc": false, + "debug": null, + "max": null + } + }, + "signal": null, + "cch": null, + "extensionData": null + }, + "pause": false, + "readonly": false, + "skipLogMenu": false, + "locked": false, + "active": false, + "mode": "ran", + "model": null + }, + { + "name": "TestClient", + "address": "192.168.13.12:9002", + "enabled": true, + "password": "test123", + "reconnectDelay": 15000, + "ssl": false, + "logs": { + "layers": { + "PHY": { + "level": "warn", + "maxSize": 1, + "payload": false, + "filter": "warn", + "color": "#000000", + "direction": {}, + "epc": false, + "debug": null, + "max": null + }, + "MAC": { + "level": "warn", + "maxSize": 1, + "payload": false, + "filter": "warn", + "color": "#000000", + "direction": {}, + "epc": false, + "debug": null, + "max": null + }, + "RLC": { + "level": "warn", + "maxSize": 1, + "payload": false, + "filter": "warn", + "color": "#000000", + "direction": {}, + "epc": false, + "debug": null, + "max": null + }, + "PDCP": { + "level": "warn", + "maxSize": 1, + "payload": false, + "filter": "warn", + "color": "#000000", + "direction": {}, + "epc": false, + "debug": null, + "max": null + }, + "RRC": { + "level": "warn", + "maxSize": 1, + "payload": false, + "filter": "warn", + "color": "#000000", + "direction": {}, + "epc": false, + "debug": null, + "max": null + }, + "NAS": { + "level": "warn", + "maxSize": 1, + "payload": false, + "filter": "warn", + "color": "#000000", + "direction": {}, + "epc": false, + "debug": null, + "max": null + }, + "S1AP": { + "level": "warn", + "maxSize": 1, + "payload": false, + "filter": "warn", + "color": "#000000", + "direction": {}, + "epc": false, + "debug": null, + "max": null + }, + "NGAP": { + "level": "warn", + "maxSize": 1, + "payload": false, + "filter": "warn", + "color": "#000000", + "direction": {}, + "epc": false, + "debug": null, + "max": null + }, + "GTPU": { + "level": "warn", + "maxSize": 1, + "payload": false, + "filter": "warn", + "color": "#000000", + "direction": {}, + "epc": false, + "debug": null, + "max": null + }, + "X2AP": { + "level": "warn", + "maxSize": 1, + "payload": false, + "filter": "warn", + "color": "#000000", + "direction": {}, + "epc": false, + "debug": null, + "max": null + }, + "XnAP": { + "level": "warn", + "maxSize": 1, + "payload": false, + "filter": "warn", + "color": "#000000", + "direction": {}, + "epc": false, + "debug": null, + "max": null + }, + "M2AP": { + "level": "warn", + "maxSize": 1, + "payload": false, + "filter": "warn", + "color": "#000000", + "direction": {}, + "epc": false, + "debug": null, + "max": null + } + }, + "signal": null, + "cch": null, + "extensionData": null + }, + "pause": false, + "readonly": false, + "skipLogMenu": false, + "locked": false, + "active": false, + "mode": "ran", + "model": null + } +] \ No newline at end of file