Add full profiles and latency tests
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user