consolidation of some of the modules, localization fixes, product advisories work, qa work
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
using System.Diagnostics.Metrics;
|
||||
|
||||
namespace StellaOps.Excititor.WebService.Telemetry;
|
||||
|
||||
internal sealed class ChunkTelemetry
|
||||
{
|
||||
private readonly Counter<long> _ingestedTotal;
|
||||
private readonly Histogram<long> _itemCount;
|
||||
private readonly Histogram<long> _payloadBytes;
|
||||
private readonly Histogram<double> _latencyMs;
|
||||
|
||||
public ChunkTelemetry(IMeterFactory meterFactory)
|
||||
{
|
||||
var meter = meterFactory.Create("StellaOps.Excititor.Chunks");
|
||||
_ingestedTotal = meter.CreateCounter<long>(
|
||||
name: "vex_chunks_ingested_total",
|
||||
unit: "chunks",
|
||||
description: "Chunks submitted to Excititor VEX ingestion.");
|
||||
_itemCount = meter.CreateHistogram<long>(
|
||||
name: "vex_chunks_item_count",
|
||||
unit: "items",
|
||||
description: "Item count per submitted chunk.");
|
||||
_payloadBytes = meter.CreateHistogram<long>(
|
||||
name: "vex_chunks_payload_bytes",
|
||||
unit: "bytes",
|
||||
description: "Payload size per submitted chunk.");
|
||||
_latencyMs = meter.CreateHistogram<double>(
|
||||
name: "vex_chunks_latency_ms",
|
||||
unit: "ms",
|
||||
description: "End-to-end processing latency per chunk request.");
|
||||
}
|
||||
|
||||
public void RecordIngested(string? tenant, string? source, string status, string? reason, long itemCount, long payloadBytes, double latencyMs)
|
||||
{
|
||||
var tags = new KeyValuePair<string, object?>[]
|
||||
{
|
||||
new("tenant", tenant ?? string.Empty),
|
||||
new("source", source ?? string.Empty),
|
||||
new("status", status),
|
||||
new("reason", string.IsNullOrWhiteSpace(reason) ? string.Empty : reason)
|
||||
};
|
||||
|
||||
var tagSpan = tags.AsSpan();
|
||||
_ingestedTotal.Add(1, tagSpan);
|
||||
_itemCount.Record(itemCount, tagSpan);
|
||||
_payloadBytes.Record(payloadBytes, tagSpan);
|
||||
_latencyMs.Record(latencyMs, tagSpan);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System.Diagnostics.Metrics;
|
||||
|
||||
namespace StellaOps.Excititor.WebService.Telemetry;
|
||||
|
||||
internal sealed class ConsoleTelemetry
|
||||
{
|
||||
public const string MeterName = "StellaOps.Excititor.Console";
|
||||
|
||||
private static readonly Meter Meter = new(MeterName);
|
||||
|
||||
public Counter<long> Requests { get; } = Meter.CreateCounter<long>("console.vex.requests");
|
||||
public Counter<long> CacheHits { get; } = Meter.CreateCounter<long>("console.vex.cache_hits");
|
||||
public Counter<long> CacheMisses { get; } = Meter.CreateCounter<long>("console.vex.cache_misses");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
|
||||
using StellaOps.Excititor.Core.Aoc;
|
||||
using StellaOps.Excititor.WebService.Contracts;
|
||||
using StellaOps.Excititor.WebService.Services;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Metrics;
|
||||
using System.Linq;
|
||||
|
||||
namespace StellaOps.Excititor.WebService.Telemetry;
|
||||
|
||||
internal static class EvidenceTelemetry
|
||||
{
|
||||
public const string MeterName = "StellaOps.Excititor.WebService.Evidence";
|
||||
|
||||
private static readonly Meter Meter = new(MeterName);
|
||||
|
||||
private static readonly Counter<long> ObservationRequestCounter =
|
||||
Meter.CreateCounter<long>(
|
||||
"excititor.vex.observation.requests",
|
||||
unit: "requests",
|
||||
description: "Number of observation projection requests handled by the evidence APIs.");
|
||||
|
||||
private static readonly Histogram<int> ObservationStatementHistogram =
|
||||
Meter.CreateHistogram<int>(
|
||||
"excititor.vex.observation.statement_count",
|
||||
unit: "statements",
|
||||
description: "Distribution of statements returned per observation projection request.");
|
||||
|
||||
private static readonly Counter<long> EvidenceRequestCounter =
|
||||
Meter.CreateCounter<long>(
|
||||
"excititor.vex.evidence.requests",
|
||||
unit: "requests",
|
||||
description: "Number of evidence chunk requests handled by the evidence APIs.");
|
||||
|
||||
private static readonly Histogram<int> EvidenceChunkHistogram =
|
||||
Meter.CreateHistogram<int>(
|
||||
"excititor.vex.evidence.chunk_count",
|
||||
unit: "chunks",
|
||||
description: "Distribution of evidence chunks streamed per request.");
|
||||
|
||||
private static readonly Counter<long> SignatureStatusCounter =
|
||||
Meter.CreateCounter<long>(
|
||||
"excititor.vex.signature.status",
|
||||
unit: "statements",
|
||||
description: "Signature verification status counts for observation statements.");
|
||||
|
||||
private static readonly Counter<long> GuardViolationCounter =
|
||||
Meter.CreateCounter<long>(
|
||||
"excititor.vex.aoc.guard_violations",
|
||||
unit: "violations",
|
||||
description: "Aggregated count of AOC guard violations detected by Excititor evidence APIs.");
|
||||
|
||||
public static void RecordObservationOutcome(string? tenant, string outcome, int returnedCount = 0, bool truncated = false)
|
||||
{
|
||||
var normalizedTenant = NormalizeTenant(tenant);
|
||||
var tags = new[]
|
||||
{
|
||||
new KeyValuePair<string, object?>("tenant", normalizedTenant),
|
||||
new KeyValuePair<string, object?>("outcome", outcome),
|
||||
new KeyValuePair<string, object?>("truncated", truncated),
|
||||
};
|
||||
|
||||
ObservationRequestCounter.Add(1, tags);
|
||||
|
||||
if (!string.Equals(outcome, "success", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ObservationStatementHistogram.Record(returnedCount, tags);
|
||||
}
|
||||
|
||||
public static void RecordChunkOutcome(string? tenant, string outcome, int chunkCount = 0, bool truncated = false)
|
||||
{
|
||||
var normalizedTenant = NormalizeTenant(tenant);
|
||||
var tags = new[]
|
||||
{
|
||||
new KeyValuePair<string, object?>("tenant", normalizedTenant),
|
||||
new KeyValuePair<string, object?>("outcome", outcome),
|
||||
new KeyValuePair<string, object?>("truncated", truncated),
|
||||
};
|
||||
|
||||
EvidenceRequestCounter.Add(1, tags);
|
||||
|
||||
if (!string.Equals(outcome, "success", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
EvidenceChunkHistogram.Record(chunkCount, tags);
|
||||
}
|
||||
|
||||
public static void RecordSignatureStatus(string? tenant, IReadOnlyList<VexObservationStatementProjection> statements)
|
||||
{
|
||||
if (statements is null || statements.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var normalizedTenant = NormalizeTenant(tenant);
|
||||
var missing = 0;
|
||||
var unverified = 0;
|
||||
var verified = 0;
|
||||
|
||||
foreach (var statement in statements)
|
||||
{
|
||||
var signature = statement.Signature;
|
||||
if (signature is null)
|
||||
{
|
||||
missing++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (signature.VerifiedAt is null)
|
||||
{
|
||||
unverified++;
|
||||
}
|
||||
else
|
||||
{
|
||||
verified++;
|
||||
}
|
||||
}
|
||||
|
||||
if (missing > 0)
|
||||
{
|
||||
SignatureStatusCounter.Add(
|
||||
missing,
|
||||
new[]
|
||||
{
|
||||
new KeyValuePair<string, object?>("tenant", normalizedTenant),
|
||||
new KeyValuePair<string, object?>("status", "missing"),
|
||||
});
|
||||
}
|
||||
|
||||
if (unverified > 0)
|
||||
{
|
||||
SignatureStatusCounter.Add(
|
||||
unverified,
|
||||
BuildSignatureTags(normalizedTenant, "unverified"));
|
||||
}
|
||||
|
||||
if (verified > 0)
|
||||
{
|
||||
SignatureStatusCounter.Add(
|
||||
verified,
|
||||
BuildSignatureTags(normalizedTenant, "verified"));
|
||||
}
|
||||
}
|
||||
|
||||
public static void RecordChunkSignatureStatus(string? tenant, IReadOnlyList<VexEvidenceChunkResponse> chunks)
|
||||
{
|
||||
if (chunks is null || chunks.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var normalizedTenant = NormalizeTenant(tenant);
|
||||
|
||||
var unsigned = 0;
|
||||
var unverified = 0;
|
||||
var verified = 0;
|
||||
|
||||
foreach (var chunk in chunks)
|
||||
{
|
||||
var signature = chunk.Signature;
|
||||
if (signature is null)
|
||||
{
|
||||
unsigned++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (signature.VerifiedAt is null)
|
||||
{
|
||||
unverified++;
|
||||
}
|
||||
else
|
||||
{
|
||||
verified++;
|
||||
}
|
||||
}
|
||||
|
||||
if (unsigned > 0)
|
||||
{
|
||||
SignatureStatusCounter.Add(unsigned, BuildSignatureTags(normalizedTenant, "unsigned"));
|
||||
}
|
||||
|
||||
if (unverified > 0)
|
||||
{
|
||||
SignatureStatusCounter.Add(unverified, BuildSignatureTags(normalizedTenant, "unverified"));
|
||||
}
|
||||
|
||||
if (verified > 0)
|
||||
{
|
||||
SignatureStatusCounter.Add(verified, BuildSignatureTags(normalizedTenant, "verified"));
|
||||
}
|
||||
}
|
||||
|
||||
public static void RecordGuardViolations(string? tenant, string surface, ExcititorAocGuardException exception)
|
||||
{
|
||||
var normalizedTenant = NormalizeTenant(tenant);
|
||||
var normalizedSurface = NormalizeSurface(surface);
|
||||
|
||||
if (exception.Violations.IsDefaultOrEmpty)
|
||||
{
|
||||
GuardViolationCounter.Add(
|
||||
1,
|
||||
new[]
|
||||
{
|
||||
new KeyValuePair<string, object?>("tenant", normalizedTenant),
|
||||
new KeyValuePair<string, object?>("surface", normalizedSurface),
|
||||
new KeyValuePair<string, object?>("code", exception.PrimaryErrorCode),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var violation in exception.Violations)
|
||||
{
|
||||
var code = string.IsNullOrWhiteSpace(violation.ErrorCode)
|
||||
? exception.PrimaryErrorCode
|
||||
: violation.ErrorCode;
|
||||
|
||||
GuardViolationCounter.Add(
|
||||
1,
|
||||
new[]
|
||||
{
|
||||
new KeyValuePair<string, object?>("tenant", normalizedTenant),
|
||||
new KeyValuePair<string, object?>("surface", normalizedSurface),
|
||||
new KeyValuePair<string, object?>("code", code),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static string NormalizeTenant(string? tenant)
|
||||
=> string.IsNullOrWhiteSpace(tenant) ? "default" : tenant;
|
||||
|
||||
private static string NormalizeSurface(string? surface)
|
||||
=> string.IsNullOrWhiteSpace(surface) ? "unknown" : surface.ToLowerInvariant();
|
||||
|
||||
private static KeyValuePair<string, object?>[] BuildSignatureTags(string tenant, string status)
|
||||
=> new[]
|
||||
{
|
||||
new KeyValuePair<string, object?>("tenant", tenant),
|
||||
new KeyValuePair<string, object?>("status", status),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
|
||||
using StellaOps.Excititor.Core.Observations;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Metrics;
|
||||
|
||||
namespace StellaOps.Excititor.WebService.Telemetry;
|
||||
|
||||
/// <summary>
|
||||
/// Telemetry metrics for VEX linkset and observation store operations (EXCITITOR-OBS-51-001).
|
||||
/// Tracks ingest latency, scope resolution success, conflict rate, and signature verification
|
||||
/// to support SLO burn alerts for AOC "evidence freshness" mission.
|
||||
/// </summary>
|
||||
internal static class LinksetTelemetry
|
||||
{
|
||||
public const string MeterName = "StellaOps.Excititor.WebService.Linksets";
|
||||
|
||||
private static readonly Meter Meter = new(MeterName);
|
||||
|
||||
// Ingest latency metrics
|
||||
private static readonly Histogram<double> IngestLatencyHistogram =
|
||||
Meter.CreateHistogram<double>(
|
||||
"excititor.vex.ingest.latency_seconds",
|
||||
unit: "s",
|
||||
description: "Latency distribution for VEX observation and linkset store operations.");
|
||||
|
||||
private static readonly Counter<long> IngestOperationCounter =
|
||||
Meter.CreateCounter<long>(
|
||||
"excititor.vex.ingest.operations_total",
|
||||
unit: "operations",
|
||||
description: "Total count of VEX ingest operations by outcome.");
|
||||
|
||||
// Scope resolution metrics
|
||||
private static readonly Counter<long> ScopeResolutionCounter =
|
||||
Meter.CreateCounter<long>(
|
||||
"excititor.vex.scope.resolution_total",
|
||||
unit: "resolutions",
|
||||
description: "Count of scope resolution attempts by outcome (success/failure).");
|
||||
|
||||
private static readonly Histogram<int> ScopeMatchCountHistogram =
|
||||
Meter.CreateHistogram<int>(
|
||||
"excititor.vex.scope.match_count",
|
||||
unit: "matches",
|
||||
description: "Distribution of matched scopes per resolution request.");
|
||||
|
||||
// Conflict/disagreement metrics
|
||||
private static readonly Counter<long> LinksetConflictCounter =
|
||||
Meter.CreateCounter<long>(
|
||||
"excititor.vex.linkset.conflicts_total",
|
||||
unit: "conflicts",
|
||||
description: "Total count of linksets with provider disagreements detected.");
|
||||
|
||||
private static readonly Histogram<int> DisagreementCountHistogram =
|
||||
Meter.CreateHistogram<int>(
|
||||
"excititor.vex.linkset.disagreement_count",
|
||||
unit: "disagreements",
|
||||
description: "Distribution of disagreement count per linkset.");
|
||||
|
||||
private static readonly Counter<long> DisagreementByStatusCounter =
|
||||
Meter.CreateCounter<long>(
|
||||
"excititor.vex.linkset.disagreement_by_status",
|
||||
unit: "disagreements",
|
||||
description: "Disagreement counts broken down by conflicting status values.");
|
||||
|
||||
// Observation store metrics
|
||||
private static readonly Counter<long> ObservationStoreCounter =
|
||||
Meter.CreateCounter<long>(
|
||||
"excititor.vex.observation.store_operations_total",
|
||||
unit: "operations",
|
||||
description: "Total observation store operations by type and outcome.");
|
||||
|
||||
private static readonly Histogram<int> ObservationBatchSizeHistogram =
|
||||
Meter.CreateHistogram<int>(
|
||||
"excititor.vex.observation.batch_size",
|
||||
unit: "observations",
|
||||
description: "Distribution of observation batch sizes for store operations.");
|
||||
|
||||
// Linkset store metrics
|
||||
private static readonly Counter<long> LinksetStoreCounter =
|
||||
Meter.CreateCounter<long>(
|
||||
"excititor.vex.linkset.store_operations_total",
|
||||
unit: "operations",
|
||||
description: "Total linkset store operations by type and outcome.");
|
||||
|
||||
// Confidence metrics
|
||||
private static readonly Histogram<double> LinksetConfidenceHistogram =
|
||||
Meter.CreateHistogram<double>(
|
||||
"excititor.vex.linkset.confidence_score",
|
||||
unit: "score",
|
||||
description: "Distribution of linkset confidence scores (0.0-1.0).");
|
||||
|
||||
/// <summary>
|
||||
/// Records latency for a VEX ingest operation.
|
||||
/// </summary>
|
||||
public static void RecordIngestLatency(string? tenant, string operation, string outcome, double latencySeconds)
|
||||
{
|
||||
var tags = BuildBaseTags(tenant, operation, outcome);
|
||||
IngestLatencyHistogram.Record(latencySeconds, tags);
|
||||
IngestOperationCounter.Add(1, tags);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records a scope resolution attempt and its outcome.
|
||||
/// </summary>
|
||||
public static void RecordScopeResolution(string? tenant, string outcome, int matchCount = 0)
|
||||
{
|
||||
var normalizedTenant = NormalizeTenant(tenant);
|
||||
var tags = new[]
|
||||
{
|
||||
new KeyValuePair<string, object?>("tenant", normalizedTenant),
|
||||
new KeyValuePair<string, object?>("outcome", outcome),
|
||||
};
|
||||
|
||||
ScopeResolutionCounter.Add(1, tags);
|
||||
|
||||
if (string.Equals(outcome, "success", StringComparison.OrdinalIgnoreCase) && matchCount > 0)
|
||||
{
|
||||
ScopeMatchCountHistogram.Record(matchCount, tags);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records conflict detection for a linkset.
|
||||
/// </summary>
|
||||
public static void RecordLinksetConflict(string? tenant, bool hasConflicts, int disagreementCount = 0)
|
||||
{
|
||||
var normalizedTenant = NormalizeTenant(tenant);
|
||||
|
||||
if (hasConflicts)
|
||||
{
|
||||
var conflictTags = new[]
|
||||
{
|
||||
new KeyValuePair<string, object?>("tenant", normalizedTenant),
|
||||
};
|
||||
LinksetConflictCounter.Add(1, conflictTags);
|
||||
|
||||
if (disagreementCount > 0)
|
||||
{
|
||||
DisagreementCountHistogram.Record(disagreementCount, conflictTags);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records a linkset with detailed disagreement breakdown.
|
||||
/// </summary>
|
||||
public static void RecordLinksetDisagreements(string? tenant, VexLinkset linkset)
|
||||
{
|
||||
if (linkset is null || !linkset.HasConflicts)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var normalizedTenant = NormalizeTenant(tenant);
|
||||
RecordLinksetConflict(normalizedTenant, true, linkset.Disagreements.Length);
|
||||
|
||||
// Record disagreements by status
|
||||
foreach (var disagreement in linkset.Disagreements)
|
||||
{
|
||||
var statusTags = new[]
|
||||
{
|
||||
new KeyValuePair<string, object?>("tenant", normalizedTenant),
|
||||
new KeyValuePair<string, object?>("status", disagreement.Status.ToLowerInvariant()),
|
||||
new KeyValuePair<string, object?>("provider", disagreement.ProviderId),
|
||||
};
|
||||
DisagreementByStatusCounter.Add(1, statusTags);
|
||||
}
|
||||
|
||||
// Record confidence score
|
||||
var confidenceScore = linkset.Confidence switch
|
||||
{
|
||||
VexLinksetConfidence.High => 0.9,
|
||||
VexLinksetConfidence.Medium => 0.7,
|
||||
VexLinksetConfidence.Low => 0.4,
|
||||
_ => 0.5
|
||||
};
|
||||
|
||||
var confidenceTags = new[]
|
||||
{
|
||||
new KeyValuePair<string, object?>("tenant", normalizedTenant),
|
||||
new KeyValuePair<string, object?>("has_conflicts", linkset.HasConflicts),
|
||||
};
|
||||
LinksetConfidenceHistogram.Record(confidenceScore, confidenceTags);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records an observation store operation.
|
||||
/// </summary>
|
||||
public static void RecordObservationStoreOperation(
|
||||
string? tenant,
|
||||
string operation,
|
||||
string outcome,
|
||||
int batchSize = 1)
|
||||
{
|
||||
var tags = BuildBaseTags(tenant, operation, outcome);
|
||||
ObservationStoreCounter.Add(1, tags);
|
||||
|
||||
if (batchSize > 0 && string.Equals(outcome, "success", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var batchTags = new[]
|
||||
{
|
||||
new KeyValuePair<string, object?>("tenant", NormalizeTenant(tenant)),
|
||||
new KeyValuePair<string, object?>("operation", operation),
|
||||
};
|
||||
ObservationBatchSizeHistogram.Record(batchSize, batchTags);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records a linkset store operation.
|
||||
/// </summary>
|
||||
public static void RecordLinksetStoreOperation(string? tenant, string operation, string outcome)
|
||||
{
|
||||
var tags = BuildBaseTags(tenant, operation, outcome);
|
||||
LinksetStoreCounter.Add(1, tags);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records linkset confidence score distribution.
|
||||
/// </summary>
|
||||
public static void RecordLinksetConfidence(string? tenant, VexLinksetConfidence confidence, bool hasConflicts)
|
||||
{
|
||||
var score = confidence switch
|
||||
{
|
||||
VexLinksetConfidence.High => 0.9,
|
||||
VexLinksetConfidence.Medium => 0.7,
|
||||
VexLinksetConfidence.Low => 0.4,
|
||||
_ => 0.5
|
||||
};
|
||||
|
||||
var tags = new[]
|
||||
{
|
||||
new KeyValuePair<string, object?>("tenant", NormalizeTenant(tenant)),
|
||||
new KeyValuePair<string, object?>("has_conflicts", hasConflicts),
|
||||
new KeyValuePair<string, object?>("confidence_level", confidence.ToString().ToLowerInvariant()),
|
||||
};
|
||||
|
||||
LinksetConfidenceHistogram.Record(score, tags);
|
||||
}
|
||||
|
||||
private static string NormalizeTenant(string? tenant)
|
||||
=> string.IsNullOrWhiteSpace(tenant) ? "default" : tenant;
|
||||
|
||||
private static KeyValuePair<string, object?>[] BuildBaseTags(string? tenant, string operation, string outcome)
|
||||
=> new[]
|
||||
{
|
||||
new KeyValuePair<string, object?>("tenant", NormalizeTenant(tenant)),
|
||||
new KeyValuePair<string, object?>("operation", operation),
|
||||
new KeyValuePair<string, object?>("outcome", outcome),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
|
||||
using StellaOps.Excititor.Core.Canonicalization;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.Metrics;
|
||||
|
||||
namespace StellaOps.Excititor.WebService.Telemetry;
|
||||
|
||||
/// <summary>
|
||||
/// Telemetry metrics for VEX normalization and canonicalization operations (EXCITITOR-VULN-29-004).
|
||||
/// Tracks advisory/product key canonicalization, normalization errors, suppression scopes,
|
||||
/// and withdrawn statement handling for Vuln Explorer and Advisory AI dashboards.
|
||||
/// </summary>
|
||||
internal static class NormalizationTelemetry
|
||||
{
|
||||
public const string MeterName = "StellaOps.Excititor.WebService.Normalization";
|
||||
|
||||
private static readonly Meter Meter = new(MeterName);
|
||||
|
||||
// Advisory key canonicalization metrics
|
||||
private static readonly Counter<long> AdvisoryKeyCanonicalizeCounter =
|
||||
Meter.CreateCounter<long>(
|
||||
"excititor.vex.canonicalize.advisory_key_total",
|
||||
unit: "operations",
|
||||
description: "Total advisory key canonicalization operations by outcome.");
|
||||
|
||||
private static readonly Counter<long> AdvisoryKeyCanonicalizeErrorCounter =
|
||||
Meter.CreateCounter<long>(
|
||||
"excititor.vex.canonicalize.advisory_key_errors",
|
||||
unit: "errors",
|
||||
description: "Advisory key canonicalization errors by error type.");
|
||||
|
||||
private static readonly Counter<long> AdvisoryKeyScopeCounter =
|
||||
Meter.CreateCounter<long>(
|
||||
"excititor.vex.canonicalize.advisory_key_scope",
|
||||
unit: "keys",
|
||||
description: "Advisory keys processed by scope (global, ecosystem, vendor, distribution, unknown).");
|
||||
|
||||
// Product key canonicalization metrics
|
||||
private static readonly Counter<long> ProductKeyCanonicalizeCounter =
|
||||
Meter.CreateCounter<long>(
|
||||
"excititor.vex.canonicalize.product_key_total",
|
||||
unit: "operations",
|
||||
description: "Total product key canonicalization operations by outcome.");
|
||||
|
||||
private static readonly Counter<long> ProductKeyCanonicalizeErrorCounter =
|
||||
Meter.CreateCounter<long>(
|
||||
"excititor.vex.canonicalize.product_key_errors",
|
||||
unit: "errors",
|
||||
description: "Product key canonicalization errors by error type.");
|
||||
|
||||
private static readonly Counter<long> ProductKeyScopeCounter =
|
||||
Meter.CreateCounter<long>(
|
||||
"excititor.vex.canonicalize.product_key_scope",
|
||||
unit: "keys",
|
||||
description: "Product keys processed by scope (package, component, ospackage, container, platform, unknown).");
|
||||
|
||||
private static readonly Counter<long> ProductKeyTypeCounter =
|
||||
Meter.CreateCounter<long>(
|
||||
"excititor.vex.canonicalize.product_key_type",
|
||||
unit: "keys",
|
||||
description: "Product keys processed by type (purl, cpe, rpm, deb, oci, platform, other).");
|
||||
|
||||
// Evidence retrieval metrics
|
||||
private static readonly Counter<long> EvidenceRetrievalCounter =
|
||||
Meter.CreateCounter<long>(
|
||||
"excititor.vex.evidence.retrieval_total",
|
||||
unit: "requests",
|
||||
description: "Total evidence retrieval requests by outcome.");
|
||||
|
||||
private static readonly Histogram<int> EvidenceStatementCountHistogram =
|
||||
Meter.CreateHistogram<int>(
|
||||
"excititor.vex.evidence.statement_count",
|
||||
unit: "statements",
|
||||
description: "Distribution of statements returned per evidence retrieval request.");
|
||||
|
||||
private static readonly Histogram<double> EvidenceRetrievalLatencyHistogram =
|
||||
Meter.CreateHistogram<double>(
|
||||
"excititor.vex.evidence.retrieval_latency_seconds",
|
||||
unit: "s",
|
||||
description: "Latency distribution for evidence retrieval operations.");
|
||||
|
||||
// Normalization error metrics
|
||||
private static readonly Counter<long> NormalizationErrorCounter =
|
||||
Meter.CreateCounter<long>(
|
||||
"excititor.vex.normalize.errors_total",
|
||||
unit: "errors",
|
||||
description: "Total normalization errors by type and provider.");
|
||||
|
||||
// Suppression scope metrics
|
||||
private static readonly Counter<long> SuppressionScopeCounter =
|
||||
Meter.CreateCounter<long>(
|
||||
"excititor.vex.suppression.scope_total",
|
||||
unit: "suppressions",
|
||||
description: "Suppression scope applications by scope type.");
|
||||
|
||||
private static readonly Counter<long> SuppressionAppliedCounter =
|
||||
Meter.CreateCounter<long>(
|
||||
"excititor.vex.suppression.applied_total",
|
||||
unit: "statements",
|
||||
description: "Statements affected by suppression scopes.");
|
||||
|
||||
// Withdrawn statement metrics
|
||||
private static readonly Counter<long> WithdrawnStatementCounter =
|
||||
Meter.CreateCounter<long>(
|
||||
"excititor.vex.withdrawn.statements_total",
|
||||
unit: "statements",
|
||||
description: "Total withdrawn statement detections by provider.");
|
||||
|
||||
private static readonly Counter<long> WithdrawnReplacementCounter =
|
||||
Meter.CreateCounter<long>(
|
||||
"excititor.vex.withdrawn.replacements_total",
|
||||
unit: "replacements",
|
||||
description: "Withdrawn statement replacements processed.");
|
||||
|
||||
/// <summary>
|
||||
/// Records a successful advisory key canonicalization.
|
||||
/// </summary>
|
||||
public static void RecordAdvisoryKeyCanonicalization(
|
||||
string? tenant,
|
||||
VexCanonicalAdvisoryKey result)
|
||||
{
|
||||
var normalizedTenant = NormalizeTenant(tenant);
|
||||
var scope = result.Scope.ToString().ToLowerInvariant();
|
||||
|
||||
AdvisoryKeyCanonicalizeCounter.Add(1, BuildOutcomeTags(normalizedTenant, "success"));
|
||||
AdvisoryKeyScopeCounter.Add(1, BuildScopeTags(normalizedTenant, scope));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records an advisory key canonicalization error.
|
||||
/// </summary>
|
||||
public static void RecordAdvisoryKeyCanonicalizeError(
|
||||
string? tenant,
|
||||
string errorType,
|
||||
string? advisoryKey = null)
|
||||
{
|
||||
var normalizedTenant = NormalizeTenant(tenant);
|
||||
var tags = new[]
|
||||
{
|
||||
new KeyValuePair<string, object?>("tenant", normalizedTenant),
|
||||
new KeyValuePair<string, object?>("error_type", errorType),
|
||||
};
|
||||
|
||||
AdvisoryKeyCanonicalizeCounter.Add(1, BuildOutcomeTags(normalizedTenant, "error"));
|
||||
AdvisoryKeyCanonicalizeErrorCounter.Add(1, tags);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records a successful product key canonicalization.
|
||||
/// </summary>
|
||||
public static void RecordProductKeyCanonicalization(
|
||||
string? tenant,
|
||||
VexCanonicalProductKey result)
|
||||
{
|
||||
var normalizedTenant = NormalizeTenant(tenant);
|
||||
var scope = result.Scope.ToString().ToLowerInvariant();
|
||||
var keyType = result.KeyType.ToString().ToLowerInvariant();
|
||||
|
||||
ProductKeyCanonicalizeCounter.Add(1, BuildOutcomeTags(normalizedTenant, "success"));
|
||||
ProductKeyScopeCounter.Add(1, BuildScopeTags(normalizedTenant, scope));
|
||||
ProductKeyTypeCounter.Add(1, new[]
|
||||
{
|
||||
new KeyValuePair<string, object?>("tenant", normalizedTenant),
|
||||
new KeyValuePair<string, object?>("key_type", keyType),
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records a product key canonicalization error.
|
||||
/// </summary>
|
||||
public static void RecordProductKeyCanonicalizeError(
|
||||
string? tenant,
|
||||
string errorType,
|
||||
string? productKey = null)
|
||||
{
|
||||
var normalizedTenant = NormalizeTenant(tenant);
|
||||
var tags = new[]
|
||||
{
|
||||
new KeyValuePair<string, object?>("tenant", normalizedTenant),
|
||||
new KeyValuePair<string, object?>("error_type", errorType),
|
||||
};
|
||||
|
||||
ProductKeyCanonicalizeCounter.Add(1, BuildOutcomeTags(normalizedTenant, "error"));
|
||||
ProductKeyCanonicalizeErrorCounter.Add(1, tags);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records an evidence retrieval operation.
|
||||
/// </summary>
|
||||
public static void RecordEvidenceRetrieval(
|
||||
string? tenant,
|
||||
string outcome,
|
||||
int statementCount,
|
||||
double latencySeconds)
|
||||
{
|
||||
var normalizedTenant = NormalizeTenant(tenant);
|
||||
var tags = BuildOutcomeTags(normalizedTenant, outcome);
|
||||
|
||||
EvidenceRetrievalCounter.Add(1, tags);
|
||||
|
||||
if (string.Equals(outcome, "success", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
EvidenceStatementCountHistogram.Record(statementCount, tags);
|
||||
}
|
||||
|
||||
EvidenceRetrievalLatencyHistogram.Record(latencySeconds, tags);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records a normalization error.
|
||||
/// </summary>
|
||||
public static void RecordNormalizationError(
|
||||
string? tenant,
|
||||
string provider,
|
||||
string errorType,
|
||||
string? detail = null)
|
||||
{
|
||||
var normalizedTenant = NormalizeTenant(tenant);
|
||||
var tags = new[]
|
||||
{
|
||||
new KeyValuePair<string, object?>("tenant", normalizedTenant),
|
||||
new KeyValuePair<string, object?>("provider", string.IsNullOrWhiteSpace(provider) ? "unknown" : provider),
|
||||
new KeyValuePair<string, object?>("error_type", errorType),
|
||||
};
|
||||
|
||||
NormalizationErrorCounter.Add(1, tags);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records a suppression scope application.
|
||||
/// </summary>
|
||||
public static void RecordSuppressionScope(
|
||||
string? tenant,
|
||||
string scopeType,
|
||||
int affectedStatements)
|
||||
{
|
||||
var normalizedTenant = NormalizeTenant(tenant);
|
||||
var scopeTags = new[]
|
||||
{
|
||||
new KeyValuePair<string, object?>("tenant", normalizedTenant),
|
||||
new KeyValuePair<string, object?>("scope_type", scopeType),
|
||||
};
|
||||
|
||||
SuppressionScopeCounter.Add(1, scopeTags);
|
||||
|
||||
if (affectedStatements > 0)
|
||||
{
|
||||
SuppressionAppliedCounter.Add(affectedStatements, scopeTags);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records a withdrawn statement detection.
|
||||
/// </summary>
|
||||
public static void RecordWithdrawnStatement(
|
||||
string? tenant,
|
||||
string provider,
|
||||
string? replacementId = null)
|
||||
{
|
||||
var normalizedTenant = NormalizeTenant(tenant);
|
||||
var tags = new[]
|
||||
{
|
||||
new KeyValuePair<string, object?>("tenant", normalizedTenant),
|
||||
new KeyValuePair<string, object?>("provider", string.IsNullOrWhiteSpace(provider) ? "unknown" : provider),
|
||||
};
|
||||
|
||||
WithdrawnStatementCounter.Add(1, tags);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(replacementId))
|
||||
{
|
||||
WithdrawnReplacementCounter.Add(1, tags);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records batch withdrawn statement processing.
|
||||
/// </summary>
|
||||
public static void RecordWithdrawnStatements(
|
||||
string? tenant,
|
||||
string provider,
|
||||
int totalWithdrawn,
|
||||
int replacements)
|
||||
{
|
||||
var normalizedTenant = NormalizeTenant(tenant);
|
||||
var tags = new[]
|
||||
{
|
||||
new KeyValuePair<string, object?>("tenant", normalizedTenant),
|
||||
new KeyValuePair<string, object?>("provider", string.IsNullOrWhiteSpace(provider) ? "unknown" : provider),
|
||||
};
|
||||
|
||||
if (totalWithdrawn > 0)
|
||||
{
|
||||
WithdrawnStatementCounter.Add(totalWithdrawn, tags);
|
||||
}
|
||||
|
||||
if (replacements > 0)
|
||||
{
|
||||
WithdrawnReplacementCounter.Add(replacements, tags);
|
||||
}
|
||||
}
|
||||
|
||||
private static string NormalizeTenant(string? tenant)
|
||||
=> string.IsNullOrWhiteSpace(tenant) ? "default" : tenant;
|
||||
|
||||
private static KeyValuePair<string, object?>[] BuildOutcomeTags(string tenant, string outcome)
|
||||
=> new[]
|
||||
{
|
||||
new KeyValuePair<string, object?>("tenant", tenant),
|
||||
new KeyValuePair<string, object?>("outcome", outcome),
|
||||
};
|
||||
|
||||
private static KeyValuePair<string, object?>[] BuildScopeTags(string tenant, string scope)
|
||||
=> new[]
|
||||
{
|
||||
new KeyValuePair<string, object?>("tenant", tenant),
|
||||
new KeyValuePair<string, object?>("scope", scope),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Excititor.Core;
|
||||
|
||||
namespace StellaOps.Excititor.WebService.Telemetry;
|
||||
|
||||
/// <summary>
|
||||
/// Implementation of <see cref="IVexNormalizationTelemetryRecorder"/> that bridges to
|
||||
/// <see cref="NormalizationTelemetry"/> static metrics and structured logging (EXCITITOR-VULN-29-004).
|
||||
/// </summary>
|
||||
internal sealed class VexNormalizationTelemetryRecorder : IVexNormalizationTelemetryRecorder
|
||||
{
|
||||
private readonly ILogger<VexNormalizationTelemetryRecorder> _logger;
|
||||
|
||||
public VexNormalizationTelemetryRecorder(ILogger<VexNormalizationTelemetryRecorder> logger)
|
||||
{
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public void RecordNormalizationError(string? tenant, string provider, string errorType, string? detail = null)
|
||||
{
|
||||
NormalizationTelemetry.RecordNormalizationError(tenant, provider, errorType, detail);
|
||||
|
||||
_logger.LogWarning(
|
||||
"VEX normalization error: tenant={Tenant} provider={Provider} errorType={ErrorType} detail={Detail}",
|
||||
tenant ?? "default",
|
||||
provider,
|
||||
errorType,
|
||||
detail ?? "(none)");
|
||||
}
|
||||
|
||||
public void RecordSuppressionScope(string? tenant, string scopeType, int affectedStatements)
|
||||
{
|
||||
NormalizationTelemetry.RecordSuppressionScope(tenant, scopeType, affectedStatements);
|
||||
|
||||
if (affectedStatements > 0)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"VEX suppression scope applied: tenant={Tenant} scopeType={ScopeType} affectedStatements={AffectedStatements}",
|
||||
tenant ?? "default",
|
||||
scopeType,
|
||||
affectedStatements);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"VEX suppression scope checked (no statements affected): tenant={Tenant} scopeType={ScopeType}",
|
||||
tenant ?? "default",
|
||||
scopeType);
|
||||
}
|
||||
}
|
||||
|
||||
public void RecordWithdrawnStatement(string? tenant, string provider, string? replacementId = null)
|
||||
{
|
||||
NormalizationTelemetry.RecordWithdrawnStatement(tenant, provider, replacementId);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(replacementId))
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"VEX withdrawn statement detected: tenant={Tenant} provider={Provider}",
|
||||
tenant ?? "default",
|
||||
provider);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"VEX withdrawn statement superseded: tenant={Tenant} provider={Provider} replacementId={ReplacementId}",
|
||||
tenant ?? "default",
|
||||
provider,
|
||||
replacementId);
|
||||
}
|
||||
}
|
||||
|
||||
public void RecordWithdrawnStatements(string? tenant, string provider, int totalWithdrawn, int replacements)
|
||||
{
|
||||
NormalizationTelemetry.RecordWithdrawnStatements(tenant, provider, totalWithdrawn, replacements);
|
||||
|
||||
if (totalWithdrawn > 0)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"VEX withdrawn statements batch: tenant={Tenant} provider={Provider} totalWithdrawn={TotalWithdrawn} replacements={Replacements}",
|
||||
tenant ?? "default",
|
||||
provider,
|
||||
totalWithdrawn,
|
||||
replacements);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user