feat(metrics): Add new histograms for chunk latency, results, and sources in AdvisoryAiMetrics
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
feat(telemetry): Record chunk latency, result count, and source count in AdvisoryAiTelemetry fix(endpoint): Include telemetry source count in advisory chunks endpoint response test(metrics): Enhance WebServiceEndpointsTests to validate new metrics for chunk latency, results, and sources refactor(tests): Update test utilities for Deno language analyzer tests chore(tests): Add performance tests for AdvisoryGuardrail with scenarios and blocked phrases docs: Archive Sprint 137 design document for scanner and surface enhancements
This commit is contained in:
@@ -24,6 +24,21 @@ internal static class AdvisoryAiMetrics
|
||||
unit: "count",
|
||||
description: "Number of advisory chunk segments blocked by guardrails.");
|
||||
|
||||
internal static readonly Histogram<double> ChunkLatencyHistogram = Meter.CreateHistogram<double>(
|
||||
"advisory_ai_chunk_latency_milliseconds",
|
||||
unit: "ms",
|
||||
description: "Elapsed time required to assemble advisory chunks.");
|
||||
|
||||
internal static readonly Histogram<long> ChunkResultHistogram = Meter.CreateHistogram<long>(
|
||||
"advisory_ai_chunk_segments",
|
||||
unit: "chunks",
|
||||
description: "Number of chunk segments returned to the caller per request.");
|
||||
|
||||
internal static readonly Histogram<long> ChunkSourceHistogram = Meter.CreateHistogram<long>(
|
||||
"advisory_ai_chunk_sources",
|
||||
unit: "sources",
|
||||
description: "Number of advisory sources contributing to a chunk response.");
|
||||
|
||||
internal static KeyValuePair<string, object?>[] BuildChunkRequestTags(string tenant, string result, bool truncated, bool cacheHit)
|
||||
=> new[]
|
||||
{
|
||||
@@ -33,6 +48,30 @@ internal static class AdvisoryAiMetrics
|
||||
CreateTag("cache", cacheHit ? "hit" : "miss"),
|
||||
};
|
||||
|
||||
internal static KeyValuePair<string, object?>[] BuildLatencyTags(string tenant, string result, bool truncated, bool cacheHit)
|
||||
=> new[]
|
||||
{
|
||||
CreateTag("tenant", tenant),
|
||||
CreateTag("result", result),
|
||||
CreateTag("truncated", BoolToString(truncated)),
|
||||
CreateTag("cache", cacheHit ? "hit" : "miss"),
|
||||
};
|
||||
|
||||
internal static KeyValuePair<string, object?>[] BuildChunkResultTags(string tenant, string result, bool truncated)
|
||||
=> new[]
|
||||
{
|
||||
CreateTag("tenant", tenant),
|
||||
CreateTag("result", result),
|
||||
CreateTag("truncated", BoolToString(truncated)),
|
||||
};
|
||||
|
||||
internal static KeyValuePair<string, object?>[] BuildSourceTags(string tenant, string result)
|
||||
=> new[]
|
||||
{
|
||||
CreateTag("tenant", tenant),
|
||||
CreateTag("result", result)
|
||||
};
|
||||
|
||||
internal static KeyValuePair<string, object?>[] BuildCacheTags(string tenant, string outcome)
|
||||
=> new[]
|
||||
{
|
||||
|
||||
@@ -913,6 +913,7 @@ var advisoryChunksEndpoint = app.MapGet("/advisories/{advisoryKey}/chunks", asyn
|
||||
buildResult.Response.Truncated,
|
||||
cacheHit,
|
||||
observations.Length,
|
||||
buildResult.Telemetry.SourceCount,
|
||||
buildResult.Response.Chunks.Count,
|
||||
duration,
|
||||
guardrailCounts));
|
||||
|
||||
@@ -33,6 +33,18 @@ internal sealed class AdvisoryAiTelemetry : IAdvisoryAiTelemetry
|
||||
AdvisoryAiMetrics.ChunkRequestCounter.Add(1,
|
||||
AdvisoryAiMetrics.BuildChunkRequestTags(tenant, result, telemetry.Truncated, telemetry.CacheHit));
|
||||
|
||||
AdvisoryAiMetrics.ChunkLatencyHistogram.Record(
|
||||
telemetry.Duration.TotalMilliseconds,
|
||||
AdvisoryAiMetrics.BuildLatencyTags(tenant, result, telemetry.Truncated, telemetry.CacheHit));
|
||||
|
||||
AdvisoryAiMetrics.ChunkResultHistogram.Record(
|
||||
telemetry.ChunkCount,
|
||||
AdvisoryAiMetrics.BuildChunkResultTags(tenant, result, telemetry.Truncated));
|
||||
|
||||
AdvisoryAiMetrics.ChunkSourceHistogram.Record(
|
||||
telemetry.SourceCount,
|
||||
AdvisoryAiMetrics.BuildSourceTags(tenant, result));
|
||||
|
||||
if (telemetry.CacheHit)
|
||||
{
|
||||
AdvisoryAiMetrics.ChunkCacheHitCounter.Add(1,
|
||||
@@ -56,13 +68,15 @@ internal sealed class AdvisoryAiTelemetry : IAdvisoryAiTelemetry
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Advisory chunk request for tenant {Tenant} key {Key} returned {Chunks} chunks across {Sources} sources (truncated: {Truncated}, cacheHit: {CacheHit}, durationMs: {Duration}).",
|
||||
"Advisory chunk request for tenant {Tenant} key {Key} returned {Chunks} chunks across {Sources} sources (observationsLoaded: {Observations}, truncated: {Truncated}, cacheHit: {CacheHit}, guardrailBlocks: {GuardrailBlocks}, durationMs: {Duration}).",
|
||||
tenant,
|
||||
telemetry.AdvisoryKey,
|
||||
telemetry.ChunkCount,
|
||||
telemetry.SourceCount,
|
||||
telemetry.ObservationCount,
|
||||
telemetry.Truncated,
|
||||
telemetry.CacheHit,
|
||||
telemetry.TotalGuardrailBlocks,
|
||||
telemetry.Duration.TotalMilliseconds.ToString("F2", CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
@@ -118,6 +132,7 @@ internal sealed record AdvisoryAiChunkRequestTelemetry(
|
||||
bool Truncated,
|
||||
bool CacheHit,
|
||||
int ObservationCount,
|
||||
int SourceCount,
|
||||
int ChunkCount,
|
||||
TimeSpan Duration,
|
||||
IReadOnlyDictionary<AdvisoryChunkGuardrailReason, int> GuardrailCounts)
|
||||
|
||||
@@ -531,7 +531,14 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
|
||||
|
||||
var metrics = await CaptureMetricsAsync(
|
||||
AdvisoryAiMetrics.MeterName,
|
||||
new[] { "advisory_ai_chunk_requests_total", "advisory_ai_chunk_cache_hits_total" },
|
||||
new[]
|
||||
{
|
||||
"advisory_ai_chunk_requests_total",
|
||||
"advisory_ai_chunk_cache_hits_total",
|
||||
"advisory_ai_chunk_latency_milliseconds",
|
||||
"advisory_ai_chunk_segments",
|
||||
"advisory_ai_chunk_sources"
|
||||
},
|
||||
async () =>
|
||||
{
|
||||
const string url = "/advisories/CVE-2025-0001/chunks?tenant=tenant-a";
|
||||
@@ -556,6 +563,17 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
|
||||
var cacheHit = Assert.Single(cacheHitMeasurements!);
|
||||
Assert.Equal(1, cacheHit.Value);
|
||||
Assert.Equal("hit", GetTagValue(cacheHit, "result"));
|
||||
|
||||
Assert.True(metrics.TryGetValue("advisory_ai_chunk_latency_milliseconds", out var latencyMeasurements));
|
||||
Assert.Equal(2, latencyMeasurements!.Count);
|
||||
Assert.All(latencyMeasurements!, measurement => Assert.True(measurement.Value > 0));
|
||||
|
||||
Assert.True(metrics.TryGetValue("advisory_ai_chunk_segments", out var segmentMeasurements));
|
||||
Assert.Equal(2, segmentMeasurements!.Count);
|
||||
Assert.Contains(segmentMeasurements!, measurement => GetTagValue(measurement, "truncated") == "false");
|
||||
|
||||
Assert.True(metrics.TryGetValue("advisory_ai_chunk_sources", out var sourceMeasurements));
|
||||
Assert.Equal(2, sourceMeasurements!.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -2161,7 +2179,7 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
|
||||
}
|
||||
};
|
||||
|
||||
listener.SetMeasurementEventCallback<long>((instrument, measurement, tags, state) =>
|
||||
void RecordMeasurement(Instrument instrument, double measurement, ReadOnlySpan<KeyValuePair<string, object?>> tags)
|
||||
{
|
||||
if (!measurementMap.TryGetValue(instrument.Name, out var list))
|
||||
{
|
||||
@@ -2175,7 +2193,13 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
|
||||
}
|
||||
|
||||
list.Add(new MetricMeasurement(instrument.Name, measurement, tagDictionary));
|
||||
});
|
||||
}
|
||||
|
||||
listener.SetMeasurementEventCallback<long>((instrument, measurement, tags, state)
|
||||
=> RecordMeasurement(instrument, measurement, tags));
|
||||
|
||||
listener.SetMeasurementEventCallback<double>((instrument, measurement, tags, state)
|
||||
=> RecordMeasurement(instrument, measurement, tags));
|
||||
|
||||
listener.Start();
|
||||
try
|
||||
@@ -2239,7 +2263,7 @@ public sealed class WebServiceEndpointsTests : IAsyncLifetime
|
||||
return new JwtSecurityTokenHandler().WriteToken(token);
|
||||
}
|
||||
|
||||
private sealed record MetricMeasurement(string Instrument, long Value, IReadOnlyDictionary<string, object?> Tags);
|
||||
private sealed record MetricMeasurement(string Instrument, double Value, IReadOnlyDictionary<string, object?> Tags);
|
||||
|
||||
private sealed class DemoJob : IJob
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user