Remotely/Server/Services/LogsManager.cs

207 lines
6.4 KiB
C#

using Microsoft.Extensions.Logging;
using Remotely.Shared.Extensions;
using Serilog;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Remotely.Server.Services
{
public interface ILogsManager
{
string GetLogsDirectory();
Task<FileInfo> ZipAllLogs();
Task DeleteLogs();
IAsyncEnumerable<string> GetLogs(
DateTimeOffset startDate,
DateTimeOffset endDate,
string messageFilter,
LogLevel? logLevelFilter);
}
public class LogsManager : ILogsManager
{
public static LogsManager Default { get; } = new();
public string GetLogsDirectory()
{
var logsDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "logs");
if (Directory.Exists("/remotely-data"))
{
logsDir = "/remotely-data/logs";
}
return Directory.CreateDirectory(logsDir).FullName;
}
public async Task<FileInfo> ZipAllLogs()
{
var logsDir = GetLogsDirectory();
var baseDir = AppDomain.CurrentDomain.BaseDirectory;
var tempDir = Directory.CreateDirectory(Path.Combine(baseDir, "temp", Guid.NewGuid().ToString())).FullName;
var zipFilePath = Path.Combine(
tempDir,
$"Remotely_Logs-{DateTimeOffset.Now:yyyy-MM-dd-HH-mm-ss}.zip");
using var zipArchive = ZipFile.Open(zipFilePath, ZipArchiveMode.Update);
var files = Directory.GetFiles(logsDir);
foreach (var file in files)
{
var entry = zipArchive.CreateEntry(Path.GetFileName(file));
using var entryStream = entry.Open();
using var fs = File.Open(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
await fs.CopyToAsync(entryStream);
}
return new FileInfo(zipFilePath);
}
public async Task DeleteLogs()
{
var logsDir = GetLogsDirectory();
var files = Directory.GetFiles(logsDir);
if (!files.Any())
{
return;
}
await foreach (var file in files.ToAsyncEnumerable())
{
try
{
File.Delete(file);
}
catch (Exception ex)
{
Console.WriteLine($"Failed to delete log file: {file}. Message: {ex.Message}");
try
{
Console.WriteLine("Attempting to zero out log contents.");
using var fs = File.Open(file, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);
fs.SetLength(0);
}
catch (Exception ex2)
{
Console.WriteLine($"Failed to clear log contents: {file}. Message: {ex2.Message}");
}
}
}
}
public async IAsyncEnumerable<string> GetLogs(
DateTimeOffset startDate,
DateTimeOffset endDate,
string messageFilter,
LogLevel? logLevelFilter)
{
var fromDate = startDate.UtcDateTime.Date;
var toDate = endDate.UtcDateTime.Date.AddDays(1);
var result = new StringBuilder();
var logsDir = GetLogsDirectory();
var files = Directory
.GetFiles(logsDir)
.Select(x => new FileInfo(x))
.Where(x =>
x.LastWriteTimeUtc >= fromDate &&
x.LastWriteTimeUtc <= toDate);
foreach (var file in files)
{
var linesAsync = GetLines(file, messageFilter, logLevelFilter);
await foreach (var line in linesAsync)
{
yield return line;
}
}
}
private async IAsyncEnumerable<string> GetLines(
FileInfo file,
string messageFilter,
LogLevel? logLevelFilter)
{
LogLevel? currentLogLevel = null;
using var fs = File.Open(file.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using var sr = new StreamReader(fs);
while (true)
{
var currentLine = await sr.ReadLineAsync();
if (currentLine is null)
{
break;
}
if (logLevelFilter is not null)
{
if (TryGetLogLevel(currentLine, out var parsedLevel))
{
currentLogLevel = parsedLevel;
}
if (currentLogLevel != logLevelFilter)
{
continue;
}
}
if (!string.IsNullOrWhiteSpace(messageFilter) &&
!currentLine.Contains(messageFilter, StringComparison.OrdinalIgnoreCase))
{
continue;
}
yield return currentLine;
}
}
private bool TryGetLogLevel(
string lineContent,
[NotNullWhen(true)] out LogLevel? logLevel)
{
try
{
var logLevelTag = lineContent[31..36];
if (_logLevelMap.TryGetValue(logLevelTag, out var result))
{
logLevel = result;
return true;
}
}
catch
{
// Ignored.
}
logLevel = default;
return false;
}
private static readonly ReadOnlyDictionary<string, LogLevel> _logLevelMap = new(new Dictionary<string, LogLevel>()
{
["[VRB]"] = LogLevel.Trace,
["[DBG]"] = LogLevel.Debug,
["[INF]"] = LogLevel.Information,
["[WRN]"] = LogLevel.Warning,
["[ERR]"] = LogLevel.Error,
["[FTL]"] = LogLevel.Critical
});
}
}