consolidation of some of the modules, localization fixes, product advisories work, qa work

This commit is contained in:
master
2026-03-05 03:54:22 +02:00
parent 7bafcc3eef
commit 8e1cb9448d
3878 changed files with 72600 additions and 46861 deletions

View File

@@ -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);
}
}

View File

@@ -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");
}

View File

@@ -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),
};
}

View File

@@ -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),
};
}

View File

@@ -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),
};
}

View File

@@ -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);
}
}
}