логи
This commit is contained in:
+31
-10
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
using Hangfire;
|
using Hangfire;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -83,15 +84,34 @@ public sealed class CreateNextListPartitionJob(
|
|||||||
var nextNumber = maxNumber + 1;
|
var nextNumber = maxNumber + 1;
|
||||||
|
|
||||||
logger.LogInformation(
|
logger.LogInformation(
|
||||||
"Создаю партицию №{Number} для типа заявок {RequestTypeId} по структуре {SourceListId}.",
|
"Создаю партицию №{Number} для типа заявок {RequestTypeId} ({RequestTypeName}) " +
|
||||||
nextNumber, requestTypeId, requestType.ListId);
|
"по структуре {SourceListId}.",
|
||||||
|
nextNumber, requestTypeId, requestType.Name, requestType.ListId);
|
||||||
|
|
||||||
// Партиция всегда создаётся на том же сайте, что и текущая (см. комментарий
|
var stopwatch = Stopwatch.StartNew();
|
||||||
// на RequestListPartition о недопустимости разъезда серии по сайтам) — сайт
|
ProvisionedList created;
|
||||||
// берём из RequestType, а не откуда-либо ещё, так что расхождение
|
try
|
||||||
// структурно невозможно.
|
{
|
||||||
var created = await provisioner.CreateNextListPairAsync(
|
// Партиция всегда создаётся на том же сайте, что и текущая (см.
|
||||||
requestType.SiteUrl, requestType.ListId, nextNumber, cancellationToken);
|
// комментарий на RequestListPartition о недопустимости разъезда серии
|
||||||
|
// по сайтам) — сайт берём из RequestType, а не откуда-либо ещё, так что
|
||||||
|
// расхождение структурно невозможно.
|
||||||
|
created = await provisioner.CreateNextListPairAsync(
|
||||||
|
requestType.SiteUrl, requestType.ListId, nextNumber, cancellationToken);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Логируем с контекстом (номер партиции, тип заявок) здесь, а не
|
||||||
|
// полагаемся только на то, что исключение долетит до лога Hangfire:
|
||||||
|
// там оно будет без этих подробностей, вперемешку с логами других задач.
|
||||||
|
// Дальше даём ему пойти в Hangfire как обычно — так сработает
|
||||||
|
// AutomaticRetry(Attempts = 3).
|
||||||
|
logger.LogError(
|
||||||
|
ex,
|
||||||
|
"Провижининг партиции №{Number} для типа заявок {RequestTypeId} не удался за {ElapsedMs} мс.",
|
||||||
|
nextNumber, requestTypeId, stopwatch.ElapsedMilliseconds);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
// Реестр и переключение записи — одной транзакцией: иначе можно остаться
|
// Реестр и переключение записи — одной транзакцией: иначе можно остаться
|
||||||
// с записанной в реестр партицией, в которую никто не пишет, или наоборот.
|
// с записанной в реестр партицией, в которую никто не пишет, или наоборот.
|
||||||
@@ -112,8 +132,9 @@ public sealed class CreateNextListPartitionJob(
|
|||||||
await transaction.CommitAsync(cancellationToken);
|
await transaction.CommitAsync(cancellationToken);
|
||||||
|
|
||||||
logger.LogInformation(
|
logger.LogInformation(
|
||||||
"Партиция №{Number} ({ListId}) создана, запись по типу заявок {RequestTypeId} переключена на неё.",
|
"Партиция №{Number} ({ListId}) создана за {ElapsedMs} мс, запись по типу заявок " +
|
||||||
nextNumber, created.ListId, requestTypeId);
|
"{RequestTypeId} переключена на неё.",
|
||||||
|
nextNumber, created.ListId, stopwatch.ElapsedMilliseconds, requestTypeId);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Tnsu.ApprovalRoute.Infrastructure.SharePoint;
|
namespace Tnsu.ApprovalRoute.Infrastructure.SharePoint;
|
||||||
@@ -23,6 +24,11 @@ public sealed class SharePointListProvisioner(
|
|||||||
int number,
|
int number,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
|
logger.LogInformation(
|
||||||
|
"Дублирую пару списков №{Number} на {SiteUrl} по образцу {SourceListId}.",
|
||||||
|
number, siteUrl, sourceDataListId);
|
||||||
|
var stopwatch = Stopwatch.StartNew();
|
||||||
|
|
||||||
var sourceData = await sharePoint.GetListByUrlAsync(siteUrl, sourceDataListId, cancellationToken)
|
var sourceData = await sharePoint.GetListByUrlAsync(siteUrl, sourceDataListId, cancellationToken)
|
||||||
?? throw new InvalidOperationException(
|
?? throw new InvalidOperationException(
|
||||||
$"Исходный список {sourceDataListId} не найден на {siteUrl} — дублировать нечего.");
|
$"Исходный список {sourceDataListId} не найден на {siteUrl} — дублировать нечего.");
|
||||||
@@ -31,11 +37,16 @@ public sealed class SharePointListProvisioner(
|
|||||||
siteUrl,
|
siteUrl,
|
||||||
sourceData,
|
sourceData,
|
||||||
ListTitleConvention.WithNumber(sourceData.Title, number),
|
ListTitleConvention.WithNumber(sourceData.Title, number),
|
||||||
|
role: "данные",
|
||||||
retarget: null,
|
retarget: null,
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
|
||||||
await CloneApprovalPairAsync(siteUrl, sourceData, newData, number, cancellationToken);
|
await CloneApprovalPairAsync(siteUrl, sourceData, newData, number, cancellationToken);
|
||||||
|
|
||||||
|
logger.LogInformation(
|
||||||
|
"Пара списков №{Number} готова за {ElapsedMs} мс: данные «{Title}» ({ListId}).",
|
||||||
|
number, stopwatch.ElapsedMilliseconds, newData.Title, newData.ServerRelativeUrl);
|
||||||
|
|
||||||
return new ProvisionedList(newData.ServerRelativeUrl, newData.Title);
|
return new ProvisionedList(newData.ServerRelativeUrl, newData.Title);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,6 +84,7 @@ public sealed class SharePointListProvisioner(
|
|||||||
siteUrl,
|
siteUrl,
|
||||||
sourceApproval,
|
sourceApproval,
|
||||||
ListTitleConvention.WithNumber(sourceApproval.Title, number),
|
ListTitleConvention.WithNumber(sourceApproval.Title, number),
|
||||||
|
role: "согласование",
|
||||||
// Ticket списка согласования смотрит на список данных — в дубле он должен
|
// Ticket списка согласования смотрит на список данных — в дубле он должен
|
||||||
// смотреть на НОВЫЙ список данных, иначе строки маршрута новых заявок
|
// смотреть на НОВЫЙ список данных, иначе строки маршрута новых заявок
|
||||||
// будут ссылаться в старый список.
|
// будут ссылаться в старый список.
|
||||||
@@ -80,10 +92,15 @@ public sealed class SharePointListProvisioner(
|
|||||||
cancellationToken);
|
cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// role — только для логов («данные»/«согласование»): по нему в логе видно, какую
|
||||||
|
/// половину пары обрабатывает конкретный вызов, не заглядывая в стек вызовов.
|
||||||
|
/// </summary>
|
||||||
private async Task<SharePointListInfo> CloneAsync(
|
private async Task<SharePointListInfo> CloneAsync(
|
||||||
string siteUrl,
|
string siteUrl,
|
||||||
SharePointListInfo source,
|
SharePointListInfo source,
|
||||||
string newTitle,
|
string newTitle,
|
||||||
|
string role,
|
||||||
(Guid From, Guid To)? retarget,
|
(Guid From, Guid To)? retarget,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
@@ -91,13 +108,16 @@ public sealed class SharePointListProvisioner(
|
|||||||
var target = existing ?? await sharePoint.CreateListAsync(siteUrl, newTitle, cancellationToken);
|
var target = existing ?? await sharePoint.CreateListAsync(siteUrl, newTitle, cancellationToken);
|
||||||
|
|
||||||
logger.LogInformation(
|
logger.LogInformation(
|
||||||
existing is null ? "Создан список «{Title}»." : "Список «{Title}» уже существует — дозаполняю колонки.",
|
existing is null
|
||||||
newTitle);
|
? "Список {Role} «{Title}» создан."
|
||||||
|
: "Список {Role} «{Title}» уже существует — дозаполняю недостающие колонки.",
|
||||||
|
role, newTitle);
|
||||||
|
|
||||||
var sourceFields = await sharePoint.GetCustomFieldsAsync(siteUrl, source.ServerRelativeUrl, cancellationToken);
|
var sourceFields = await sharePoint.GetCustomFieldsAsync(siteUrl, source.ServerRelativeUrl, cancellationToken);
|
||||||
var targetFields = await sharePoint.GetCustomFieldsAsync(siteUrl, target.ServerRelativeUrl, cancellationToken);
|
var targetFields = await sharePoint.GetCustomFieldsAsync(siteUrl, target.ServerRelativeUrl, cancellationToken);
|
||||||
var alreadyThere = targetFields.Select(x => x.InternalName).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
var alreadyThere = targetFields.Select(x => x.InternalName).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var copied = 0;
|
||||||
foreach (var field in sourceFields)
|
foreach (var field in sourceFields)
|
||||||
{
|
{
|
||||||
if (alreadyThere.Contains(field.InternalName)) continue;
|
if (alreadyThere.Contains(field.InternalName)) continue;
|
||||||
@@ -107,9 +127,19 @@ public sealed class SharePointListProvisioner(
|
|||||||
: RetargetLookup(field.SchemaXml, retarget.Value.From, retarget.Value.To);
|
: RetargetLookup(field.SchemaXml, retarget.Value.From, retarget.Value.To);
|
||||||
|
|
||||||
await sharePoint.AddFieldFromXmlAsync(siteUrl, target.Id, schemaXml, cancellationToken);
|
await sharePoint.AddFieldFromXmlAsync(siteUrl, target.Id, schemaXml, cancellationToken);
|
||||||
logger.LogInformation(" колонка {InternalName} → «{Title}»", field.InternalName, newTitle);
|
copied++;
|
||||||
|
|
||||||
|
// По каждой колонке — только на Debug: при штатной первой партии их
|
||||||
|
// десятки на список, и на Information это был бы шум. Итог всё равно
|
||||||
|
// виден в сводке ниже.
|
||||||
|
logger.LogDebug(
|
||||||
|
"Список {Role} «{Title}»: скопирована колонка {InternalName}.", role, newTitle, field.InternalName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.LogInformation(
|
||||||
|
"Список {Role} «{Title}»: скопировано колонок {Copied}, уже было {AlreadyThere}.",
|
||||||
|
role, newTitle, copied, sourceFields.Count - copied);
|
||||||
|
|
||||||
return target;
|
return target;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user