feat(core): add log line parsing and source model

This commit is contained in:
dualfroz committed 2026-08-24 14:25:19 +02:00
1 parent 224424df7a
commit a54e251b41
5 files changed
+148

No files matched your search

+8
View File
@@ -0,0 +1,8 @@
using Vantage.Core.Models;
namespace Vantage.Core.Logs;
public interface ILogProvider
{
Task<IReadOnlyList<LogLine>> ReadAsync(HostConfig host, LogSource source, LogQuery query, CancellationToken cancellationToken);
}
+33
View File
@@ -0,0 +1,33 @@
using Vantage.Core.Ssh;
namespace Vantage.Core.Logs;
public static class LogCommand
{
public static string Build(LogSource source, LogQuery query)
{
var lines = Math.Clamp(query.Lines, 1, 100_000);
var baseCommand = source.Kind switch
{
LogSourceKind.JournalUnit => BuildJournal(source.Target, query, lines),
LogSourceKind.File => BuildFile(source.Target, lines),
_ => throw new ArgumentOutOfRangeException(nameof(source))
};
if (string.IsNullOrWhiteSpace(query.Filter))
return baseCommand;
return baseCommand + " | grep -F -- " + ShellArgs.Quote(query.Filter);
}
private static string BuildJournal(string unit, LogQuery query, int lines)
{
var command = $"journalctl -u {ShellArgs.Quote(unit)} -n {lines} --no-pager -o short-iso";
if (!string.IsNullOrWhiteSpace(query.Since))
command += " --since " + ShellArgs.Quote(query.Since);
return command;
}
private static string BuildFile(string path, int lines) =>
$"tail -n {lines} -- {ShellArgs.Quote(path)}";
}
+3
View File
@@ -0,0 +1,3 @@
namespace Vantage.Core.Logs;
public readonly record struct LogLine(DateTimeOffset? Timestamp, string? Unit, string Message, string Raw);
+77
View File
@@ -0,0 +1,77 @@
using System.Globalization;
namespace Vantage.Core.Logs;
public static class LogLineParser
{
public static IReadOnlyList<LogLine> Parse(string logOutput)
{
var result = new List<LogLine>();
foreach (var rawLine in logOutput.Replace("\r\n", "\n").Split('\n'))
{
if (rawLine.Length == 0)
continue;
result.Add(ParseLine(rawLine));
}
return result;
}
public static LogLine ParseLine(string line)
{
var firstSpace = line.IndexOf(' ');
if (firstSpace <= 0 || !TryParseTimestamp(line[..firstSpace], out var timestamp))
return new LogLine(null, null, line, line);
var rest = line[(firstSpace + 1)..];
var secondSpace = rest.IndexOf(' ');
if (secondSpace <= 0)
return new LogLine(timestamp, null, rest, line);
var afterHost = rest[(secondSpace + 1)..];
var colon = afterHost.IndexOf(':');
if (colon <= 0)
return new LogLine(timestamp, null, afterHost, line);
var unit = StripPid(afterHost[..colon].Trim());
var message = afterHost[(colon + 1)..].TrimStart();
return new LogLine(timestamp, unit, message, line);
}
private static string StripPid(string unit)
{
var bracket = unit.IndexOf('[');
return bracket > 0 ? unit[..bracket] : unit;
}
private static bool TryParseTimestamp(string token, out DateTimeOffset timestamp)
{
if (DateTimeOffset.TryParse(token, CultureInfo.InvariantCulture, DateTimeStyles.None, out timestamp))
return true;
var withColon = InsertOffsetColon(token);
if (withColon is not null &&
DateTimeOffset.TryParse(withColon, CultureInfo.InvariantCulture, DateTimeStyles.None, out timestamp))
return true;
timestamp = default;
return false;
}
private static string? InsertOffsetColon(string token)
{
if (token.Length < 5)
return null;
var sign = token[^5];
if (sign != '+' && sign != '-')
return null;
var offset = token[^4..];
if (!offset.All(char.IsDigit))
return null;
return token[..^2] + ":" + token[^2..];
}
}
+27
View File
@@ -0,0 +1,27 @@
namespace Vantage.Core.Logs;
public enum LogSourceKind
{
JournalUnit,
File
}
public sealed class LogSource
{
public LogSourceKind Kind { get; set; } = LogSourceKind.JournalUnit;
public string Target { get; set; } = "";
public string Label { get; set; } = "";
public static LogSource Journal(string unit, string? label = null) =>
new() { Kind = LogSourceKind.JournalUnit, Target = unit, Label = label ?? unit };
public static LogSource File(string path, string? label = null) =>
new() { Kind = LogSourceKind.File, Target = path, Label = label ?? path };
}
public sealed class LogQuery
{
public int Lines { get; set; } = 200;
public string? Filter { get; set; }
public string? Since { get; set; }
}