part #2
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
namespace StellaOps.Evidence.Budgets;
|
||||
|
||||
public sealed record BudgetCheckResult
|
||||
{
|
||||
public required bool IsWithinBudget { get; init; }
|
||||
public IReadOnlyList<string> Issues { get; init; } = [];
|
||||
public BudgetExceededAction RecommendedAction { get; init; }
|
||||
public bool CanAutoPrune { get; init; }
|
||||
public long BytesToFree { get; init; }
|
||||
|
||||
public static BudgetCheckResult WithinBudget() => new() { IsWithinBudget = true };
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
namespace StellaOps.Evidence.Budgets;
|
||||
|
||||
public enum BudgetExceededAction
|
||||
{
|
||||
/// <summary>Log warning but continue.</summary>
|
||||
Warn,
|
||||
|
||||
/// <summary>Block the operation.</summary>
|
||||
Block,
|
||||
|
||||
/// <summary>Automatically prune lowest priority evidence.</summary>
|
||||
AutoPrune
|
||||
}
|
||||
19
src/__Libraries/StellaOps.Evidence/Budgets/BudgetStatus.cs
Normal file
19
src/__Libraries/StellaOps.Evidence/Budgets/BudgetStatus.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace StellaOps.Evidence.Budgets;
|
||||
|
||||
public sealed record BudgetStatus
|
||||
{
|
||||
public required Guid ScanId { get; init; }
|
||||
public required long TotalBudgetBytes { get; init; }
|
||||
public required long UsedBytes { get; init; }
|
||||
public required long RemainingBytes { get; init; }
|
||||
public required decimal UtilizationPercent { get; init; }
|
||||
public required IReadOnlyDictionary<EvidenceType, TypeBudgetStatus> ByType { get; init; }
|
||||
}
|
||||
|
||||
public sealed record TypeBudgetStatus
|
||||
{
|
||||
public required EvidenceType Type { get; init; }
|
||||
public required long UsedBytes { get; init; }
|
||||
public long? LimitBytes { get; init; }
|
||||
public decimal UtilizationPercent { get; init; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace StellaOps.Evidence.Budgets;
|
||||
|
||||
public enum CompressionLevel
|
||||
{
|
||||
None,
|
||||
Fast,
|
||||
Optimal,
|
||||
Maximum
|
||||
}
|
||||
@@ -51,69 +51,3 @@ public sealed record EvidenceBudget
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public enum EvidenceType
|
||||
{
|
||||
Verdict,
|
||||
PolicyTrace,
|
||||
CallGraph,
|
||||
RuntimeCapture,
|
||||
Sbom,
|
||||
Vex,
|
||||
Attestation,
|
||||
PathWitness,
|
||||
Advisory
|
||||
}
|
||||
|
||||
public enum RetentionTier
|
||||
{
|
||||
/// <summary>Immediately accessible, highest cost.</summary>
|
||||
Hot,
|
||||
|
||||
/// <summary>Quick retrieval, moderate cost.</summary>
|
||||
Warm,
|
||||
|
||||
/// <summary>Delayed retrieval, lower cost.</summary>
|
||||
Cold,
|
||||
|
||||
/// <summary>Long-term storage, lowest cost.</summary>
|
||||
Archive
|
||||
}
|
||||
|
||||
public sealed record RetentionPolicy
|
||||
{
|
||||
/// <summary>
|
||||
/// How long evidence stays in this tier.
|
||||
/// </summary>
|
||||
public required TimeSpan Duration { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Compression algorithm for this tier.
|
||||
/// </summary>
|
||||
public CompressionLevel Compression { get; init; } = CompressionLevel.None;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to deduplicate within this tier.
|
||||
/// </summary>
|
||||
public bool Deduplicate { get; init; } = true;
|
||||
}
|
||||
|
||||
public enum CompressionLevel
|
||||
{
|
||||
None,
|
||||
Fast,
|
||||
Optimal,
|
||||
Maximum
|
||||
}
|
||||
|
||||
public enum BudgetExceededAction
|
||||
{
|
||||
/// <summary>Log warning but continue.</summary>
|
||||
Warn,
|
||||
|
||||
/// <summary>Block the operation.</summary>
|
||||
Block,
|
||||
|
||||
/// <summary>Automatically prune lowest priority evidence.</summary>
|
||||
AutoPrune
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace StellaOps.Evidence.Budgets;
|
||||
|
||||
public sealed partial class EvidenceBudgetService
|
||||
{
|
||||
public async Task<PruneResult> PruneToFitAsync(Guid scanId, long targetBytes, CancellationToken ct)
|
||||
{
|
||||
var budget = _options.CurrentValue;
|
||||
var usage = await GetCurrentUsageAsync(scanId, ct).ConfigureAwait(false);
|
||||
|
||||
if (usage.TotalBytes <= targetBytes)
|
||||
{
|
||||
return PruneResult.NoPruningNeeded();
|
||||
}
|
||||
|
||||
var bytesToPrune = usage.TotalBytes - targetBytes;
|
||||
var pruned = new List<PrunedItem>();
|
||||
|
||||
// Get all evidence items, sorted by pruning priority
|
||||
var items = await _repository.GetByScanIdAsync(scanId, ct).ConfigureAwait(false);
|
||||
var candidates = items
|
||||
.Where(i => !budget.AlwaysPreserve.Contains(i.Type))
|
||||
.OrderBy(i => GetPrunePriority(i))
|
||||
.ThenBy(i => i.CreatedAt.UtcDateTime.Ticks)
|
||||
.ThenBy(i => i.Id)
|
||||
.ToList();
|
||||
|
||||
long prunedBytes = 0;
|
||||
foreach (var item in candidates)
|
||||
{
|
||||
if (prunedBytes >= bytesToPrune)
|
||||
break;
|
||||
|
||||
// Move to archive tier or delete
|
||||
await _repository.MoveToTierAsync(item.Id, RetentionTier.Archive, ct).ConfigureAwait(false);
|
||||
pruned.Add(new PrunedItem(item.Id, item.Type, item.SizeBytes));
|
||||
prunedBytes += item.SizeBytes;
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Pruned {Count} items ({Bytes:N0} bytes) for scan {ScanId}",
|
||||
pruned.Count, prunedBytes, scanId);
|
||||
|
||||
return new PruneResult
|
||||
{
|
||||
Success = prunedBytes >= bytesToPrune,
|
||||
BytesPruned = prunedBytes,
|
||||
ItemsPruned = pruned,
|
||||
BytesRemaining = usage.TotalBytes - prunedBytes
|
||||
};
|
||||
}
|
||||
|
||||
private static int GetPrunePriority(EvidenceItem item)
|
||||
{
|
||||
// Lower = prune first
|
||||
return item.Type switch
|
||||
{
|
||||
EvidenceType.RuntimeCapture => 1,
|
||||
EvidenceType.CallGraph => 2,
|
||||
EvidenceType.Advisory => 3,
|
||||
EvidenceType.PathWitness => 4,
|
||||
EvidenceType.PolicyTrace => 5,
|
||||
EvidenceType.Sbom => 6,
|
||||
EvidenceType.Vex => 7,
|
||||
EvidenceType.Attestation => 8,
|
||||
EvidenceType.Verdict => 9, // Never prune
|
||||
_ => 5
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
namespace StellaOps.Evidence.Budgets;
|
||||
|
||||
public sealed partial class EvidenceBudgetService
|
||||
{
|
||||
public async Task<BudgetStatus> GetBudgetStatusAsync(Guid scanId, CancellationToken ct)
|
||||
{
|
||||
var budget = _options.CurrentValue;
|
||||
var usage = await GetCurrentUsageAsync(scanId, ct).ConfigureAwait(false);
|
||||
|
||||
return new BudgetStatus
|
||||
{
|
||||
ScanId = scanId,
|
||||
TotalBudgetBytes = budget.MaxScanSizeBytes,
|
||||
UsedBytes = usage.TotalBytes,
|
||||
RemainingBytes = Math.Max(0, budget.MaxScanSizeBytes - usage.TotalBytes),
|
||||
UtilizationPercent = (decimal)usage.TotalBytes / budget.MaxScanSizeBytes * 100,
|
||||
ByType = usage.ByType.ToDictionary(
|
||||
kvp => kvp.Key,
|
||||
kvp => new TypeBudgetStatus
|
||||
{
|
||||
Type = kvp.Key,
|
||||
UsedBytes = kvp.Value,
|
||||
LimitBytes = budget.MaxPerType.GetValueOrDefault(kvp.Key),
|
||||
UtilizationPercent = budget.MaxPerType.TryGetValue(kvp.Key, out var limit)
|
||||
? (decimal)kvp.Value / limit * 100
|
||||
: 0
|
||||
})
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace StellaOps.Evidence.Budgets;
|
||||
|
||||
public sealed partial class EvidenceBudgetService
|
||||
{
|
||||
private async Task<UsageStats> GetCurrentUsageAsync(Guid scanId, CancellationToken ct)
|
||||
{
|
||||
// Implementation to calculate current usage from repository
|
||||
var items = await _repository.GetByScanIdAsync(scanId, ct).ConfigureAwait(false);
|
||||
|
||||
var totalBytes = items.Sum(i => i.SizeBytes);
|
||||
var byType = items
|
||||
.GroupBy(i => i.Type)
|
||||
.OrderBy(g => g.Key)
|
||||
.ToDictionary(g => g.Key, g => g.Sum(i => i.SizeBytes));
|
||||
|
||||
return new UsageStats
|
||||
{
|
||||
TotalBytes = totalBytes,
|
||||
ByType = byType
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,10 @@
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Globalization;
|
||||
|
||||
namespace StellaOps.Evidence.Budgets;
|
||||
|
||||
public interface IEvidenceBudgetService
|
||||
{
|
||||
Task<BudgetCheckResult> CheckBudgetAsync(Guid scanId, EvidenceItem item, CancellationToken ct);
|
||||
Task<BudgetStatus> GetBudgetStatusAsync(Guid scanId, CancellationToken ct);
|
||||
Task<PruneResult> PruneToFitAsync(Guid scanId, long targetBytes, CancellationToken ct);
|
||||
}
|
||||
|
||||
public sealed class EvidenceBudgetService : IEvidenceBudgetService
|
||||
public sealed partial class EvidenceBudgetService : IEvidenceBudgetService
|
||||
{
|
||||
private readonly IEvidenceRepository _repository;
|
||||
private readonly IOptionsMonitor<EvidenceBudget> _options;
|
||||
@@ -32,7 +24,7 @@ public sealed class EvidenceBudgetService : IEvidenceBudgetService
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(item);
|
||||
var budget = _options.CurrentValue;
|
||||
var currentUsage = await GetCurrentUsageAsync(scanId, ct);
|
||||
var currentUsage = await GetCurrentUsageAsync(scanId, ct).ConfigureAwait(false);
|
||||
|
||||
var issues = new List<string>();
|
||||
|
||||
@@ -72,182 +64,4 @@ public sealed class EvidenceBudgetService : IEvidenceBudgetService
|
||||
BytesToFree = Math.Max(0, projectedTotal - budget.MaxScanSizeBytes)
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<BudgetStatus> GetBudgetStatusAsync(Guid scanId, CancellationToken ct)
|
||||
{
|
||||
var budget = _options.CurrentValue;
|
||||
var usage = await GetCurrentUsageAsync(scanId, ct);
|
||||
|
||||
return new BudgetStatus
|
||||
{
|
||||
ScanId = scanId,
|
||||
TotalBudgetBytes = budget.MaxScanSizeBytes,
|
||||
UsedBytes = usage.TotalBytes,
|
||||
RemainingBytes = Math.Max(0, budget.MaxScanSizeBytes - usage.TotalBytes),
|
||||
UtilizationPercent = (decimal)usage.TotalBytes / budget.MaxScanSizeBytes * 100,
|
||||
ByType = usage.ByType.ToDictionary(
|
||||
kvp => kvp.Key,
|
||||
kvp => new TypeBudgetStatus
|
||||
{
|
||||
Type = kvp.Key,
|
||||
UsedBytes = kvp.Value,
|
||||
LimitBytes = budget.MaxPerType.GetValueOrDefault(kvp.Key),
|
||||
UtilizationPercent = budget.MaxPerType.TryGetValue(kvp.Key, out var limit)
|
||||
? (decimal)kvp.Value / limit * 100
|
||||
: 0
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<PruneResult> PruneToFitAsync(Guid scanId, long targetBytes, CancellationToken ct)
|
||||
{
|
||||
var budget = _options.CurrentValue;
|
||||
var usage = await GetCurrentUsageAsync(scanId, ct);
|
||||
|
||||
if (usage.TotalBytes <= targetBytes)
|
||||
{
|
||||
return PruneResult.NoPruningNeeded();
|
||||
}
|
||||
|
||||
var bytesToPrune = usage.TotalBytes - targetBytes;
|
||||
var pruned = new List<PrunedItem>();
|
||||
|
||||
// Get all evidence items, sorted by pruning priority
|
||||
var items = await _repository.GetByScanIdAsync(scanId, ct);
|
||||
var candidates = items
|
||||
.Where(i => !budget.AlwaysPreserve.Contains(i.Type))
|
||||
.OrderBy(i => GetPrunePriority(i))
|
||||
.ThenBy(i => i.CreatedAt.UtcDateTime.Ticks)
|
||||
.ThenBy(i => i.Id)
|
||||
.ToList();
|
||||
|
||||
long prunedBytes = 0;
|
||||
foreach (var item in candidates)
|
||||
{
|
||||
if (prunedBytes >= bytesToPrune)
|
||||
break;
|
||||
|
||||
// Move to archive tier or delete
|
||||
await _repository.MoveToTierAsync(item.Id, RetentionTier.Archive, ct);
|
||||
pruned.Add(new PrunedItem(item.Id, item.Type, item.SizeBytes));
|
||||
prunedBytes += item.SizeBytes;
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Pruned {Count} items ({Bytes:N0} bytes) for scan {ScanId}",
|
||||
pruned.Count, prunedBytes, scanId);
|
||||
|
||||
return new PruneResult
|
||||
{
|
||||
Success = prunedBytes >= bytesToPrune,
|
||||
BytesPruned = prunedBytes,
|
||||
ItemsPruned = pruned,
|
||||
BytesRemaining = usage.TotalBytes - prunedBytes
|
||||
};
|
||||
}
|
||||
|
||||
private static int GetPrunePriority(EvidenceItem item)
|
||||
{
|
||||
// Lower = prune first
|
||||
return item.Type switch
|
||||
{
|
||||
EvidenceType.RuntimeCapture => 1,
|
||||
EvidenceType.CallGraph => 2,
|
||||
EvidenceType.Advisory => 3,
|
||||
EvidenceType.PathWitness => 4,
|
||||
EvidenceType.PolicyTrace => 5,
|
||||
EvidenceType.Sbom => 6,
|
||||
EvidenceType.Vex => 7,
|
||||
EvidenceType.Attestation => 8,
|
||||
EvidenceType.Verdict => 9, // Never prune
|
||||
_ => 5
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<UsageStats> GetCurrentUsageAsync(Guid scanId, CancellationToken ct)
|
||||
{
|
||||
// Implementation to calculate current usage from repository
|
||||
var items = await _repository.GetByScanIdAsync(scanId, ct);
|
||||
|
||||
var totalBytes = items.Sum(i => i.SizeBytes);
|
||||
var byType = items
|
||||
.GroupBy(i => i.Type)
|
||||
.OrderBy(g => g.Key)
|
||||
.ToDictionary(g => g.Key, g => g.Sum(i => i.SizeBytes));
|
||||
|
||||
return new UsageStats
|
||||
{
|
||||
TotalBytes = totalBytes,
|
||||
ByType = byType
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record BudgetCheckResult
|
||||
{
|
||||
public required bool IsWithinBudget { get; init; }
|
||||
public IReadOnlyList<string> Issues { get; init; } = [];
|
||||
public BudgetExceededAction RecommendedAction { get; init; }
|
||||
public bool CanAutoPrune { get; init; }
|
||||
public long BytesToFree { get; init; }
|
||||
|
||||
public static BudgetCheckResult WithinBudget() => new() { IsWithinBudget = true };
|
||||
}
|
||||
|
||||
public sealed record BudgetStatus
|
||||
{
|
||||
public required Guid ScanId { get; init; }
|
||||
public required long TotalBudgetBytes { get; init; }
|
||||
public required long UsedBytes { get; init; }
|
||||
public required long RemainingBytes { get; init; }
|
||||
public required decimal UtilizationPercent { get; init; }
|
||||
public required IReadOnlyDictionary<EvidenceType, TypeBudgetStatus> ByType { get; init; }
|
||||
}
|
||||
|
||||
public sealed record TypeBudgetStatus
|
||||
{
|
||||
public required EvidenceType Type { get; init; }
|
||||
public required long UsedBytes { get; init; }
|
||||
public long? LimitBytes { get; init; }
|
||||
public decimal UtilizationPercent { get; init; }
|
||||
}
|
||||
|
||||
public sealed record PruneResult
|
||||
{
|
||||
public required bool Success { get; init; }
|
||||
public long BytesPruned { get; init; }
|
||||
public IReadOnlyList<PrunedItem> ItemsPruned { get; init; } = [];
|
||||
public long BytesRemaining { get; init; }
|
||||
|
||||
public static PruneResult NoPruningNeeded() => new() { Success = true };
|
||||
}
|
||||
|
||||
public sealed record PrunedItem(Guid ItemId, EvidenceType Type, long SizeBytes);
|
||||
|
||||
public sealed record UsageStats
|
||||
{
|
||||
public long TotalBytes { get; init; }
|
||||
public IReadOnlyDictionary<EvidenceType, long> ByType { get; init; } = new Dictionary<EvidenceType, long>();
|
||||
}
|
||||
|
||||
// Supporting interfaces and types
|
||||
|
||||
public interface IEvidenceRepository
|
||||
{
|
||||
Task<IReadOnlyList<EvidenceItem>> GetByScanIdAsync(Guid scanId, CancellationToken ct);
|
||||
Task<IReadOnlyList<EvidenceItem>> GetByScanIdAndTypeAsync(Guid scanId, EvidenceType type, CancellationToken ct);
|
||||
Task<IReadOnlyList<EvidenceItem>> GetOlderThanAsync(RetentionTier tier, DateTimeOffset cutoff, CancellationToken ct);
|
||||
Task MoveToTierAsync(Guid itemId, RetentionTier tier, CancellationToken ct);
|
||||
Task UpdateContentAsync(Guid itemId, byte[] content, CancellationToken ct);
|
||||
}
|
||||
|
||||
public sealed record EvidenceItem
|
||||
{
|
||||
public required Guid Id { get; init; }
|
||||
public required Guid ScanId { get; init; }
|
||||
public required EvidenceType Type { get; init; }
|
||||
public required long SizeBytes { get; init; }
|
||||
public required RetentionTier Tier { get; init; }
|
||||
public required DateTimeOffset CreatedAt { get; init; }
|
||||
public string? ArchiveKey { get; init; }
|
||||
}
|
||||
|
||||
12
src/__Libraries/StellaOps.Evidence/Budgets/EvidenceItem.cs
Normal file
12
src/__Libraries/StellaOps.Evidence/Budgets/EvidenceItem.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace StellaOps.Evidence.Budgets;
|
||||
|
||||
public sealed record EvidenceItem
|
||||
{
|
||||
public required Guid Id { get; init; }
|
||||
public required Guid ScanId { get; init; }
|
||||
public required EvidenceType Type { get; init; }
|
||||
public required long SizeBytes { get; init; }
|
||||
public required RetentionTier Tier { get; init; }
|
||||
public required DateTimeOffset CreatedAt { get; init; }
|
||||
public string? ArchiveKey { get; init; }
|
||||
}
|
||||
14
src/__Libraries/StellaOps.Evidence/Budgets/EvidenceType.cs
Normal file
14
src/__Libraries/StellaOps.Evidence/Budgets/EvidenceType.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace StellaOps.Evidence.Budgets;
|
||||
|
||||
public enum EvidenceType
|
||||
{
|
||||
Verdict,
|
||||
PolicyTrace,
|
||||
CallGraph,
|
||||
RuntimeCapture,
|
||||
Sbom,
|
||||
Vex,
|
||||
Attestation,
|
||||
PathWitness,
|
||||
Advisory
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace StellaOps.Evidence.Budgets;
|
||||
|
||||
public interface IEvidenceBudgetService
|
||||
{
|
||||
Task<BudgetCheckResult> CheckBudgetAsync(Guid scanId, EvidenceItem item, CancellationToken ct);
|
||||
Task<BudgetStatus> GetBudgetStatusAsync(Guid scanId, CancellationToken ct);
|
||||
Task<PruneResult> PruneToFitAsync(Guid scanId, long targetBytes, CancellationToken ct);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace StellaOps.Evidence.Budgets;
|
||||
|
||||
public interface IEvidenceRepository
|
||||
{
|
||||
Task<IReadOnlyList<EvidenceItem>> GetByScanIdAsync(Guid scanId, CancellationToken ct);
|
||||
Task<IReadOnlyList<EvidenceItem>> GetByScanIdAndTypeAsync(Guid scanId, EvidenceType type, CancellationToken ct);
|
||||
Task<IReadOnlyList<EvidenceItem>> GetOlderThanAsync(RetentionTier tier, DateTimeOffset cutoff, CancellationToken ct);
|
||||
Task MoveToTierAsync(Guid itemId, RetentionTier tier, CancellationToken ct);
|
||||
Task UpdateContentAsync(Guid itemId, byte[] content, CancellationToken ct);
|
||||
}
|
||||
13
src/__Libraries/StellaOps.Evidence/Budgets/PruneResult.cs
Normal file
13
src/__Libraries/StellaOps.Evidence/Budgets/PruneResult.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace StellaOps.Evidence.Budgets;
|
||||
|
||||
public sealed record PruneResult
|
||||
{
|
||||
public required bool Success { get; init; }
|
||||
public long BytesPruned { get; init; }
|
||||
public IReadOnlyList<PrunedItem> ItemsPruned { get; init; } = [];
|
||||
public long BytesRemaining { get; init; }
|
||||
|
||||
public static PruneResult NoPruningNeeded() => new() { Success = true };
|
||||
}
|
||||
|
||||
public sealed record PrunedItem(Guid ItemId, EvidenceType Type, long SizeBytes);
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace StellaOps.Evidence.Budgets;
|
||||
|
||||
public sealed record RetentionPolicy
|
||||
{
|
||||
/// <summary>
|
||||
/// How long evidence stays in this tier.
|
||||
/// </summary>
|
||||
public required TimeSpan Duration { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Compression algorithm for this tier.
|
||||
/// </summary>
|
||||
public CompressionLevel Compression { get; init; } = CompressionLevel.None;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to deduplicate within this tier.
|
||||
/// </summary>
|
||||
public bool Deduplicate { get; init; } = true;
|
||||
}
|
||||
16
src/__Libraries/StellaOps.Evidence/Budgets/RetentionTier.cs
Normal file
16
src/__Libraries/StellaOps.Evidence/Budgets/RetentionTier.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace StellaOps.Evidence.Budgets;
|
||||
|
||||
public enum RetentionTier
|
||||
{
|
||||
/// <summary>Immediately accessible, highest cost.</summary>
|
||||
Hot,
|
||||
|
||||
/// <summary>Quick retrieval, moderate cost.</summary>
|
||||
Warm,
|
||||
|
||||
/// <summary>Delayed retrieval, lower cost.</summary>
|
||||
Cold,
|
||||
|
||||
/// <summary>Long-term storage, lowest cost.</summary>
|
||||
Archive
|
||||
}
|
||||
7
src/__Libraries/StellaOps.Evidence/Budgets/UsageStats.cs
Normal file
7
src/__Libraries/StellaOps.Evidence/Budgets/UsageStats.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace StellaOps.Evidence.Budgets;
|
||||
|
||||
public sealed record UsageStats
|
||||
{
|
||||
public long TotalBytes { get; init; }
|
||||
public IReadOnlyDictionary<EvidenceType, long> ByType { get; init; } = new Dictionary<EvidenceType, long>();
|
||||
}
|
||||
Reference in New Issue
Block a user