using System.Text; namespace QuestionsApi.Services; public sealed class ServiceMetrics { private long _attemptsCreated; private long _attemptDuplicates; private long _counterFailures; private long _ratingFailures; private long _reconciliations; private long _lastReconciliationUnixSeconds; private long _httpRequests; private long _httpDurationMicroseconds; private long _answerStatisticsBackfillComplete; private readonly System.Collections.Concurrent.ConcurrentDictionary _dynamicSelections = new(); private readonly System.Collections.Concurrent.ConcurrentDictionary _dynamicAttempts = new(); private readonly System.Collections.Concurrent.ConcurrentDictionary _dynamicCorrect = new(); private long _dynamicTargetGapTotal; private readonly System.Collections.Concurrent.ConcurrentDictionary _creditAwards = new(); private readonly System.Collections.Concurrent.ConcurrentDictionary _creditCoins = new(); private readonly System.Collections.Concurrent.ConcurrentDictionary _creditDuplicates = new(); private readonly System.Collections.Concurrent.ConcurrentDictionary _creditCapped = new(); public void AttemptCreated() => Interlocked.Increment(ref _attemptsCreated); public void AttemptDuplicate() => Interlocked.Increment(ref _attemptDuplicates); public void CounterFailure() => Interlocked.Increment(ref _counterFailures); public void RatingFailure() => Interlocked.Increment(ref _ratingFailures); public void AnswerStatisticsBackfillCompleted() => Interlocked.Exchange(ref _answerStatisticsBackfillComplete, 1); public void Reconciled() { Interlocked.Increment(ref _reconciliations); Interlocked.Exchange(ref _lastReconciliationUnixSeconds, DateTimeOffset.UtcNow.ToUnixTimeSeconds()); } public void HttpRequest(TimeSpan duration) { Interlocked.Increment(ref _httpRequests); Interlocked.Add(ref _httpDurationMicroseconds, (long)(duration.TotalMilliseconds * 1000)); } public void DynamicSelected(string tier, int targetGap) { _dynamicSelections.AddOrUpdate(tier, 1, (_, value) => value + 1); Interlocked.Add(ref _dynamicTargetGapTotal, Math.Abs(targetGap)); } public void DynamicAttempt(string tier, bool correct) { _dynamicAttempts.AddOrUpdate(tier, 1, (_, value) => value + 1); if (correct) _dynamicCorrect.AddOrUpdate(tier, 1, (_, value) => value + 1); } public void CreditAward(string reason, int amount) { _creditAwards.AddOrUpdate(reason, 1, (_, value) => value + 1); _creditCoins.AddOrUpdate(reason, amount, (_, value) => value + amount); } public void CreditDuplicate(string reason) => _creditDuplicates.AddOrUpdate(reason, 1, (_, value) => value + 1); public void CreditCapped(string reason) => _creditCapped.AddOrUpdate(reason, 1, (_, value) => value + 1); public string RenderPrometheus() { var output = new StringBuilder(); AppendCounter(output, "step3_question_attempts_created_total", Interlocked.Read(ref _attemptsCreated)); AppendCounter(output, "step3_question_attempt_duplicates_total", Interlocked.Read(ref _attemptDuplicates)); AppendCounter(output, "step3_answer_counter_failures_total", Interlocked.Read(ref _counterFailures)); AppendCounter(output, "step3_rating_failures_total", Interlocked.Read(ref _ratingFailures)); AppendCounter(output, "step3_answer_reconciliations_total", Interlocked.Read(ref _reconciliations)); AppendCounter(output, "step3_http_requests_total", Interlocked.Read(ref _httpRequests)); output.AppendLine("# TYPE step3_dynamic_selections_total counter"); output.AppendLine("# TYPE step3_dynamic_attempts_total counter"); output.AppendLine("# TYPE step3_dynamic_correct_total counter"); foreach (var tier in _dynamicSelections.Keys.Order()) { AppendLabeledSample(output, "step3_dynamic_selections_total", tier, _dynamicSelections.GetValueOrDefault(tier)); AppendLabeledSample(output, "step3_dynamic_attempts_total", tier, _dynamicAttempts.GetValueOrDefault(tier)); AppendLabeledSample(output, "step3_dynamic_correct_total", tier, _dynamicCorrect.GetValueOrDefault(tier)); } AppendCounter(output, "step3_dynamic_target_gap_points_total", Interlocked.Read(ref _dynamicTargetGapTotal)); output.AppendLine("# TYPE step3_credit_awards_total counter"); output.AppendLine("# TYPE step3_credit_coins_awarded_total counter"); output.AppendLine("# TYPE step3_credit_award_duplicates_total counter"); output.AppendLine("# TYPE step3_credit_award_capped_total counter"); foreach (var reason in _creditAwards.Keys.Concat(_creditDuplicates.Keys).Concat(_creditCapped.Keys).Distinct().Order()) { AppendReasonSample(output, "step3_credit_awards_total", reason, _creditAwards.GetValueOrDefault(reason)); AppendReasonSample(output, "step3_credit_coins_awarded_total", reason, _creditCoins.GetValueOrDefault(reason)); AppendReasonSample(output, "step3_credit_award_duplicates_total", reason, _creditDuplicates.GetValueOrDefault(reason)); AppendReasonSample(output, "step3_credit_award_capped_total", reason, _creditCapped.GetValueOrDefault(reason)); } output.AppendLine("# TYPE step3_http_request_duration_seconds summary"); output.Append("step3_http_request_duration_seconds_count ") .Append(Interlocked.Read(ref _httpRequests)) .AppendLine(); output.Append("step3_http_request_duration_seconds_sum ") .Append((Interlocked.Read(ref _httpDurationMicroseconds) / 1_000_000.0).ToString("0.######", System.Globalization.CultureInfo.InvariantCulture)) .AppendLine(); output.AppendLine("# TYPE step3_answer_last_reconciliation_unixtime gauge"); output.Append("step3_answer_last_reconciliation_unixtime ") .Append(Interlocked.Read(ref _lastReconciliationUnixSeconds)) .AppendLine(); output.AppendLine("# TYPE step3_answer_statistics_v2_backfill_complete gauge"); output.Append("step3_answer_statistics_v2_backfill_complete ") .Append(Interlocked.Read(ref _answerStatisticsBackfillComplete)) .AppendLine(); return output.ToString(); } private static void AppendCounter(StringBuilder output, string name, long value) { output.Append("# TYPE ").Append(name).AppendLine(" counter"); output.Append(name).Append(' ').Append(value).AppendLine(); } private static void AppendLabeledSample(StringBuilder output, string name, string tier, long value) { output.Append(name).Append("{tier=\"").Append(tier.Replace("\"", "")).Append("\"} ").Append(value).AppendLine(); } private static void AppendReasonSample(StringBuilder output, string name, string reason, long value) => output.Append(name).Append("{reason=\"").Append(reason.Replace("\"", "")).Append("\"} ").Append(value).AppendLine(); }