using AyCode.Core.Enums; using AyCode.Core.Helpers; using AyCode.Core.Loggers; using AyCode.Entities; using AyCode.Entities.LogItems; using AyCode.Services.SignalRs; using Microsoft.AspNetCore.SignalR.Client; namespace AyCode.Services.Loggers { /// /// Nem-generikus, JSON-protokollos alak — back-compat a meglévő fogyasztóknak /// (wire-típus: AcLogItemClient; a szerver-oldali párja az AcLoggerSignalRHub<TLogger> shim). /// public class AcSignaRClientLogItemWriter : AcSignaRClientLogItemWriter { public AcSignaRClientLogItemWriter(string fullHubName, AppType appType = AppType.Web, LogLevel logLevel = LogLevel.Detail, string? categoryName = null) : base(fullHubName, appType, logLevel, categoryName) { } } /// /// SignalR log-item feltöltő writer, generikus wire-típussal. A -et a fogyasztó /// adja (tipikusan egy [AcBinarySerializable]-lel jelölt AcLogItemClient-leszármazott — így a csatorna /// AcBinary-protokollal mehet; a generált serializer a fogyasztó rétegében él, az AyCode.Entities generator-mentes marad). /// Az opcionális acBinaryProtocolOptions-szel a kapcsolat az AcBinary protokollt használja; /// enélkül a SignalR default JSON-ja megy (back-compat). /// public class AcSignaRClientLogItemWriter : AcLogItemWriterBase, IAcLogWriterClientBase where TLogItem : class, IAcLogItemClient { private readonly HubConnection _hubConnection; private readonly string _fullHubName; // A log-csatorna a SAJÁT hibáit nem logolhatja önmagán át (rekurzió) → Console.Error. // Spam-védelem: hiba-állapotonként EGY jelentés; sikeres küldés után a kapu újraélesedik, és // helyreálláskor kiírjuk, hány log-item veszett el. (Egyszerű mezők — a torz dupla-jelentés ártalmatlan.) private bool _failureReported; private bool _connectedReported; private long _droppedCount; public AcSignaRClientLogItemWriter(string fullHubName, AppType appType = AppType.Web, LogLevel logLevel = LogLevel.Detail, string? categoryName = null, AcBinaryHubProtocolOptions? acBinaryProtocolOptions = null) : base(appType, logLevel, categoryName) { _fullHubName = fullHubName; // VISSZACSATOLÁS-VÉDELEM: erre a builderre TILOS app-loggert kötni (AddAcDefaults / ConfigureLogging) — // a protokoll saját logja a feltöltőn át újra ide érkezne (log → sorosítás → log → … robbanó hurok). // AcBinary-nál ezért a hívó az opciókban EXPLICIT süket loggert adjon (pl. NullLogger.Instance), // hogy az AddAcBinaryProtocol ??= DI-fallbackje se tölthesse fel. var hubBuilder = new HubConnectionBuilder().WithUrl(fullHubName); if (acBinaryProtocolOptions != null) hubBuilder.AddAcBinaryProtocol(acBinaryProtocolOptions); _hubConnection = hubBuilder //.WithAutomaticReconnect() .Build(); StartConnection().Forget(); } /// NEM dob: a kapcsolódási hibát maga jelenti (ReportFailureOnce) — a hívók (ctor fire-and-forget, /// WriteLogItemCallback) a State-ből látják az eredményt. public async Task StartConnection() { try { if (_hubConnection.State == HubConnectionState.Disconnected) await _hubConnection.StartAsync(); if (_hubConnection.State != HubConnectionState.Connected) await TaskHelper.WaitToAsync(() => _hubConnection.State == HubConnectionState.Connected, 10000, 10, 25); // Felépülés-marker (kapcsolatonként egyszer) — enélkül a „minden csendben OK" és a „soha el sem indult" // megkülönböztethetetlen a konzolból. if (_hubConnection.State == HubConnectionState.Connected && !_connectedReported) { _connectedReported = true; Console.WriteLine($"[{nameof(AcSignaRClientLogItemWriter)}] connected; url: {_fullHubName}"); } } catch (Exception ex) { ReportFailureOnce("connect failed", ex); } } public async Task StopConnection() { await _hubConnection.StopAsync(); await _hubConnection.DisposeAsync(); } public override void Write(AppType appType, LogLevel logLevel, string? logText, string? callerMemberName, string? categoryName, string? errorType, string? exMessage) => WriteLogItem(CreateLogItem(DateTime.UtcNow, appType, Environment.CurrentManagedThreadId, logLevel, logText, callerMemberName, categoryName, errorType, exMessage)); protected override async void WriteLogItemCallback(TLogItem logItem) { // Teljes try/catch: async void — egy StartAsync/SendAsync-kivétel különben kezeletlenül szökne el. try { //Ez fontos, mert a signalR nem küldi a DateTime.Kind-ot! A szeró oldalon kap egy utc DateTime-ot, ami a kliens lokális DateTime-ját tartalmazza! - J. logItem.TimeStampUtc = logItem.TimeStampUtc.ToUniversalTime(); await StartConnection(); if (_hubConnection.State != HubConnectionState.Connected) { _droppedCount++; ReportFailureOnce($"connection not available ({_hubConnection.State}), log item DROPPED", null); return; } //var refreshToken = Setting.UserBasicDetails?.RefreshToken ?? Guid.NewGuid().ToString("N"); await _hubConnection.SendAsync("AddLogItem", /*refreshToken,*/ logItem); ReportRecoveryIfNeeded(); } catch (Exception ex) { _droppedCount++; ReportFailureOnce("send failed, log item DROPPED", ex); } } /// Hiba-állapotonként EGYSZER ír a Console.Error-ra (nem spamel) — a következő sikeres küldés élesíti újra. private void ReportFailureOnce(string message, Exception? ex) { _connectedReported = false; // a következő sikeres felépülés újra jelentkezhessen if (_failureReported) return; _failureReported = true; Console.Error.WriteLine($"[{nameof(AcSignaRClientLogItemWriter)}] {message}; url: {_fullHubName}; droppedCount: {_droppedCount}{(ex == null ? string.Empty : $"; ex: {ex.GetType().Name}: {ex.Message}")}"); } /// Sikeres küldésnél: ha hiba-állapotból jöttünk vissza, jelentjük a kiesés alatt elveszett logok számát. private void ReportRecoveryIfNeeded() { if (!_failureReported) return; _failureReported = false; Console.Error.WriteLine($"[{nameof(AcSignaRClientLogItemWriter)}] connection recovered; url: {_fullHubName}; DROPPED items while down: {_droppedCount}"); _droppedCount = 0; } } }