using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using XFEExtension.NetCore.XFEConsole;
using XFEWorkflow.Abstractions;
using XFEWorkflow.Core.Runtime;
namespace XFEWorkflow.Infrastructure.Storage;
public static class WorkflowLogExporter
{
private static readonly SemaphoreSlim writeGate = new(1);
public static string Format(IReadOnlyList<WorkflowLogEntry> entries, string format, SecretRedactor redactor)
{
if (format == "log") return entries.Count == 0 ? "" : string.Join(Environment.NewLine, entries.Select(entry =>
{
// Redact before XFELogEntry escapes line breaks, so multiline secrets
// cannot survive as a different representation in a text export.
var log = new XFELogEntry { Time = entry.Time, Level = entry.Level, LogText = redactor.Redact(entry.LogText) };
var source = redactor.Redact("\t来源=" + entry.SourceLabel + "\t运行=" + entry.RunId + "\t节点=" + entry.NodeId).Replace("\r", "\\r").Replace("\n", "\\n");
return log + source;
})) + Environment.NewLine;
if (format != "json") throw new ArgumentException("日志格式必须是 log 或 json");
var json = JsonSerializer.SerializeToNode(entries, WorkflowJson.Options)!;
void Redact(JsonNode? value)
{
if (value is JsonObject obj)
foreach (var key in obj.Select(pair => pair.Key).ToArray())
if (obj[key] is JsonValue scalar && scalar.TryGetValue<string>(out var text)) obj[key] = redactor.Redact(text); else Redact(obj[key]);
else if (value is JsonArray array)
for (var i = 0; i < array.Count; i++)
if (array[i] is JsonValue scalar && scalar.TryGetValue<string>(out var text)) array[i] = redactor.Redact(text); else Redact(array[i]);
}
Redact(json);
return json.Display(true);
}
public static async Task<JsonObject> ExportAsync(string path, IReadOnlyList<WorkflowLogEntry> entries, string format, bool append, SecretRedactor redactor, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("请选择或填写日志文件路径");
if (format == "json" && append) throw new ArgumentException("JSON 日志不支持追加,请关闭追加或选择 log 格式");
var text = Format(entries, format, redactor);
path = Path.GetFullPath(path);
await writeGate.WaitAsync(ct).ConfigureAwait(false);
try
{
ct.ThrowIfCancellationRequested();
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
if (append) await File.AppendAllTextAsync(path, text, new UTF8Encoding(false), ct).ConfigureAwait(false);
else
{
var temporary = path + "." + Guid.NewGuid().ToString("N") + ".tmp";
try
{
await File.WriteAllTextAsync(temporary, text, new UTF8Encoding(false), ct).ConfigureAwait(false);
ct.ThrowIfCancellationRequested(); File.Move(temporary, path, true);
}
finally { if (File.Exists(temporary)) File.Delete(temporary); }
}
}
finally { writeGate.Release(); }
return new() { ["path"] = path, ["count"] = entries.Count };
}
}
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using XFEExtension.NetCore.XFEConsole;
using XFEWorkflow.Abstractions;
using XFEWorkflow.Core.Runtime;
namespace XFEWorkflow.Infrastructure.Storage;
public static class WorkflowLogExporter
{
private static readonly SemaphoreSlim writeGate = new(1);
public static string Format(IReadOnlyList<WorkflowLogEntry> entries, string format, SecretRedactor redactor)
{
if (format == "log") return entries.Count == 0 ? "" : string.Join(Environment.NewLine, entries.Select(entry =>
{
// Redact before XFELogEntry escapes line breaks, so multiline secrets
// cannot survive as a different representation in a text export.
var log = new XFELogEntry { Time = entry.Time, Level = entry.Level, LogText = redactor.Redact(entry.LogText) };
var source = redactor.Redact("\t来源=" + entry.SourceLabel + "\t运行=" + entry.RunId + "\t节点=" + entry.NodeId).Replace("\r", "\\r").Replace("\n", "\\n");
return log + source;
})) + Environment.NewLine;
if (format != "json") throw new ArgumentException("日志格式必须是 log 或 json");
var json = JsonSerializer.SerializeToNode(entries, WorkflowJson.Options)!;
void Redact(JsonNode? value)
{
if (value is JsonObject obj)
foreach (var key in obj.Select(pair => pair.Key).ToArray())
if (obj[key] is JsonValue scalar && scalar.TryGetValue<string>(out var text)) obj[key] = redactor.Redact(text); else Redact(obj[key]);
else if (value is JsonArray array)
for (var i = 0; i < array.Count; i++)
if (array[i] is JsonValue scalar && scalar.TryGetValue<string>(out var text)) array[i] = redactor.Redact(text); else Redact(array[i]);
}
Redact(json);
return json.Display(true);
}
public static async Task<JsonObject> ExportAsync(string path, IReadOnlyList<WorkflowLogEntry> entries, string format, bool append, SecretRedactor redactor, CancellationToken ct = default)
{
ct.ThrowIfCancellationRequested();
if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("请选择或填写日志文件路径");
if (format == "json" && append) throw new ArgumentException("JSON 日志不支持追加,请关闭追加或选择 log 格式");
var text = Format(entries, format, redactor);
path = Path.GetFullPath(path);
await writeGate.WaitAsync(ct).ConfigureAwait(false);
try
{
ct.ThrowIfCancellationRequested();
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
if (append) await File.AppendAllTextAsync(path, text, new UTF8Encoding(false), ct).ConfigureAwait(false);
else
{
var temporary = path + "." + Guid.NewGuid().ToString("N") + ".tmp";
try
{
await File.WriteAllTextAsync(temporary, text, new UTF8Encoding(false), ct).ConfigureAwait(false);
ct.ThrowIfCancellationRequested(); File.Move(temporary, path, true);
}
finally { if (File.Exists(temporary)) File.Delete(temporary); }
}
}
finally { writeGate.Release(); }
return new() { ["path"] = path, ["count"] = entries.Count };
}
}