using System; using System.Diagnostics; using System.Net.Http; using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Caching.Memory; namespace JobTrackerApi.Services { public sealed record SummarizerMetrics( bool Healthy, string? Model, double? HealthLatencyMs, int Requests, int CacheHits, int CacheMisses, int Failures, double? AverageLatencyMs, DateTimeOffset? LastSuccessAt, DateTimeOffset? LastFailureAt, string? LastError ); public interface ISummarizerService { Task SummarizeAsync(string text, int maxLength = 150, int minLength = 30); Task GetMetricsAsync(CancellationToken cancellationToken = default); } public class SummarizerService : ISummarizerService { private readonly IHttpClientFactory _httpFactory; private readonly IMemoryCache _cache; private readonly object _metricsLock = new(); private int _requests; private int _cacheHits; private int _cacheMisses; private int _failures; private long _totalLatencyTicks; private DateTimeOffset? _lastSuccessAt; private DateTimeOffset? _lastFailureAt; private string? _lastError; public SummarizerService(IHttpClientFactory httpFactory, IMemoryCache cache) { _httpFactory = httpFactory; _cache = cache; } public async Task SummarizeAsync(string text, int maxLength = 150, int minLength = 30) { if (string.IsNullOrWhiteSpace(text)) return null; var key = $"summ:{text.GetHashCode()}:{maxLength}:{minLength}"; Interlocked.Increment(ref _requests); if (_cache.TryGetValue(key, out var cached)) { Interlocked.Increment(ref _cacheHits); lock (_metricsLock) { _lastSuccessAt = DateTimeOffset.UtcNow; _lastError = null; } return cached; } Interlocked.Increment(ref _cacheMisses); var client = _httpFactory.CreateClient("summarizer"); var payload = JsonSerializer.Serialize(new { text, max_length = maxLength, min_length = minLength }); using var content = new StringContent(payload, Encoding.UTF8, "application/json"); var sw = Stopwatch.StartNew(); try { var res = await client.PostAsync("/summarize", content); sw.Stop(); Interlocked.Add(ref _totalLatencyTicks, sw.ElapsedTicks); if (!res.IsSuccessStatusCode) return null; using var stream = await res.Content.ReadAsStreamAsync(); using var doc = await JsonDocument.ParseAsync(stream); if (doc.RootElement.TryGetProperty("summary", out var el)) { var s = el.GetString(); if (!string.IsNullOrWhiteSpace(s)) _cache.Set(key, s, TimeSpan.FromHours(6)); lock (_metricsLock) { _lastSuccessAt = DateTimeOffset.UtcNow; _lastError = null; } return s; } return null; } catch (Exception ex) { sw.Stop(); Interlocked.Add(ref _totalLatencyTicks, sw.ElapsedTicks); Interlocked.Increment(ref _failures); lock (_metricsLock) { _lastFailureAt = DateTimeOffset.UtcNow; _lastError = ex.Message; } return null; } } public async Task GetMetricsAsync(CancellationToken cancellationToken = default) { var client = _httpFactory.CreateClient("summarizer"); string? model = null; double? healthLatencyMs = null; var healthy = false; string? healthError = null; try { var sw = Stopwatch.StartNew(); using var res = await client.GetAsync("/health", cancellationToken); sw.Stop(); healthLatencyMs = Math.Round(sw.Elapsed.TotalMilliseconds, 1); healthy = res.IsSuccessStatusCode; if (healthy) { using var stream = await res.Content.ReadAsStreamAsync(cancellationToken); using var doc = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken); if (doc.RootElement.TryGetProperty("model", out var modelEl)) { model = modelEl.GetString(); } } else { healthError = $"Health check returned {(int)res.StatusCode}."; } } catch (Exception ex) { healthError = ex.Message; } var requests = Volatile.Read(ref _requests); var cacheHits = Volatile.Read(ref _cacheHits); var cacheMisses = Volatile.Read(ref _cacheMisses); var failures = Volatile.Read(ref _failures); var totalLatencyTicks = Volatile.Read(ref _totalLatencyTicks); DateTimeOffset? lastSuccessAt; DateTimeOffset? lastFailureAt; string? lastError; lock (_metricsLock) { lastSuccessAt = _lastSuccessAt; lastFailureAt = _lastFailureAt; lastError = _lastError; } if (!healthy && !string.IsNullOrWhiteSpace(healthError)) { lastError = healthError; } double? averageLatencyMs = requests > 0 ? Math.Round(TimeSpan.FromTicks(totalLatencyTicks).TotalMilliseconds / requests, 1) : null; return new SummarizerMetrics( Healthy: healthy, Model: model, HealthLatencyMs: healthLatencyMs, Requests: requests, CacheHits: cacheHits, CacheMisses: cacheMisses, Failures: failures, AverageLatencyMs: averageLatencyMs, LastSuccessAt: lastSuccessAt, LastFailureAt: lastFailureAt, LastError: lastError ); } } }