feat(core): add log line parsing and source model
This commit is contained in:
1 parent
224424df7a
commit
a54e251b41
5 files changed
+148
No files matched your search
@@ -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);
|
||||
}
|
||||
@@ -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)}";
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
namespace Vantage.Core.Logs;
|
||||
|
||||
public readonly record struct LogLine(DateTimeOffset? Timestamp, string? Unit, string Message, string Raw);
|
||||
@@ -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..];
|
||||
}
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
Reference in new issue
Block a user