using Microsoft.Extensions.Logging; using System.Globalization; using System.Security.Cryptography; using System.Text; using System.Linq; namespace StellaOps.AdvisoryAI.UnifiedSearch; public sealed record UnifiedSearchTelemetryEvent( string Tenant, string QueryHash, string Intent, int ResultCount, long DurationMs, bool UsedVector, IReadOnlyDictionary DomainWeights, IReadOnlyList TopDomains); public interface IUnifiedSearchTelemetrySink { void Record(UnifiedSearchTelemetryEvent telemetryEvent); } internal sealed class LoggingUnifiedSearchTelemetrySink : IUnifiedSearchTelemetrySink { private readonly ILogger _logger; public LoggingUnifiedSearchTelemetrySink(ILogger logger) { _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } public void Record(UnifiedSearchTelemetryEvent telemetryEvent) { ArgumentNullException.ThrowIfNull(telemetryEvent); var weights = string.Join( ",", telemetryEvent.DomainWeights .OrderBy(static pair => pair.Key, StringComparer.Ordinal) .Select(static pair => $"{pair.Key}:{pair.Value.ToString("F3", CultureInfo.InvariantCulture)}")); var topDomains = telemetryEvent.TopDomains.Count == 0 ? "-" : string.Join(",", telemetryEvent.TopDomains.OrderBy(static value => value, StringComparer.Ordinal)); _logger.LogInformation( "unified_search telemetry tenant={Tenant} query_hash={QueryHash} intent={Intent} results={ResultCount} duration_ms={DurationMs} used_vector={UsedVector} top_domains={TopDomains} weights={Weights}", telemetryEvent.Tenant, telemetryEvent.QueryHash, telemetryEvent.Intent, telemetryEvent.ResultCount, telemetryEvent.DurationMs, telemetryEvent.UsedVector, topDomains, weights); } } internal static class UnifiedSearchTelemetryHash { public static string HashQuery(string query) { ArgumentNullException.ThrowIfNull(query); var bytes = Encoding.UTF8.GetBytes(query); var hash = SHA256.HashData(bytes); return Convert.ToHexString(hash).ToLowerInvariant(); } }