Add analytics to summerizer app and dashboard updates
This commit is contained in:
@@ -1,21 +1,47 @@
|
||||
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<string?> SummarizeAsync(string text, int maxLength = 150, int minLength = 30);
|
||||
Task<SummarizerMetrics> 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)
|
||||
{
|
||||
@@ -28,15 +54,31 @@ namespace JobTrackerApi.Services
|
||||
if (string.IsNullOrWhiteSpace(text)) return null;
|
||||
|
||||
var key = $"summ:{text.GetHashCode()}:{maxLength}:{minLength}";
|
||||
if (_cache.TryGetValue<string>(key, out var cached)) return cached;
|
||||
Interlocked.Increment(ref _requests);
|
||||
|
||||
if (_cache.TryGetValue<string>(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();
|
||||
@@ -45,15 +87,103 @@ namespace JobTrackerApi.Services
|
||||
{
|
||||
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
|
||||
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<SummarizerMetrics> 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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user