Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 148 additions & 0 deletions src/Extension/RemoteDebuggerLauncher/Logging/LoggerFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// ----------------------------------------------------------------------------
// <copyright company="Michael Koster">
// Copyright (c) Michael Koster. All rights reserved.
// Licensed under the MIT License.
// </copyright>
// ----------------------------------------------------------------------------

using System;
using System.Composition;
using System.Globalization;
using System.IO;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.Shell;
using Serilog;
using Serilog.Extensions.Logging;

namespace RemoteDebuggerLauncher.Logging
{
/// <summary>
/// Factory for creating loggers. Exposed as a MEF component.
/// </summary>
[Export(typeof(ILoggerFactory))]
internal class LoggerFactory : ILoggerFactory
{
private readonly SVsServiceProvider serviceProvider;
private Microsoft.Extensions.Logging.ILoggerFactory loggerFactory;
private bool initialized = false;
private readonly object lockObject = new object();

/// <summary>
/// Initializes a new instance of the <see cref="LoggerFactory"/> class.
/// </summary>
/// <param name="serviceProvider">The service provider to get options.</param>
[ImportingConstructor]
public LoggerFactory(SVsServiceProvider serviceProvider)
{
this.serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
}

/// <inheritdoc />
public Microsoft.Extensions.Logging.ILogger CreateLogger(string categoryName)
{
EnsureInitialized();
return loggerFactory.CreateLogger(categoryName);
}

/// <inheritdoc />
public void AddProvider(ILoggerProvider provider)
{
EnsureInitialized();
loggerFactory.AddProvider(provider);
}

/// <inheritdoc />
public void Dispose()
{
loggerFactory?.Dispose();
}

private void EnsureInitialized()
{
if (initialized)
{
return;
}

lock (lockObject)
{
if (initialized)
{
return;
}

try
{
// Get the options page accessor service
var optionsPageAccessor = serviceProvider.GetService(typeof(SOptionsPageAccessor)) as IOptionsPageAccessor;
LogLevel minLogLevel = LogLevel.None;

if (optionsPageAccessor != null)
{
minLogLevel = optionsPageAccessor.QueryLogLevel();
}

if (minLogLevel == LogLevel.None)
{
// Create a null logger factory when logging is disabled
loggerFactory = new NullLoggerFactory();
}
else
{
// Configure Serilog
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
var logDirectory = Path.Combine(localAppData, "RemoteDebuggerLauncher", "Logfiles");
var timestamp = DateTime.Now.ToString("yyyyMMdd-HHmmss", CultureInfo.InvariantCulture);
var logFilePath = Path.Combine(logDirectory, $"RemoteDebuggerLauncher-{timestamp}.log");

// Ensure directory exists
if (!Directory.Exists(logDirectory))
{
Directory.CreateDirectory(logDirectory);
}

// Configure Serilog logger
var serilogLogger = new LoggerConfiguration()
.MinimumLevel.Is(MapToSerilogLevel(minLogLevel))
.WriteTo.File(
logFilePath,
outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff}] [{Level:u5}] {SourceContext}: {Message:lj}{NewLine}{Exception}",
formatProvider: CultureInfo.InvariantCulture)
.CreateLogger();

// Create Microsoft.Extensions.Logging factory with Serilog
loggerFactory = new SerilogLoggerFactory(serilogLogger, dispose: true);
}
}
catch
{
// If we can't configure logging, use null logger factory
loggerFactory = new NullLoggerFactory();
}

initialized = true;
}
}

private static Serilog.Events.LogEventLevel MapToSerilogLevel(LogLevel logLevel)
{
switch (logLevel)
{
case LogLevel.Trace:
return Serilog.Events.LogEventLevel.Verbose;
case LogLevel.Debug:
return Serilog.Events.LogEventLevel.Debug;
case LogLevel.Information:
return Serilog.Events.LogEventLevel.Information;
case LogLevel.Warning:
return Serilog.Events.LogEventLevel.Warning;
case LogLevel.Error:
return Serilog.Events.LogEventLevel.Error;
case LogLevel.Critical:
return Serilog.Events.LogEventLevel.Fatal;
default:
return Serilog.Events.LogEventLevel.Information;
}
}
}
}
58 changes: 58 additions & 0 deletions src/Extension/RemoteDebuggerLauncher/Logging/NullLogger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// ----------------------------------------------------------------------------
// <copyright company="Michael Koster">
// Copyright (c) Michael Koster. All rights reserved.
// Licensed under the MIT License.
// </copyright>
// ----------------------------------------------------------------------------

using System;
using Microsoft.Extensions.Logging;

namespace RemoteDebuggerLauncher.Logging
{
/// <summary>
/// A logger implementation that does nothing. Used when logging is disabled.
/// </summary>
internal class NullLogger : ILogger
{
/// <summary>
/// Gets the singleton instance of the null logger.
/// </summary>
public static NullLogger Instance { get; } = new NullLogger();

/// <inheritdoc />
public IDisposable BeginScope<TState>(TState state)
{
return NullScope.Instance;
}

/// <inheritdoc />
public bool IsEnabled(LogLevel logLevel)
{
return false;
}

/// <inheritdoc />
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
// Do nothing
}

/// <summary>
/// A no-op disposable for scope handling.
/// </summary>
private sealed class NullScope : IDisposable
{
public static NullScope Instance { get; } = new NullScope();

private NullScope()
{
}

public void Dispose()
{
// Do nothing
}
}
}
}
35 changes: 35 additions & 0 deletions src/Extension/RemoteDebuggerLauncher/Logging/NullLoggerFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// ----------------------------------------------------------------------------
// <copyright company="Michael Koster">
// Copyright (c) Michael Koster. All rights reserved.
// Licensed under the MIT License.
// </copyright>
// ----------------------------------------------------------------------------

using Microsoft.Extensions.Logging;

namespace RemoteDebuggerLauncher.Logging
{
/// <summary>
/// A logger factory that creates null loggers. Used when logging is disabled.
/// </summary>
internal class NullLoggerFactory : ILoggerFactory
{
/// <inheritdoc />
public ILogger CreateLogger(string categoryName)
{
return NullLogger.Instance;
}

/// <inheritdoc />
public void AddProvider(ILoggerProvider provider)
{
// Do nothing
}

/// <inheritdoc />
public void Dispose()
{
// Nothing to dispose
}
}
}
91 changes: 91 additions & 0 deletions src/Extension/RemoteDebuggerLauncher/Logging/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Logging Infrastructure

This directory contains the logging infrastructure for the Remote Debugger Launcher extension.

## Overview

The logging system uses **Serilog** with Microsoft's `Microsoft.Extensions.Logging` integration and provides:

- **LogLevel-based filtering**: Trace, Debug, Information, Warning, Error, Critical, and None
- **File-based logging**: Logs are written to `%localappdata%\RemoteDebuggerLauncher\Logfiles`
- **Structured logging**: Serilog provides rich structured logging capabilities
- **MEF integration**: Loggers can be injected via MEF's `[ImportingConstructor]`
- **Configuration**: Logging level can be configured in Visual Studio Options

## Packages Used

- **Serilog**: Core structured logging library
- **Serilog.Extensions.Logging**: Bridge between Serilog and Microsoft.Extensions.Logging
- **Serilog.Sinks.File**: File sink for writing logs to disk
- **Microsoft.Extensions.Logging.Abstractions**: Standard logging abstractions

## Files

- **LoggerFactory.cs**: MEF-exportable factory that configures Serilog and creates loggers
- **NullLogger.cs**: No-op logger used when logging is disabled
- **NullLoggerFactory.cs**: Factory that creates null loggers when logging is disabled

## Usage

### Injecting a Logger via MEF

```csharp
using Microsoft.Extensions.Logging;

[Export]
internal class MyService
{
private readonly ILogger logger;

[ImportingConstructor]
public MyService(ILoggerFactory loggerFactory)
{
logger = loggerFactory.CreateLogger(nameof(MyService));
}

public void DoWork()
{
logger.LogInformation("Starting work");
try
{
// Do work
logger.LogDebug("Work completed successfully");
}
catch (Exception ex)
{
logger.LogError(ex, "Work failed");
}
}
}
```

### Configuration

Users can configure the logging level in Visual Studio:
1. Go to **Tools > Options**
2. Navigate to **RemoteDebuggerLauncher > Local**
3. Set the **Log level** property to the desired level
4. Set to **None** to disable logging

## Log File Location

Log files are created in: `%localappdata%\RemoteDebuggerLauncher\Logfiles\RemoteDebuggerLauncher-{timestamp}.log`

Each session creates a new log file with a timestamp in the format: `yyyyMMdd-HHmmss`

## Log Format

Serilog uses the following format template:

```
[{Timestamp:yyyy-MM-dd HH:mm:ss.fff}] [{Level:u5}] {SourceContext}: {Message:lj}{NewLine}{Exception}
```

Example output:

```
[2024-01-05 07:30:45.123] [INFO ] SecureShellKeySetupService: Starting server fingerprint registration for user@host:22
[2024-01-05 07:30:45.456] [ERROR] SecureShellKeySetupService: Failed to register server fingerprint
System.InvalidOperationException: Connection failed
at ...
```
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// </copyright>
// ----------------------------------------------------------------------------

using Microsoft.Extensions.Logging;
using RemoteDebuggerLauncher.Shared;

namespace RemoteDebuggerLauncher
Expand Down Expand Up @@ -85,5 +86,11 @@ internal interface IOptionsPageAccessor
/// </summary>
/// <returns>One of the <see see="PublishMode"/> values.</returns>
PublishMode QueryPublishMode();

/// <summary>
/// Queries the logging level.
/// </summary>
/// <returns>One of the <see cref="LogLevel"/> values.</returns>
LogLevel QueryLogLevel();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

using System.ComponentModel;
using System.Runtime.InteropServices;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.Shell;
using RemoteDebuggerLauncher.Shared;

Expand All @@ -28,5 +29,11 @@ internal class LocalOptionsPage : DialogPage
[DisplayName("Publish mode")]
[Description("The type of application the publish step should produce, either self contained (includes the runtime) or framework dependent (requires .NET to be installed on the device.")]
public PublishMode PublishMode { get; set; } = PublishMode.FrameworkDependent;

[Category(PackageConstants.Options.PageCategoryDiagnostics)]
[DisplayName("Log level")]
[Description("The minimum logging level for diagnostics. Set to 'None' to disable logging.")]
[DefaultValue(LogLevel.None)]
public LogLevel LogLevel { get; set; } = LogLevel.None;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
// ----------------------------------------------------------------------------

using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Logging;
using RemoteDebuggerLauncher.Shared;

namespace RemoteDebuggerLauncher
Expand Down Expand Up @@ -103,6 +104,12 @@ public PublishMode QueryPublishMode()
return GetLocalPage().PublishMode;
}

/// <inheritdoc />
public LogLevel QueryLogLevel()
{
return GetLocalPage().LogLevel;
}

private DeviceOptionsPage GetDevicePage()
{
if (devicePage == null)
Expand Down
Loading
Loading