Add full profiles and latency tests

This commit is contained in:
cesnimda
2026-03-22 12:06:25 +01:00
parent 91f6361055
commit 0fa481cab6
11 changed files with 704 additions and 103 deletions
+141
View File
@@ -6,6 +6,9 @@ using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace JobTrackerApi.Services
{
@@ -13,6 +16,11 @@ namespace JobTrackerApi.Services
bool Healthy,
string? Model,
double? HealthLatencyMs,
double? ProbeLatencyMs,
DateTimeOffset? LastProbeAt,
DateTimeOffset? LastProbeSuccessAt,
DateTimeOffset? LastProbeFailureAt,
int ProbeFailures,
int Requests,
int CacheHits,
int CacheMisses,
@@ -26,6 +34,7 @@ namespace JobTrackerApi.Services
public interface ISummarizerService
{
Task<string?> SummarizeAsync(string text, int maxLength = 150, int minLength = 30);
Task RunProbeAsync(CancellationToken cancellationToken = default);
Task<SummarizerMetrics> GetMetricsAsync(CancellationToken cancellationToken = default);
}
@@ -41,6 +50,11 @@ namespace JobTrackerApi.Services
private long _totalLatencyTicks;
private DateTimeOffset? _lastSuccessAt;
private DateTimeOffset? _lastFailureAt;
private double? _lastProbeLatencyMs;
private DateTimeOffset? _lastProbeAt;
private DateTimeOffset? _lastProbeSuccessAt;
private DateTimeOffset? _lastProbeFailureAt;
private int _probeFailures;
private string? _lastError;
public SummarizerService(IHttpClientFactory httpFactory, IMemoryCache cache)
@@ -111,6 +125,69 @@ namespace JobTrackerApi.Services
}
}
public async Task RunProbeAsync(CancellationToken cancellationToken = default)
{
const string probeText = "Summarizer latency probe for job tracker telemetry.";
var client = _httpFactory.CreateClient("summarizer");
var payload = JsonSerializer.Serialize(new { text = probeText, max_length = 48, min_length = 12 });
using var content = new StringContent(payload, Encoding.UTF8, "application/json");
var sw = Stopwatch.StartNew();
try
{
using var res = await client.PostAsync("/summarize", content, cancellationToken);
sw.Stop();
lock (_metricsLock)
{
_lastProbeAt = DateTimeOffset.UtcNow;
_lastProbeLatencyMs = Math.Round(sw.Elapsed.TotalMilliseconds, 1);
}
if (!res.IsSuccessStatusCode)
{
Interlocked.Increment(ref _probeFailures);
lock (_metricsLock)
{
_lastProbeFailureAt = DateTimeOffset.UtcNow;
_lastError = $"Probe returned {(int)res.StatusCode}.";
}
return;
}
using var stream = await res.Content.ReadAsStreamAsync(cancellationToken);
using var doc = await JsonDocument.ParseAsync(stream, cancellationToken: cancellationToken);
if (!doc.RootElement.TryGetProperty("summary", out var summaryEl) || string.IsNullOrWhiteSpace(summaryEl.GetString()))
{
Interlocked.Increment(ref _probeFailures);
lock (_metricsLock)
{
_lastProbeFailureAt = DateTimeOffset.UtcNow;
_lastError = "Probe returned an empty summary.";
}
return;
}
lock (_metricsLock)
{
_lastProbeSuccessAt = DateTimeOffset.UtcNow;
_lastError = null;
}
}
catch (Exception ex)
{
sw.Stop();
Interlocked.Increment(ref _probeFailures);
lock (_metricsLock)
{
_lastProbeAt = DateTimeOffset.UtcNow;
_lastProbeLatencyMs = Math.Round(sw.Elapsed.TotalMilliseconds, 1);
_lastProbeFailureAt = DateTimeOffset.UtcNow;
_lastError = ex.Message;
}
}
}
public async Task<SummarizerMetrics> GetMetricsAsync(CancellationToken cancellationToken = default)
{
var client = _httpFactory.CreateClient("summarizer");
@@ -154,11 +231,19 @@ namespace JobTrackerApi.Services
DateTimeOffset? lastSuccessAt;
DateTimeOffset? lastFailureAt;
double? probeLatencyMs;
DateTimeOffset? lastProbeAt;
DateTimeOffset? lastProbeSuccessAt;
DateTimeOffset? lastProbeFailureAt;
string? lastError;
lock (_metricsLock)
{
lastSuccessAt = _lastSuccessAt;
lastFailureAt = _lastFailureAt;
probeLatencyMs = _lastProbeLatencyMs;
lastProbeAt = _lastProbeAt;
lastProbeSuccessAt = _lastProbeSuccessAt;
lastProbeFailureAt = _lastProbeFailureAt;
lastError = _lastError;
}
@@ -175,6 +260,11 @@ namespace JobTrackerApi.Services
Healthy: healthy,
Model: model,
HealthLatencyMs: healthLatencyMs,
ProbeLatencyMs: probeLatencyMs,
LastProbeAt: lastProbeAt,
LastProbeSuccessAt: lastProbeSuccessAt,
LastProbeFailureAt: lastProbeFailureAt,
ProbeFailures: Volatile.Read(ref _probeFailures),
Requests: requests,
CacheHits: cacheHits,
CacheMisses: cacheMisses,
@@ -186,4 +276,55 @@ namespace JobTrackerApi.Services
);
}
}
public sealed class SummarizerProbeHostedService : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger<SummarizerProbeHostedService> _logger;
private readonly IConfiguration _cfg;
public SummarizerProbeHostedService(IServiceScopeFactory scopeFactory, ILogger<SummarizerProbeHostedService> logger, IConfiguration cfg)
{
_scopeFactory = scopeFactory;
_logger = logger;
_cfg = cfg;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var enabled = _cfg.GetValue("Summarizer:ProbeEnabled", true);
if (!enabled)
{
return;
}
var intervalSeconds = Math.Clamp(_cfg.GetValue("Summarizer:ProbeIntervalSeconds", 300), 30, 3600);
var initialDelaySeconds = Math.Clamp(_cfg.GetValue("Summarizer:ProbeInitialDelaySeconds", 15), 0, 600);
if (initialDelaySeconds > 0)
{
await Task.Delay(TimeSpan.FromSeconds(initialDelaySeconds), stoppingToken);
}
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(intervalSeconds));
do
{
try
{
using var scope = _scopeFactory.CreateScope();
var summarizer = scope.ServiceProvider.GetRequiredService<ISummarizerService>();
await summarizer.RunProbeAsync(stoppingToken);
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
break;
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Summarizer latency probe failed.");
}
}
while (await timer.WaitForNextTickAsync(stoppingToken));
}
}
}