Skip to content

Commit

Permalink
Allow the client to update the server logging level dynamically
Browse files Browse the repository at this point in the history
  • Loading branch information
dibarbet committed Oct 25, 2024
1 parent a02225c commit 56374f2
Show file tree
Hide file tree
Showing 7 changed files with 99 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public static Task<ExportProvider> CreateExportProviderAsync(
{
var devKitDependencyPath = includeDevKitComponents ? GetDevKitExtensionPath() : null;
serverConfiguration = new ServerConfiguration(LaunchDebugger: false,
MinimumLogLevel: LogLevel.Trace,
LogConfiguration: new LogConfiguration(LogLevel.Trace),
StarredCompletionsPath: null,
TelemetryLevel: null,
SessionId: null,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Composition;
using System.Text.Json.Serialization;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.LanguageServer.Handler;
using Microsoft.CodeAnalysis.LanguageServer.Logging;
using Microsoft.Extensions.Logging;
using Roslyn.LanguageServer.Protocol;

namespace Microsoft.CodeAnalysis.LanguageServer.LanguageServer.Handler.Logging;

[ExportCSharpVisualBasicStatelessLspService(typeof(UpdateLogLevelHandler)), Shared]
[Method(MethodName)]
[method: ImportingConstructor]
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
internal class UpdateLogLevelHandler(ServerConfiguration serverConfiguration) : ILspServiceNotificationHandler<UpdateLogLevelParams>
{
private const string MethodName = "roslyn/updateLogLevel";

public bool MutatesSolutionState => false;

public bool RequiresLSPSolution => false;

public Task HandleNotificationAsync(UpdateLogLevelParams request, RequestContext requestContext, CancellationToken cancellationToken)
{
var level = Enum.Parse<LogLevel>(request.LogLevelValue);
serverConfiguration.LogConfiguration.UpdateLogLevel(level);
return Task.CompletedTask;
}
}

/// <summary>
/// Request parameters for updating the log level in the server dynamically.
/// </summary>
/// <param name="LogLevelValue">the string value of the <see cref="LogLevel"/> enum</param>
internal record class UpdateLogLevelParams([property: JsonPropertyName("logLevel")] string LogLevelValue);
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,17 @@
using Microsoft.Extensions.Logging;
using Roslyn.LanguageServer.Protocol;
using StreamJsonRpc;
using Roslyn.Utilities;

namespace Microsoft.CodeAnalysis.LanguageServer.Logging;

/// <summary>
/// Implements an ILogger that seamlessly switches from a fallback logger
/// to LSP log messages as soon as the server initializes.
/// </summary>
internal sealed class LspLogMessageLogger : ILogger
internal sealed class LspLogMessageLogger(string categoryName, ILoggerFactory fallbackLoggerFactory, ServerConfiguration serverConfiguration) : ILogger
{
private readonly string _categoryName;
private readonly ILogger _fallbackLogger;

public LspLogMessageLogger(string categoryName, ILoggerFactory fallbackLoggerFactory)
{
_categoryName = categoryName;
_fallbackLogger = fallbackLoggerFactory.CreateLogger(categoryName);
}
private readonly Lazy<ILogger> _fallbackLogger = new(() => fallbackLoggerFactory.CreateLogger(categoryName));

public IDisposable BeginScope<TState>(TState state) where TState : notnull
{
Expand All @@ -31,16 +25,21 @@ public IDisposable BeginScope<TState>(TState state) where TState : notnull

public bool IsEnabled(LogLevel logLevel)
{
return true;
return serverConfiguration.LogConfiguration.GetLogLevel() <= logLevel;
}

public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
if (!IsEnabled(logLevel))
{
return;
}

var server = LanguageServerHost.Instance;
if (server == null)
{
// If the language server has not been initialized yet, log using the fallback logger.
_fallbackLogger.Log(logLevel, eventId, state, exception, formatter);
_fallbackLogger.Value.Log(logLevel, eventId, state, exception, formatter);
return;
}

Expand All @@ -59,22 +58,13 @@ public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Except

if (message != null && logLevel != LogLevel.None)
{
message = $"[{_categoryName}] {message}";
message = $"[{categoryName}] {message}";
try
{
var _ = server.GetRequiredLspService<IClientLanguageServerManager>().SendNotificationAsync(Methods.WindowLogMessageName, new LogMessageParams()
{
Message = message,
MessageType = logLevel switch
{
LogLevel.Trace => MessageType.Log,
LogLevel.Debug => MessageType.Log,
LogLevel.Information => MessageType.Info,
LogLevel.Warning => MessageType.Warning,
LogLevel.Error => MessageType.Error,
LogLevel.Critical => MessageType.Error,
_ => throw new InvalidOperationException($"Unexpected logLevel argument {logLevel}"),
}
MessageType = LogLevelToMessageType(logLevel),
}, CancellationToken.None);
}
catch (Exception ex) when (ex is ObjectDisposedException or ConnectionLostException)
Expand All @@ -84,4 +74,18 @@ public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Except
}
}
}

private static MessageType LogLevelToMessageType(LogLevel logLevel)
{
return logLevel switch
{
LogLevel.Trace => MessageType.Log,
LogLevel.Debug => MessageType.Debug,
LogLevel.Information => MessageType.Info,
LogLevel.Warning => MessageType.Warning,
LogLevel.Error => MessageType.Error,
LogLevel.Critical => MessageType.Error,
_ => throw ExceptionUtilities.UnexpectedValue(logLevel),
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,18 @@
using Microsoft.Extensions.Logging;

namespace Microsoft.CodeAnalysis.LanguageServer.Logging;
internal class LspLogMessageLoggerProvider : ILoggerProvider
internal class LspLogMessageLoggerProvider(ILoggerFactory fallbackLoggerFactory, ServerConfiguration serverConfiguration) : ILoggerProvider
{
private readonly ILoggerFactory _fallbackLoggerFactory;
private readonly ConcurrentDictionary<string, LspLogMessageLogger> _loggers = new(StringComparer.OrdinalIgnoreCase);

public LspLogMessageLoggerProvider(ILoggerFactory fallbackLoggerFactory)
{
_fallbackLoggerFactory = fallbackLoggerFactory;
}

public ILogger CreateLogger(string categoryName)
{
return _loggers.GetOrAdd(categoryName, new LspLogMessageLogger(categoryName, _fallbackLoggerFactory));
return _loggers.GetOrAdd(categoryName, new LspLogMessageLogger(categoryName, fallbackLoggerFactory, serverConfiguration));
}

public void Dispose()
{
_loggers.Clear();
_fallbackLoggerFactory.Dispose();
fallbackLoggerFactory.Dispose();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,18 @@ static async Task RunAsync(ServerConfiguration serverConfiguration, Cancellation
// Create a console logger as a fallback to use before the LSP server starts.
using var loggerFactory = LoggerFactory.Create(builder =>
{
builder.SetMinimumLevel(serverConfiguration.MinimumLogLevel);
// The actual logger is responsible for deciding whether to log based on the current log level.
// The factory should be configured to log everything.
builder.SetMinimumLevel(LogLevel.Trace);
builder.AddProvider(new LspLogMessageLoggerProvider(fallbackLoggerFactory:
// Add a console logger as a fallback for when the LSP server has not finished initializing.
LoggerFactory.Create(builder =>
{
builder.SetMinimumLevel(serverConfiguration.MinimumLogLevel);
builder.SetMinimumLevel(LogLevel.Trace);
builder.AddConsole();
// The console logger outputs control characters on unix for colors which don't render correctly in VSCode.
builder.AddSimpleConsole(formatterOptions => formatterOptions.ColorBehavior = LoggerColorBehavior.Disabled);
})
}), serverConfiguration
));
});

Expand Down Expand Up @@ -249,7 +251,7 @@ static CliRootCommand CreateCommandLineParser()

var serverConfiguration = new ServerConfiguration(
LaunchDebugger: launchDebugger,
MinimumLogLevel: logLevel,
LogConfiguration: new LogConfiguration(logLevel),
StarredCompletionsPath: starredCompletionsPath,
TelemetryLevel: telemetryLevel,
SessionId: sessionId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public void InitializeConfiguration(ServerConfiguration serverConfiguration)

internal record class ServerConfiguration(
bool LaunchDebugger,
LogLevel MinimumLogLevel,
LogConfiguration LogConfiguration,
string? StarredCompletionsPath,
string? TelemetryLevel,
string? SessionId,
Expand All @@ -52,3 +52,23 @@ internal record class ServerConfiguration(
string? RazorSourceGenerator,
string? RazorDesignTimePath,
string ExtensionLogDirectory);

internal class LogConfiguration
{
private int _currentLogLevel;

public LogConfiguration(LogLevel initialLogLevel)
{
_currentLogLevel = (int)initialLogLevel;
}

public void UpdateLogLevel(LogLevel level)
{
Interlocked.Exchange(ref _currentLogLevel, (int)level);
}

public LogLevel GetLogLevel()
{
return (LogLevel)_currentLogLevel;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -154,14 +154,15 @@ static void ReportProgress(BufferedProgress<RunTestsPartialResult> progress, str

private static TraceLevel GetTraceLevel(ServerConfiguration serverConfiguration)
{
return serverConfiguration.MinimumLogLevel switch
var level = serverConfiguration.LogConfiguration.GetLogLevel();
return level switch
{
Microsoft.Extensions.Logging.LogLevel.Trace or Microsoft.Extensions.Logging.LogLevel.Debug => TraceLevel.Verbose,
Microsoft.Extensions.Logging.LogLevel.Information => TraceLevel.Info,
Microsoft.Extensions.Logging.LogLevel.Warning => TraceLevel.Warning,
Microsoft.Extensions.Logging.LogLevel.Error or Microsoft.Extensions.Logging.LogLevel.Critical => TraceLevel.Error,
Microsoft.Extensions.Logging.LogLevel.None => TraceLevel.Off,
_ => throw new InvalidOperationException($"Unexpected log level {serverConfiguration.MinimumLogLevel}"),
_ => throw new InvalidOperationException($"Unexpected log level {level}"),
};
}

Expand Down

0 comments on commit 56374f2

Please sign in to comment.