Remotely/Shared/Services/FileLogger.cs
Jared Goodwin 3ef4cdf81a
Extract remote control functionality into separate library. (#539)
* Convert server to new single-file startup model.

* Add remote control implementations.

* Implement IViewerAuthorizer.

* Update hub endpoints.

* Implement HubEventHandler.

* Implement ViewerHubDataProvider.

* Implement page data provider.

* Implement RCL and refactor.

* Update submodule.

* Replace submodule with NuGet.

* Update copy URL.

* Update NuGet.

* Remove deprecated WebRTC.

* Remove deprecated WebRTC.

* Update Immense.RemoteControl

* Building out desktop projects.

* Bring more services into submodule.

* Update submodule.

* Update submodule.

* Refactoring for module.

* Update submodule.

* Update submodule

* Got Windows desktop app running.

* Refactor for submodule changes.

* FIx unattended session start.

* Switch desktop app out of console mode.

* Fix tests.

* Update publishing.

* Remove ClickOnce middleware.

* Remove ClickOnce remnants.

* Update submodule

* Add some logging.

* Update Linux path.

* Update submodule.

* Add cleanup service for unattended sessions that failed to start.

* Update submodule.

* Fix chat.

* Add ValidateExecutableReferencesMatchSelfContained property.

* Add other submodule projects.  Align checkbox.

* Update submodule.  Reduce deserialization in the browser, resulting in faster renders.

* Update submodule.

* Update submodule.

* Update submodule.

* Update submodule.

* Add orgId back for branding.

* Get branding loading in desktop apps.

* Update submodule.

* Create log dir.

* Refactor version check on config page.

* Update submodule.

* Update submodule.

* Change submodule URL.

* Correct namespace.

* Update submodule.

* Checkout submodules recursively.
2022-12-23 06:39:12 -08:00

150 lines
4.5 KiB
C#

using Microsoft.Extensions.Logging;
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
namespace Remotely.Shared.Services
{
public class FileLogger : ILogger
{
private static readonly ConcurrentQueue<string> _logQueue = new();
private static readonly ConcurrentStack<string> _scopeStack = new();
private static readonly SemaphoreSlim _writeLock = new(1, 1);
private readonly string _categoryName;
private readonly System.Timers.Timer _sinkTimer = new(5000) { AutoReset = false };
public FileLogger(string categoryName)
{
_categoryName = categoryName;
_sinkTimer.Elapsed += SinkTimer_Elapsed;
}
private static string LogPath => Path.Combine(Path.GetTempPath(), "Remotely", "Logs", $"LogFile_{DateTime.Now:yyyy-MM-dd}.log");
public IDisposable BeginScope<TState>(TState state)
{
_scopeStack.Push($"{state}");
return new NoopDisposable();
}
public bool IsEnabled(LogLevel logLevel)
{
switch (logLevel)
{
#if DEBUG
case LogLevel.Trace:
case LogLevel.Debug:
return true;
#endif
case LogLevel.Information:
case LogLevel.Warning:
case LogLevel.Error:
case LogLevel.Critical:
return true;
case LogLevel.None:
default:
return false;
}
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
try
{
var scopeStack = _scopeStack.Any() ?
new string[] { _scopeStack.First(), _scopeStack.Last() } :
Array.Empty<string>();
var message = FormatLogEntry(logLevel, _categoryName, $"{state}", exception, scopeStack);
_logQueue.Enqueue(message);
_sinkTimer.Start();
}
catch (Exception ex)
{
Console.WriteLine($"Error queueing log entry: {ex.Message}");
}
}
private static void CheckLogFileExists()
{
Directory.CreateDirectory(Path.GetDirectoryName(LogPath)!);
if (!File.Exists(LogPath))
{
File.Create(LogPath).Close();
}
}
private static string FormatLogEntry(LogLevel logLevel, string categoryName, string state, Exception exception, string[] scopeStack)
{
var ex = exception;
var exMessage = exception?.Message;
while (ex?.InnerException is not null)
{
exMessage += $" | {ex.InnerException.Message}";
ex = ex.InnerException;
}
var entry =
$"[{logLevel}]\t" +
$"{DateTimeOffset.Now:yyyy-MM-dd HH:mm:ss.fff}\t";
entry += scopeStack.Any() ?
$"[{string.Join(" - ", scopeStack)} - {categoryName}]\t" :
$"[{categoryName}]\t";
entry += $"Message: {state}\t";
if (!string.IsNullOrWhiteSpace(exMessage))
{
entry += exMessage;
}
if (exception is not null)
{
entry += $"{Environment.NewLine}{exception.StackTrace}";
}
entry += Environment.NewLine;
return entry;
}
private async void SinkTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
{
try
{
await _writeLock.WaitAsync();
CheckLogFileExists();
var message = string.Empty;
while (_logQueue.TryDequeue(out var entry))
{
message += entry;
}
File.AppendAllText(LogPath, message);
}
catch (Exception ex)
{
Console.WriteLine($"Error writing log entry: {ex.Message}");
}
finally
{
_writeLock.Release();
}
}
private class NoopDisposable : IDisposable
{
public void Dispose()
{
_scopeStack.TryPop(out _);
}
}
}
}