Le journal d'audit ne suit plus les ecritures de la machine
Regression introduite par le commit qui journalise les sections.
WeatherSyncService ecrit `section.WeatherResult` sur cron, a 6 h et a 13 h.
Le job existait et etait benin tant que les sections n'etaient pas auditees ;
depuis, chaque rafraichissement produisait une ligne portant la prevision
OpenWeather complete en avant ET en apres -- quelques dizaines de Ko, deux
fois par jour, par section meteo, indefiniment.
Le stockage est le moindre probleme : ce bruit noie les modifications
humaines que l'ecran d'audit existe pour montrer.
Correctif : une liste de colonnes machine (WeatherResult,
WeatherUpdatedDate, DateUpdate) exclues du journal, et surtout aucune ligne
produite quand une modification ne touche qu'elles -- plutot qu'une ligne au
diff vide, qui aurait deplace le bruit sans le retirer.
DateUpdate y est pour une raison distincte de la meteo : estampille a chaque
SaveChanges, il figurait dans tous les diffs sans jamais rien y apprendre. Le
signal etait deja la -- un test ecrit hier devait l'ecarter a la main pour
rester lisible (`newValues.Keys.Where(k => k != "DateUpdate")`). Quand un
test doit filtrer une donnee pour etre lisible, la donnee n'a rien a y faire.
Verifie au passage : AgendaSyncService ecrit des EventAgenda, qui ne sont pas
audites, et ne touche pas la ligne Section. Lui n'est pas concerne.
⚠️ Consequence sur la dette signalee hier : sans le flot meteo, la table ne
grossit plus qu'au rythme des editions humaines -- de l'ordre de quelques
dizaines de milliers de lignes par an sur 4 clients. L'index sur Timestamp
evoque hier n'a plus lieu d'etre, donc pas de migration et le gel du lot B
n'est pas remis en cause. La purge reste pertinente, mais comme sujet RGPD
(lot J), plus comme sujet de volume.
3 tests. dotnet test 203/203.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
2b1b20cd49
commit
7a80766d43
@ -79,11 +79,85 @@ namespace ManagerService.Tests.Data
|
||||
var oldValues = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(update.OldValues!);
|
||||
var newValues = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(update.NewValues!);
|
||||
|
||||
Assert.Equal("Label", Assert.Single(newValues!.Keys.Where(k => k != "DateUpdate")));
|
||||
// DateUpdate est estampillé à chaque SaveChanges : il n'a rien à faire dans le
|
||||
// diff, et son absence est ce qui rend cette assertion lisible.
|
||||
Assert.Equal("Label", Assert.Single(newValues!.Keys));
|
||||
Assert.Equal("Label", oldValues!["Label"].GetString());
|
||||
Assert.Equal("Renommée", newValues["Label"].GetString());
|
||||
}
|
||||
|
||||
// ── COLONNES MACHINE ─────────────────────────────────────────────────
|
||||
//
|
||||
// WeatherSyncService réécrit WeatherResult deux fois par jour, sur cron. Depuis que
|
||||
// les sections sont journalisées, chaque passage produisait une ligne portant la
|
||||
// prévision complète en avant ET en après — du bruit qui noie les modifications
|
||||
// humaines que l'écran d'audit existe pour montrer.
|
||||
|
||||
private static SectionWeather NewWeatherSection() =>
|
||||
new SectionWeather
|
||||
{
|
||||
Id = "w1",
|
||||
InstanceId = "i1",
|
||||
Label = "Météo",
|
||||
ConfigurationId = "c1",
|
||||
Type = SectionType.Weather,
|
||||
Title = new List<TranslationDTO>(),
|
||||
Description = new List<TranslationDTO>(),
|
||||
WeatherCity = "Namur"
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void WeatherRefresh_ProducesNoAuditEntry()
|
||||
{
|
||||
using var db = DbContextFactory.Create();
|
||||
db.Sections.Add(NewWeatherSection());
|
||||
db.SaveChanges();
|
||||
|
||||
var section = db.Sections.OfType<SectionWeather>().Single();
|
||||
section.WeatherResult = "{\"list\":[/* 30 Ko de prévisions */]}";
|
||||
section.WeatherUpdatedDate = DateTimeOffset.UtcNow;
|
||||
db.SaveChanges();
|
||||
|
||||
Assert.Empty(db.AuditLogs.Where(a => a.Action == "Update").ToList());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HumanEditOnAWeatherSection_IsStillAudited_WithoutTheForecast()
|
||||
{
|
||||
using var db = DbContextFactory.Create();
|
||||
db.Sections.Add(NewWeatherSection());
|
||||
db.SaveChanges();
|
||||
|
||||
var section = db.Sections.OfType<SectionWeather>().Single();
|
||||
section.WeatherCity = "Liège";
|
||||
section.WeatherResult = "{\"list\":[/* rafraîchi dans la foulée */]}";
|
||||
db.SaveChanges();
|
||||
|
||||
var update = db.AuditLogs.Single(a => a.Action == "Update");
|
||||
var newValues = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(update.NewValues!);
|
||||
|
||||
Assert.Equal("WeatherCity", Assert.Single(newValues!.Keys));
|
||||
Assert.Equal("Liège", newValues["WeatherCity"].GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SectionCreation_DoesNotCarryTheForecastBlob()
|
||||
{
|
||||
using var db = DbContextFactory.Create();
|
||||
|
||||
var section = NewWeatherSection();
|
||||
section.WeatherResult = "{\"list\":[/* déjà rempli */]}";
|
||||
db.Sections.Add(section);
|
||||
db.SaveChanges();
|
||||
|
||||
var values = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(
|
||||
db.AuditLogs.Single().NewValues!);
|
||||
|
||||
Assert.DoesNotContain("WeatherResult", values!.Keys);
|
||||
Assert.DoesNotContain("DateUpdate", values.Keys);
|
||||
Assert.Equal("Namur", values["WeatherCity"].GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SectionDelete_IsAudited()
|
||||
{
|
||||
|
||||
@ -128,6 +128,28 @@ namespace ManagerService.Data
|
||||
private static Type AuditedTypeOf(object entity) =>
|
||||
AuditedTypes.FirstOrDefault(t => t.IsInstanceOfType(entity));
|
||||
|
||||
/// <summary>
|
||||
/// Colonnes écrites par la machine, pas par un utilisateur. Elles n'apparaissent pas
|
||||
/// dans le journal, et une modification qui ne touche qu'elles n'y produit aucune ligne.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <c>WeatherSyncService</c> réécrit <c>WeatherResult</c> sur cron, à 6 h et à 13 h.
|
||||
/// Les sections étant désormais journalisées, chaque rafraîchissement produisait une
|
||||
/// ligne portant la prévision OpenWeather complète en avant ET en après — quelques
|
||||
/// dizaines de Ko, deux fois par jour, par section météo. Le coût de stockage est le
|
||||
/// moindre problème : ce bruit noie les modifications humaines que l'écran d'audit
|
||||
/// existe pour montrer.
|
||||
///
|
||||
/// <c>DateUpdate</c> y est pour une raison distincte : il est estampillé à chaque
|
||||
/// SaveChanges, donc présent dans tous les diffs sans jamais rien y apprendre.
|
||||
/// </remarks>
|
||||
private static readonly HashSet<string> AuditIgnoredProperties = new()
|
||||
{
|
||||
nameof(IAuditableEntity.DateUpdate),
|
||||
nameof(SectionWeather.WeatherResult),
|
||||
nameof(SectionWeather.WeatherUpdatedDate)
|
||||
};
|
||||
|
||||
private List<AuditLog> BuildAuditEntries()
|
||||
{
|
||||
var userId = _httpContextAccessor?.HttpContext?.User?.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
@ -158,22 +180,31 @@ namespace ManagerService.Data
|
||||
var instanceId = entry.Properties
|
||||
.FirstOrDefault(p => p.Metadata.Name == "InstanceId")?.CurrentValue?.ToString();
|
||||
|
||||
var reportable = entry.Properties
|
||||
.Where(p => !AuditIgnoredProperties.Contains(p.Metadata.Name))
|
||||
.ToList();
|
||||
|
||||
string? oldValues = null;
|
||||
string? newValues = null;
|
||||
|
||||
if (entry.State == EntityState.Modified)
|
||||
{
|
||||
var changed = reportable.Where(p => p.IsModified).ToList();
|
||||
|
||||
// Une écriture qui ne touche que des colonnes machine n'est pas un
|
||||
// événement : pas de ligne du tout, plutôt qu'une ligne au diff vide.
|
||||
if (changed.Count == 0)
|
||||
continue;
|
||||
|
||||
oldValues = JsonSerializer.Serialize(
|
||||
entry.Properties.Where(p => p.IsModified)
|
||||
.ToDictionary(p => p.Metadata.Name, p => p.OriginalValue));
|
||||
changed.ToDictionary(p => p.Metadata.Name, p => p.OriginalValue));
|
||||
newValues = JsonSerializer.Serialize(
|
||||
entry.Properties.Where(p => p.IsModified)
|
||||
.ToDictionary(p => p.Metadata.Name, p => p.CurrentValue));
|
||||
changed.ToDictionary(p => p.Metadata.Name, p => p.CurrentValue));
|
||||
}
|
||||
else if (entry.State == EntityState.Added)
|
||||
{
|
||||
newValues = JsonSerializer.Serialize(
|
||||
entry.Properties.ToDictionary(p => p.Metadata.Name, p => p.CurrentValue));
|
||||
reportable.ToDictionary(p => p.Metadata.Name, p => p.CurrentValue));
|
||||
}
|
||||
|
||||
var log = new AuditLog
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user