refactor(dotnet): add collection audit trail
This commit is contained in:
@@ -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);
|
||||||
@@ -1,22 +1,13 @@
|
|||||||
using System.Text.Json;
|
|
||||||
using QuantEngine.Core.Interfaces;
|
|
||||||
using QuantEngine.Application.Interfaces;
|
using QuantEngine.Application.Interfaces;
|
||||||
|
|
||||||
namespace QuantEngine.Application.Services;
|
namespace QuantEngine.Application.Services;
|
||||||
|
|
||||||
public class DataCollectionService
|
public class DataCollectionService
|
||||||
{
|
{
|
||||||
private readonly IKisApiClient _kisApiClient;
|
|
||||||
private readonly ICollectionRepository _repository;
|
|
||||||
private readonly ICollectionOrchestrator _orchestrator;
|
private readonly ICollectionOrchestrator _orchestrator;
|
||||||
|
|
||||||
public DataCollectionService(
|
public DataCollectionService(ICollectionOrchestrator orchestrator)
|
||||||
IKisApiClient kisApiClient,
|
|
||||||
ICollectionRepository repository,
|
|
||||||
ICollectionOrchestrator orchestrator)
|
|
||||||
{
|
{
|
||||||
_kisApiClient = kisApiClient;
|
|
||||||
_repository = repository;
|
|
||||||
_orchestrator = orchestrator;
|
_orchestrator = orchestrator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
|
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using System.IO;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using QuantEngine.Core.Interfaces;
|
using QuantEngine.Core.Interfaces;
|
||||||
using QuantEngine.Application.Interfaces;
|
using QuantEngine.Application.Interfaces;
|
||||||
using QuantEngine.Application.Services;
|
using QuantEngine.Application.Services;
|
||||||
|
using QuantEngine.Application.Models;
|
||||||
|
|
||||||
namespace QuantEngine.Application.Services;
|
namespace QuantEngine.Application.Services;
|
||||||
|
|
||||||
@@ -15,6 +17,7 @@ public class KisDataCollectionOrchestrator : ICollectionOrchestrator
|
|||||||
private readonly PriceDataNormalizer _normalizer;
|
private readonly PriceDataNormalizer _normalizer;
|
||||||
private readonly SourcePriorityResolver _priorityResolver;
|
private readonly SourcePriorityResolver _priorityResolver;
|
||||||
private readonly ILogger<KisDataCollectionOrchestrator> _logger;
|
private readonly ILogger<KisDataCollectionOrchestrator> _logger;
|
||||||
|
private readonly string _auditRoot;
|
||||||
|
|
||||||
public KisDataCollectionOrchestrator(
|
public KisDataCollectionOrchestrator(
|
||||||
IKisApiClient kisApiClient,
|
IKisApiClient kisApiClient,
|
||||||
@@ -28,6 +31,28 @@ public class KisDataCollectionOrchestrator : ICollectionOrchestrator
|
|||||||
_normalizer = normalizer;
|
_normalizer = normalizer;
|
||||||
_priorityResolver = priorityResolver;
|
_priorityResolver = priorityResolver;
|
||||||
_logger = logger;
|
_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<CollectionRunResult> RunCollectionAsync(string runId, string account, List<string> tickers)
|
public async Task<CollectionRunResult> RunCollectionAsync(string runId, string account, List<string> tickers)
|
||||||
@@ -45,6 +70,7 @@ public class KisDataCollectionOrchestrator : ICollectionOrchestrator
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Starting collection run {RunId}", runId);
|
_logger.LogInformation("Starting collection run {RunId}", runId);
|
||||||
|
AppendAudit(new CollectionExecutionAudit(runId, "RUNNING", DateTimeOffset.UtcNow, null, 0, 0, "started"));
|
||||||
|
|
||||||
var kisSource = new KisApiPriceSource(_kisApiClient);
|
var kisSource = new KisApiPriceSource(_kisApiClient);
|
||||||
var rows = new List<Dictionary<string, object>>();
|
var rows = new List<Dictionary<string, object>>();
|
||||||
@@ -157,6 +183,7 @@ public class KisDataCollectionOrchestrator : ICollectionOrchestrator
|
|||||||
result.SourceCounts = sourceCounts;
|
result.SourceCounts = sourceCounts;
|
||||||
result.Rows = rows;
|
result.Rows = rows;
|
||||||
result.Errors = errors;
|
result.Errors = errors;
|
||||||
|
AppendAudit(new CollectionExecutionAudit(runId, result.Status, DateTimeOffset.Parse(startedAt), DateTimeOffset.Parse(finishedAt), result.SuccessCount, result.ErrorCount, "finished"));
|
||||||
|
|
||||||
// Save run record
|
// Save run record
|
||||||
await _repository.SaveRunAsync(new CollectionRunRecord(
|
await _repository.SaveRunAsync(new CollectionRunRecord(
|
||||||
@@ -204,6 +231,7 @@ public class KisDataCollectionOrchestrator : ICollectionOrchestrator
|
|||||||
result.Status = "FAILED";
|
result.Status = "FAILED";
|
||||||
result.FinishedAt = DataNormalizationHelper.KstNowIso();
|
result.FinishedAt = DataNormalizationHelper.KstNowIso();
|
||||||
result.ErrorMessage = ex.Message;
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -358,4 +386,3 @@ public class KisDataCollectionOrchestrator : ICollectionOrchestrator
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -93,6 +93,11 @@ public class KisDataCollectionOrchestratorTests
|
|||||||
var runId = "test-run-002";
|
var runId = "test-run-002";
|
||||||
var ticker = "005930";
|
var ticker = "005930";
|
||||||
var account = "mock";
|
var account = "mock";
|
||||||
|
var auditPath = Path.Combine(FindRepoRoot(), "Temp", "collection_audit", $"{runId}.jsonl");
|
||||||
|
if (File.Exists(auditPath))
|
||||||
|
{
|
||||||
|
File.Delete(auditPath);
|
||||||
|
}
|
||||||
|
|
||||||
_repositoryMock
|
_repositoryMock
|
||||||
.Setup(r => r.GetLatestSnapshotsForTickerAsync(ticker, It.IsAny<int>()))
|
.Setup(r => r.GetLatestSnapshotsForTickerAsync(ticker, It.IsAny<int>()))
|
||||||
@@ -128,6 +133,8 @@ public class KisDataCollectionOrchestratorTests
|
|||||||
Assert.NotNull(result);
|
Assert.NotNull(result);
|
||||||
Assert.Equal("COMPLETED", result.Status);
|
Assert.Equal("COMPLETED", result.Status);
|
||||||
Assert.Equal(1, result.SuccessCount);
|
Assert.Equal(1, result.SuccessCount);
|
||||||
|
Assert.True(File.Exists(auditPath));
|
||||||
|
Assert.Contains("COMPLETED", File.ReadAllText(auditPath));
|
||||||
|
|
||||||
_kisApiClientMock.Verify(
|
_kisApiClientMock.Verify(
|
||||||
k => k.GetCurrentPriceAsync(ticker, account),
|
k => k.GetCurrentPriceAsync(ticker, account),
|
||||||
@@ -390,4 +397,19 @@ public class KisDataCollectionOrchestratorTests
|
|||||||
Assert.False(result);
|
Assert.False(result);
|
||||||
Assert.Null(args[2]);
|
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.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user