From 6f252162efe0e3b4ea678a4f595bb4a8c73495ac Mon Sep 17 00:00:00 2001 From: kjh2064 Date: Mon, 13 Jul 2026 00:14:05 +0900 Subject: [PATCH] refactor(dotnet): add collection audit trail --- .../Models/CollectionExecutionAudit.cs | 10 +++++++ .../Services/DataCollectionService.cs | 11 +------ .../Services/KisDataCollectionOrchestrator.cs | 29 ++++++++++++++++++- .../KisDataCollectionOrchestratorTests.cs | 22 ++++++++++++++ 4 files changed, 61 insertions(+), 11 deletions(-) create mode 100644 src/dotnet/QuantEngine.Application/Models/CollectionExecutionAudit.cs diff --git a/src/dotnet/QuantEngine.Application/Models/CollectionExecutionAudit.cs b/src/dotnet/QuantEngine.Application/Models/CollectionExecutionAudit.cs new file mode 100644 index 00000000..9c030acb --- /dev/null +++ b/src/dotnet/QuantEngine.Application/Models/CollectionExecutionAudit.cs @@ -0,0 +1,10 @@ +namespace QuantEngine.Application.Models; + +public sealed record CollectionExecutionAudit( + string RunId, + string State, + DateTimeOffset StartedAt, + DateTimeOffset? FinishedAt, + int SuccessCount, + int ErrorCount, + string? Message); diff --git a/src/dotnet/QuantEngine.Application/Services/DataCollectionService.cs b/src/dotnet/QuantEngine.Application/Services/DataCollectionService.cs index debb5ddb..ca056bd7 100644 --- a/src/dotnet/QuantEngine.Application/Services/DataCollectionService.cs +++ b/src/dotnet/QuantEngine.Application/Services/DataCollectionService.cs @@ -1,22 +1,13 @@ -using System.Text.Json; -using QuantEngine.Core.Interfaces; using QuantEngine.Application.Interfaces; namespace QuantEngine.Application.Services; public class DataCollectionService { - private readonly IKisApiClient _kisApiClient; - private readonly ICollectionRepository _repository; private readonly ICollectionOrchestrator _orchestrator; - public DataCollectionService( - IKisApiClient kisApiClient, - ICollectionRepository repository, - ICollectionOrchestrator orchestrator) + public DataCollectionService(ICollectionOrchestrator orchestrator) { - _kisApiClient = kisApiClient; - _repository = repository; _orchestrator = orchestrator; } diff --git a/src/dotnet/QuantEngine.Application/Services/KisDataCollectionOrchestrator.cs b/src/dotnet/QuantEngine.Application/Services/KisDataCollectionOrchestrator.cs index 4ce98226..de099d71 100644 --- a/src/dotnet/QuantEngine.Application/Services/KisDataCollectionOrchestrator.cs +++ b/src/dotnet/QuantEngine.Application/Services/KisDataCollectionOrchestrator.cs @@ -1,10 +1,12 @@ using System.Text.Json; using System.Text.Json.Serialization; +using System.IO; using Microsoft.Extensions.Logging; using QuantEngine.Core.Interfaces; using QuantEngine.Application.Interfaces; using QuantEngine.Application.Services; +using QuantEngine.Application.Models; namespace QuantEngine.Application.Services; @@ -15,6 +17,7 @@ public class KisDataCollectionOrchestrator : ICollectionOrchestrator private readonly PriceDataNormalizer _normalizer; private readonly SourcePriorityResolver _priorityResolver; private readonly ILogger _logger; + private readonly string _auditRoot; public KisDataCollectionOrchestrator( IKisApiClient kisApiClient, @@ -28,6 +31,28 @@ public class KisDataCollectionOrchestrator : ICollectionOrchestrator _normalizer = normalizer; _priorityResolver = priorityResolver; _logger = logger; + _auditRoot = FindRepoTempRoot(); + } + + private static string FindRepoTempRoot() + { + var current = new DirectoryInfo(AppContext.BaseDirectory); + while (current != null) + { + if (Directory.Exists(Path.Combine(current.FullName, ".git"))) + { + return Path.Combine(current.FullName, "Temp", "collection_audit"); + } + current = current.Parent; + } + return Path.Combine(Directory.GetCurrentDirectory(), "Temp", "collection_audit"); + } + + private void AppendAudit(CollectionExecutionAudit audit) + { + Directory.CreateDirectory(_auditRoot); + var path = Path.Combine(_auditRoot, $"{audit.RunId}.jsonl"); + File.AppendAllText(path, JsonSerializer.Serialize(audit, new JsonSerializerOptions { WriteIndented = false }) + Environment.NewLine); } public async Task RunCollectionAsync(string runId, string account, List tickers) @@ -45,6 +70,7 @@ public class KisDataCollectionOrchestrator : ICollectionOrchestrator try { _logger.LogInformation("Starting collection run {RunId}", runId); + AppendAudit(new CollectionExecutionAudit(runId, "RUNNING", DateTimeOffset.UtcNow, null, 0, 0, "started")); var kisSource = new KisApiPriceSource(_kisApiClient); var rows = new List>(); @@ -157,6 +183,7 @@ public class KisDataCollectionOrchestrator : ICollectionOrchestrator result.SourceCounts = sourceCounts; result.Rows = rows; result.Errors = errors; + AppendAudit(new CollectionExecutionAudit(runId, result.Status, DateTimeOffset.Parse(startedAt), DateTimeOffset.Parse(finishedAt), result.SuccessCount, result.ErrorCount, "finished")); // Save run record await _repository.SaveRunAsync(new CollectionRunRecord( @@ -204,6 +231,7 @@ public class KisDataCollectionOrchestrator : ICollectionOrchestrator result.Status = "FAILED"; result.FinishedAt = DataNormalizationHelper.KstNowIso(); result.ErrorMessage = ex.Message; + AppendAudit(new CollectionExecutionAudit(runId, result.Status, DateTimeOffset.Parse(startedAt), DateTimeOffset.Parse(result.FinishedAt), result.SuccessCount, result.ErrorCount, ex.Message)); return result; } } @@ -358,4 +386,3 @@ public class KisDataCollectionOrchestrator : ICollectionOrchestrator } - diff --git a/src/dotnet/QuantEngine.Core.Tests/KisDataCollectionOrchestratorTests.cs b/src/dotnet/QuantEngine.Core.Tests/KisDataCollectionOrchestratorTests.cs index d6df374e..2f0353a5 100644 --- a/src/dotnet/QuantEngine.Core.Tests/KisDataCollectionOrchestratorTests.cs +++ b/src/dotnet/QuantEngine.Core.Tests/KisDataCollectionOrchestratorTests.cs @@ -93,6 +93,11 @@ public class KisDataCollectionOrchestratorTests var runId = "test-run-002"; var ticker = "005930"; var account = "mock"; + var auditPath = Path.Combine(FindRepoRoot(), "Temp", "collection_audit", $"{runId}.jsonl"); + if (File.Exists(auditPath)) + { + File.Delete(auditPath); + } _repositoryMock .Setup(r => r.GetLatestSnapshotsForTickerAsync(ticker, It.IsAny())) @@ -128,6 +133,8 @@ public class KisDataCollectionOrchestratorTests Assert.NotNull(result); Assert.Equal("COMPLETED", result.Status); Assert.Equal(1, result.SuccessCount); + Assert.True(File.Exists(auditPath)); + Assert.Contains("COMPLETED", File.ReadAllText(auditPath)); _kisApiClientMock.Verify( k => k.GetCurrentPriceAsync(ticker, account), @@ -390,4 +397,19 @@ public class KisDataCollectionOrchestratorTests Assert.False(result); Assert.Null(args[2]); } + + private static string FindRepoRoot() + { + var current = new DirectoryInfo(AppContext.BaseDirectory); + while (current != null) + { + if (Directory.Exists(Path.Combine(current.FullName, ".git"))) + { + return current.FullName; + } + current = current.Parent; + } + + throw new InvalidOperationException("Repository root not found."); + } }