Files
KArtSell.Aegis/src/KArtSell.Host/Features/Observability/MetricsSql.cs
T
kjh2064 ca85a2c902 fix: Phase 2-3 DB isolation + Gate 3 data layer real connection (AGENTS.md v16.0)
**DB Isolation (P0):**
- Test connection string: kartselldb → kartselldb_test (prevents accidental production truncates)
- Production Host appsettings unchanged (kartselldb is correct for operations)

**Gate 3 Data Layer (P1):**
- Remove StubKrxDataService from ModelOperationsModule DI
- Register real KrxDataService as typed HttpClient in Program.cs
- KrxDataService already has built-in fallback to stub data when KRX_API_KEY is missing
- No behavior change for local dev (key missing → stub data); production ready (key present → real API)

**Tech Debt Registration (AGENTS.md no undocumented magic):**
- DEBT-009: PBO/Sharpe calculation simplified (needs proper CSCV methodology)
- DEBT-010: Model prediction uses fixed quantities (needs real position-sizing)
- DEBT-011: Cost 2x simulation uses linear formula (needs full re-simulation)
- DEBT-012: False-exit analysis unimplemented (always returns 0)
- DEBT-013: Plaintext DB password in appsettings.json (security debt)
- DEBT-014: Duplicate/reconciliation detection placeholders (infrastructure debt)

Gate 3 marked "rehearsal ready" (real KRX data, simplified analytics).
See TECH_DEBT_REGISTER.md for full impact/effort estimates.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-02 21:09:15 +09:00

99 lines
4.2 KiB
C#

using Dapper;
using Npgsql;
namespace KArtSell.Host.Features.Observability;
/// <summary>
/// Data queries for observability metrics.
/// All queries use PIT (point-in-time) pattern: published_at <= cutoff.
/// Schema-qualified, explicit columns, no SELECT *.
/// </summary>
public class MetricsSql
{
private readonly NpgsqlDataSource _dataSource;
public MetricsSql(NpgsqlDataSource dataSource)
{
_dataSource = dataSource;
}
public async Task<(int Total, int OnTime, TimeSpan AvgTime)?> GetBatchSlaAsync(CancellationToken cancellationToken = default)
{
const string sql = """
SELECT
COUNT(*) as total,
COUNT(CASE WHEN status = 'success' THEN 1 END) as on_time,
AVG(duration_seconds) as avg_seconds
FROM observability.batch_sla_metrics
WHERE published_at <= @now
AND completed_at >= @sevenDaysAgo
""";
await using var connection = await _dataSource.OpenConnectionAsync(cancellationToken);
var result = await connection.QueryFirstOrDefaultAsync<(int, int, double)?>(
sql,
new { now = DateTime.UtcNow, sevenDaysAgo = DateTime.UtcNow.AddDays(-7) },
commandTimeout: 5);
if (result == null || result.Value.Item1 == 0)
return null;
var (total, onTime, avgSec) = result.Value;
return (total, onTime, TimeSpan.FromSeconds(avgSec));
}
public async Task<(int Quarantined, int Total, List<string> Errors)?> GetDataQualityQuarantineAsync(CancellationToken cancellationToken = default)
{
const string sql = """
SELECT
COUNT(*) as total,
COUNT(CASE WHEN resolution_status IS NULL THEN 1 END) as quarantined,
STRING_AGG(DISTINCT reason, ', ') as errors
FROM observability.data_quality_quarantine
WHERE published_at <= @now
AND quarantined_at >= @sevenDaysAgo
""";
await using var connection = await _dataSource.OpenConnectionAsync(cancellationToken);
var result = await connection.QueryFirstOrDefaultAsync<(int Total, int Quarantined, string? Errors)?>(
sql,
new { now = DateTime.UtcNow, sevenDaysAgo = DateTime.UtcNow.AddDays(-7) },
commandTimeout: 5);
if (result == null || result.Value.Item1 == 0)
return null;
var (total, quarantined, errors) = result.Value;
var errorList = string.IsNullOrEmpty(errors) ? new List<string>() : errors.Split(',').Select(e => e.Trim()).Take(5).ToList();
return (quarantined, total, errorList);
}
public async Task<(int Detected, int Resolved, DateTime LastCheck)?> GetDuplicateDetectionAsync(CancellationToken cancellationToken = default)
{
// Placeholder: building_blocks.outbox_message table exists (0000_building_blocks.sql).
// Duplicate detection logging (via operation_audit_trail or dedicated table) not yet implemented.
// Returns null until OutboxPollerJob hooks duplicate tracking (see DEBT-014).
await Task.CompletedTask; // Async compliance
return null;
}
public async Task<(int Detected, int Resolved, List<string> Pending)?> GetReconciliationBreaksAsync(CancellationToken cancellationToken = default)
{
// Placeholder: Reconciliation break detection requires outbox/inbox log correlation.
// Requires audit trail showing Evidence version mismatches. Not yet implemented.
// Returns null until operation_audit_trail is populated by job consumers (see DEBT-014).
await Task.CompletedTask; // Async compliance
return null;
}
public async Task<(decimal Baseline, decimal Current)?> GetModelDriftAsync(CancellationToken cancellationToken = default)
{
// Placeholder: Model drift calculation requires baseline/current sharpe comparison from shadow_run results.
// Returns null until Gate 3 rehearsal populates model_operations.shadow_run with real metrics.
// Once shadow_run results exist, baseline/current sharpe can be calculated and compared (see DEBT-009).
await Task.CompletedTask; // Async compliance
return null;
}
}