Skip to content

Commit b07c862

Browse files
committed
Begin working on logging
1 parent 6af86e7 commit b07c862

14 files changed

Lines changed: 380 additions & 20 deletions

File tree

src/Platforms/SecureFolderFS.UI/Helpers/BaseLifecycleHelper.cs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
using System.Threading;
55
using System.Threading.Tasks;
66
using Microsoft.Extensions.DependencyInjection;
7+
using Microsoft.Extensions.Logging;
78
using OwlCore.Storage;
89
using OwlCore.Storage.System.IO;
910
using SecureFolderFS.Sdk.Services;
1011
using SecureFolderFS.Shared.ComponentModel;
1112
using SecureFolderFS.Shared.Extensions;
13+
using SecureFolderFS.Shared.Logging;
1214
using SecureFolderFS.UI.ServiceImplementation;
1315
using AddService = Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions;
1416

@@ -56,7 +58,7 @@ public virtual void LogException(Exception? ex)
5658

5759
protected virtual IServiceCollection ConfigureServices(IModifiableFolder settingsFolder)
5860
{
59-
return ServiceCollection
61+
ServiceCollection
6062

6163
// Singleton services
6264
.Foundation<IVaultService, VaultService>(AddService.AddSingleton)
@@ -72,7 +74,22 @@ protected virtual IServiceCollection ConfigureServices(IModifiableFolder setting
7274
#else
7375
.Foundation<ITelemetryService, SentryTelemetryService>(AddService.AddSingleton)
7476
#endif
75-
; // Finish service initialization
77+
;
78+
79+
// Configure logging
80+
ServiceCollection.AddLogging(builder =>
81+
{
82+
#if DEBUG
83+
builder.SetMinimumLevel(LogLevel.Trace);
84+
builder.AddDebugOutput(LogLevel.Trace);
85+
#else
86+
builder.SetMinimumLevel(LogLevel.Warning);
87+
#endif
88+
// Opt-in: file logging
89+
// builder.AddFileOutput(Path.Combine(AppDirectory, "app.log"), LogLevel.Information);
90+
});
91+
92+
return ServiceCollection;
7693
}
7794

7895
public abstract void LogExceptionToFile(Exception? ex);

src/Sdk/SecureFolderFS.Sdk/ViewModels/Controls/Storage/Browser/FolderViewModel.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Linq;
55
using System.Threading;
66
using System.Threading.Tasks;
7+
using Microsoft.Extensions.Logging;
78
using OwlCore.Storage;
89
using SecureFolderFS.Sdk.Attributes;
910
using SecureFolderFS.Sdk.Services;
@@ -16,7 +17,7 @@
1617

1718
namespace SecureFolderFS.Sdk.ViewModels.Controls.Storage.Browser
1819
{
19-
[Inject<ISettingsService>]
20+
[Inject<ISettingsService>, Inject<ILogger>]
2021
[Bindable(true)]
2122
public partial class FolderViewModel : BrowserItemViewModel, IViewDesignation
2223
{
@@ -76,6 +77,7 @@ public virtual void OnDisappearing()
7677
/// <returns>A <see cref="Task"/> that represents the asynchronous operation.</returns>
7778
public async Task ListContentsAsync(CancellationToken cancellationToken = default)
7879
{
80+
var scope = Logger.GetPerformanceScope();
7981
SelectedItems.Clear();
8082
Items.DisposeAll();
8183
Items.Clear();
@@ -92,6 +94,8 @@ public async Task ListContentsAsync(CancellationToken cancellationToken = defaul
9294
// Apply adaptive layout
9395
if (SettingsService.UserSettings.IsAdaptiveLayoutEnabled && BrowserViewModel.TransferViewModel is { IsPickingFolder: false })
9496
ApplyAdaptiveLayout();
97+
98+
Logger.LogPerformance(scope, minThresholdMs: 200);
9599
}
96100

97101
/// <inheritdoc/>

src/Shared/SecureFolderFS.Shared/DI.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using Microsoft.Extensions.Logging;
23

34
namespace SecureFolderFS.Shared
45
{
@@ -19,6 +20,17 @@ public sealed class DI : IServiceProvider
1920
/// </summary>
2021
public bool IsAvailable => _serviceProvider is not null;
2122

23+
/// <summary>
24+
/// Retrieves a logger instance for the specified type.
25+
/// </summary>
26+
/// <typeparam name="T">The type for which the logger is created.</typeparam>
27+
/// <returns>An <see cref="ILogger{T}"/> instance for the specified type.</returns>
28+
public ILogger<T> GetLogger<T>()
29+
where T : class
30+
{
31+
return GetService<ILoggerFactory>().CreateLogger<T>();
32+
}
33+
2234
/// <inheritdoc/>
2335
public object? GetService(Type serviceType)
2436
{
@@ -53,6 +65,17 @@ public T GetService<T>()
5365
return GetService<T>();
5466
}
5567

68+
/// <summary>
69+
/// Retrieves a logger instance for the specified type using the default service provider.
70+
/// </summary>
71+
/// <typeparam name="T">The type for which the logger is created.</typeparam>
72+
/// <returns>An <see cref="ILogger{T}"/> instance for the specified type.</returns>
73+
public static ILogger<T> Logger<T>()
74+
where T : class
75+
{
76+
return Default.GetLogger<T>();
77+
}
78+
5679
/// <summary>
5780
/// Resolves a specific service identified by <typeparamref name="T"/> from <see cref="DI.Default"/>.
5881
/// </summary>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using System.Diagnostics;
2+
using System.Runtime.CompilerServices;
3+
using Microsoft.Extensions.Logging;
4+
5+
namespace SecureFolderFS.Shared.Extensions
6+
{
7+
public static class LoggingExtensions
8+
{
9+
public static long GetPerformanceScope(this ILogger logger)
10+
{
11+
return Stopwatch.GetTimestamp();
12+
}
13+
14+
public static void LogPerformance(this ILogger logger, long scope, int minThresholdMs = -1, [CallerMemberName] string caller = "")
15+
{
16+
var elapsed = Stopwatch.GetElapsedTime(scope);
17+
if (!logger.IsEnabled(LogLevel.Debug))
18+
return;
19+
20+
if (minThresholdMs < 0 || elapsed.TotalMilliseconds > minThresholdMs)
21+
logger.LogDebug("{Caller} completed in {ElapsedMs:F2}ms", caller, elapsed.TotalMilliseconds);
22+
}
23+
}
24+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
using System;
2+
using System.Diagnostics;
3+
using Microsoft.Extensions.Logging;
4+
5+
namespace SecureFolderFS.Shared.Logging
6+
{
7+
internal sealed class DebugOutputLogger : ILogger
8+
{
9+
private readonly string _categoryName;
10+
private readonly LogLevel _minLevel;
11+
12+
public DebugOutputLogger(string categoryName, LogLevel minLevel)
13+
{
14+
_categoryName = categoryName;
15+
_minLevel = minLevel;
16+
}
17+
18+
/// <inheritdoc/>
19+
public bool IsEnabled(LogLevel logLevel)
20+
{
21+
#if !DEBUG
22+
return false;
23+
#endif
24+
return logLevel >= _minLevel && logLevel != LogLevel.None;
25+
}
26+
27+
/// <inheritdoc/>
28+
public IDisposable? BeginScope<TState>(TState state) where TState : notnull
29+
{
30+
return null;
31+
}
32+
33+
/// <inheritdoc/>
34+
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
35+
{
36+
if (!IsEnabled(logLevel))
37+
return;
38+
39+
var message = formatter(state, exception);
40+
var timestamp = DateTime.Now.ToString("HH:mm:ss.fff");
41+
var level = GetLevelTag(logLevel);
42+
43+
Debug.WriteLine($"[{timestamp}] {level} {_categoryName}: {message}");
44+
45+
if (exception is not null)
46+
Debug.WriteLine($"[{timestamp}] {level} {_categoryName}: >>> {exception}");
47+
}
48+
49+
private static string GetLevelTag(LogLevel logLevel) => logLevel switch
50+
{
51+
LogLevel.Trace => "[TRC]",
52+
LogLevel.Debug => "[DBG]",
53+
LogLevel.Information => "[INF]",
54+
LogLevel.Warning => "[WRN]",
55+
LogLevel.Error => "[ERR]",
56+
LogLevel.Critical => "[CRT]",
57+
_ => "[???]"
58+
};
59+
}
60+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
using System;
2+
using System.Collections.Concurrent;
3+
using Microsoft.Extensions.Logging;
4+
5+
namespace SecureFolderFS.Shared.Logging
6+
{
7+
/// <summary>
8+
/// An <see cref="ILoggerProvider"/> that writes log messages to the IDE Debug Output window via <see cref="System.Diagnostics.Debug"/>.
9+
/// </summary>
10+
public sealed class DebugOutputLoggerProvider : ILoggerProvider
11+
{
12+
private readonly ConcurrentDictionary<string, DebugOutputLogger> _loggers = new();
13+
private readonly LogLevel _minLevel;
14+
15+
public DebugOutputLoggerProvider(LogLevel minLevel = LogLevel.Trace)
16+
{
17+
_minLevel = minLevel;
18+
}
19+
20+
/// <inheritdoc/>
21+
public ILogger CreateLogger(string categoryName)
22+
{
23+
return _loggers.GetOrAdd(categoryName, name => new DebugOutputLogger(name, _minLevel));
24+
}
25+
26+
/// <inheritdoc/>
27+
public void Dispose()
28+
{
29+
_loggers.Clear();
30+
}
31+
}
32+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
using System;
2+
using System.IO;
3+
using Microsoft.Extensions.Logging;
4+
5+
namespace SecureFolderFS.Shared.Logging
6+
{
7+
internal sealed class FileOutputLogger : ILogger
8+
{
9+
private readonly string _categoryName;
10+
private readonly LogLevel _minLevel;
11+
private readonly FileOutputLoggerProvider _provider;
12+
13+
public FileOutputLogger(string categoryName, LogLevel minLevel, FileOutputLoggerProvider provider)
14+
{
15+
_categoryName = categoryName;
16+
_minLevel = minLevel;
17+
_provider = provider;
18+
}
19+
20+
/// <inheritdoc/>
21+
public bool IsEnabled(LogLevel logLevel)
22+
{
23+
return logLevel >= _minLevel && logLevel != LogLevel.None;
24+
}
25+
26+
/// <inheritdoc/>
27+
public IDisposable? BeginScope<TState>(TState state) where TState : notnull
28+
{
29+
return null;
30+
}
31+
32+
/// <inheritdoc/>
33+
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
34+
{
35+
if (!IsEnabled(logLevel))
36+
return;
37+
38+
var message = formatter(state, exception);
39+
var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff");
40+
var level = GetLevelTag(logLevel);
41+
42+
var line = $"[{timestamp}] {level} {_categoryName}: {message}";
43+
if (exception is not null)
44+
line += $"{Environment.NewLine} >>> {exception}";
45+
46+
_provider.WriteMessage(line);
47+
}
48+
49+
private static string GetLevelTag(LogLevel logLevel) => logLevel switch
50+
{
51+
LogLevel.Trace => "[TRC]",
52+
LogLevel.Debug => "[DBG]",
53+
LogLevel.Information => "[INF]",
54+
LogLevel.Warning => "[WRN]",
55+
LogLevel.Error => "[ERR]",
56+
LogLevel.Critical => "[CRT]",
57+
_ => "[???]"
58+
};
59+
}
60+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using System;
2+
using System.Collections.Concurrent;
3+
using System.IO;
4+
using Microsoft.Extensions.Logging;
5+
6+
namespace SecureFolderFS.Shared.Logging
7+
{
8+
/// <summary>
9+
/// An <see cref="ILoggerProvider"/> that writes log messages to a file on disk.
10+
/// </summary>
11+
public sealed class FileOutputLoggerProvider : ILoggerProvider
12+
{
13+
private readonly ConcurrentDictionary<string, FileOutputLogger> _loggers = new();
14+
private readonly object _writeLock = new();
15+
private readonly string _filePath;
16+
private readonly LogLevel _minLevel;
17+
18+
public FileOutputLoggerProvider(string filePath, LogLevel minLevel = LogLevel.Information)
19+
{
20+
_filePath = filePath;
21+
_minLevel = minLevel;
22+
23+
var directory = Path.GetDirectoryName(filePath);
24+
if (directory is not null)
25+
Directory.CreateDirectory(directory);
26+
}
27+
28+
/// <inheritdoc/>
29+
public ILogger CreateLogger(string categoryName)
30+
{
31+
return _loggers.GetOrAdd(categoryName, name => new FileOutputLogger(name, _minLevel, this));
32+
}
33+
34+
internal void WriteMessage(string message)
35+
{
36+
lock (_writeLock)
37+
{
38+
File.AppendAllText(_filePath, message + Environment.NewLine);
39+
}
40+
}
41+
42+
/// <inheritdoc/>
43+
public void Dispose()
44+
{
45+
_loggers.Clear();
46+
}
47+
}
48+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using System;
2+
using Microsoft.Extensions.DependencyInjection;
3+
using Microsoft.Extensions.Logging;
4+
5+
namespace SecureFolderFS.Shared.Logging
6+
{
7+
public static class LoggingBuilderExtensions
8+
{
9+
/// <summary>
10+
/// Adds <see cref="DebugOutputLoggerProvider"/> that writes to the IDE Debug Output window.
11+
/// </summary>
12+
/// <param name="builder">The <see cref="ILoggingBuilder"/> to configure.</param>
13+
/// <param name="minLevel">The minimum log level to output.</param>
14+
/// <returns>The <see cref="ILoggingBuilder"/> so that calls can be chained.</returns>
15+
public static ILoggingBuilder AddDebugOutput(this ILoggingBuilder builder, LogLevel minLevel = LogLevel.Trace)
16+
{
17+
builder.Services.AddSingleton<ILoggerProvider>(new DebugOutputLoggerProvider(minLevel));
18+
return builder;
19+
}
20+
21+
/// <summary>
22+
/// Adds <see cref="FileOutputLoggerProvider"/> that writes to a log file on disk.
23+
/// </summary>
24+
/// <param name="builder">The <see cref="ILoggingBuilder"/> to configure.</param>
25+
/// <param name="filePath">The absolute path to the log file.</param>
26+
/// <param name="minLevel">The minimum log level to output.</param>
27+
/// <returns>The <see cref="ILoggingBuilder"/> so that calls can be chained.</returns>
28+
public static ILoggingBuilder AddFileOutput(this ILoggingBuilder builder, string filePath, LogLevel minLevel = LogLevel.Information)
29+
{
30+
builder.Services.AddSingleton<ILoggerProvider>(new FileOutputLoggerProvider(filePath, minLevel));
31+
return builder;
32+
}
33+
}
34+
}

0 commit comments

Comments
 (0)