From 475fe6569ceab0803a46e79e6e55eaee9ecaa29f Mon Sep 17 00:00:00 2001 From: Jason Malinowski Date: Thu, 21 Sep 2023 19:58:11 -0700 Subject: [PATCH 1/2] Use ILogger for logging in the LSIF generator Previously we were just passing around a TextWriter which wasn't great. --- src/Features/Lsif/Generator/Generator.cs | 14 +-- .../Lsif/Generator/Logging/PlainTextLogger.cs | 46 ++++++++++ src/Features/Lsif/Generator/Program.cs | 90 ++++++++++--------- .../GeneratorTest/Utilities/TestLsifOutput.vb | 34 +++++-- 4 files changed, 130 insertions(+), 54 deletions(-) create mode 100644 src/Features/Lsif/Generator/Logging/PlainTextLogger.cs diff --git a/src/Features/Lsif/Generator/Generator.cs b/src/Features/Lsif/Generator/Generator.cs index e64f9d76a7561..f91931dab5c2d 100644 --- a/src/Features/Lsif/Generator/Generator.cs +++ b/src/Features/Lsif/Generator/Generator.cs @@ -18,6 +18,7 @@ using Microsoft.CodeAnalysis.LanguageServerIndexFormat.Generator.Writing; using Microsoft.CodeAnalysis.LanguageService; using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.LanguageServer.Protocol; using Roslyn.Utilities; using LspProtocol = Microsoft.VisualStudio.LanguageServer.Protocol; @@ -53,18 +54,18 @@ internal sealed class Generator }; private readonly ILsifJsonWriter _lsifJsonWriter; - private readonly TextWriter _logFile; + private readonly ILogger _logger; private readonly IdFactory _idFactory = new IdFactory(); - private Generator(ILsifJsonWriter lsifJsonWriter, TextWriter logFile) + private Generator(ILsifJsonWriter lsifJsonWriter, ILogger logger) { _lsifJsonWriter = lsifJsonWriter; - _logFile = logFile; + _logger = logger; } - public static Generator CreateAndWriteCapabilitiesVertex(ILsifJsonWriter lsifJsonWriter, TextWriter logFile) + public static Generator CreateAndWriteCapabilitiesVertex(ILsifJsonWriter lsifJsonWriter, ILogger logger) { - var generator = new Generator(lsifJsonWriter, logFile); + var generator = new Generator(lsifJsonWriter, logger); // Pass the set of supported SemanticTokenTypes. Order must match the order used for serialization of // semantic tokens array. This array is analogous to the equivalent array in @@ -167,8 +168,7 @@ public async Task GenerateForProjectAsync( var exception = tasks[i].Exception!.InnerExceptions.Single(); exceptions.Add(exception); - await _logFile.WriteLineAsync($"Exception while processing {documents[i].FilePath}:"); - await _logFile.WriteLineAsync(exception.ToString()); + _logger.LogError(exception, $"Exception while processing {documents[i].FilePath}"); } } diff --git a/src/Features/Lsif/Generator/Logging/PlainTextLogger.cs b/src/Features/Lsif/Generator/Logging/PlainTextLogger.cs new file mode 100644 index 0000000000000..c250270946991 --- /dev/null +++ b/src/Features/Lsif/Generator/Logging/PlainTextLogger.cs @@ -0,0 +1,46 @@ +// 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; +using System.IO; +using Microsoft.Extensions.Logging; + +namespace Microsoft.CodeAnalysis.LanguageServerIndexFormat.Generator.Logging +{ + internal sealed class PlainTextLogger : ILogger + { + private readonly TextWriter _writer; + private readonly object _gate = new object(); + + public PlainTextLogger(TextWriter writer) + { + _writer = writer; + } + + public IDisposable BeginScope(TState state) + { + throw new NotImplementedException(); + } + + public bool IsEnabled(LogLevel logLevel) + { + return true; + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + // Not all formatters will actually include the exception even if we pass it through, so include it here. + var message = formatter(state, exception); + var exceptionString = exception?.ToString(); + + lock (_gate) + { + _writer.WriteLine(message); + + if (exceptionString != null) + _writer.WriteLine(exceptionString); + } + } + } +} diff --git a/src/Features/Lsif/Generator/Program.cs b/src/Features/Lsif/Generator/Program.cs index 8c213568ed7ff..88e6b4b4bea5e 100644 --- a/src/Features/Lsif/Generator/Program.cs +++ b/src/Features/Lsif/Generator/Program.cs @@ -13,9 +13,12 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Build.Locator; +using Microsoft.CodeAnalysis.LanguageServerIndexFormat.Generator.Logging; using Microsoft.CodeAnalysis.LanguageServerIndexFormat.Generator.Writing; using Microsoft.CodeAnalysis.MSBuild; using Microsoft.CodeAnalysis.Shared.Extensions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Roslyn.Utilities; using CompilerInvocationsReader = Microsoft.Build.Logging.StructuredLogger.CompilerInvocationsReader; @@ -53,9 +56,9 @@ public static Task Main(string[] args) project: parseResult.GetValue(project), compilerInvocation: parseResult.GetValue(compilerInvocation), binLog: parseResult.GetValue(binLog), - output: parseResult.GetValue(output), + outputFileName: parseResult.GetValue(output), outputFormat: parseResult.GetValue(outputFormat), - log: parseResult.GetValue(log), + logFileName: parseResult.GetValue(log), cancellationToken); }); @@ -67,13 +70,13 @@ private static async Task GenerateAsync( FileInfo? project, FileInfo? compilerInvocation, FileInfo? binLog, - string? output, + string? outputFileName, LsifFormat outputFormat, - string? log, + string? logFileName, CancellationToken cancellationToken) { // If we have an output file, we'll write to that, else we'll use Console.Out - using var outputFile = output != null ? new StreamWriter(output, append: false, Encoding.UTF8) : null; + using var outputFile = outputFileName != null ? new StreamWriter(outputFileName, append: false, Encoding.UTF8) : null; TextWriter outputWriter; if (outputFile is null) @@ -86,7 +89,6 @@ private static async Task GenerateAsync( outputWriter = outputFile; } - using var logFile = log != null ? new StreamWriter(log) { AutoFlush = true } : TextWriter.Null; ILsifJsonWriter lsifWriter = outputFormat switch { LsifFormat.Json => new JsonModeLsifJsonWriter(outputWriter), @@ -94,6 +96,14 @@ private static async Task GenerateAsync( _ => throw new NotImplementedException() }; + using var logFile = logFileName is not null and not "stderr" ? new StreamWriter(logFileName) { AutoFlush = true } : null; + + ILogger logger; + if (logFile is not null) + logger = new PlainTextLogger(logFile); + else + logger = NullLogger.Instance; + var totalExecutionTime = Stopwatch.StartNew(); try @@ -109,17 +119,17 @@ private static async Task GenerateAsync( if (solution != null) { - await LocateAndRegisterMSBuild(logFile, solution.Directory); - await GenerateFromSolutionAsync(solution, lsifWriter, logFile, cancellationToken); + LocateAndRegisterMSBuild(logger, solution.Directory); + await GenerateFromSolutionAsync(solution, lsifWriter, logger, cancellationToken); } else if (project != null) { - await LocateAndRegisterMSBuild(logFile, project.Directory); - await GenerateFromProjectAsync(project, lsifWriter, logFile, cancellationToken); + LocateAndRegisterMSBuild(logger, project.Directory); + await GenerateFromProjectAsync(project, lsifWriter, logger, cancellationToken); } else if (compilerInvocation != null) { - await GenerateFromCompilerInvocationAsync(compilerInvocation, lsifWriter, logFile, cancellationToken); + await GenerateFromCompilerInvocationAsync(compilerInvocation, lsifWriter, logger, cancellationToken); } else { @@ -129,24 +139,24 @@ private static async Task GenerateAsync( // any MSBuild builds or tasks/targets in our process. Since we're reading a binlog, simply none of the SDK will be loaded. We might load analyzers // or source generators from the SDK or user-built, but those must generally target netstandard2.0 so we don't really expect them to have problems loading // on one version of the runtime versus another. - await LocateAndRegisterMSBuild(logFile, sourceDirectory: null); - await GenerateFromBinaryLogAsync(binLog, lsifWriter, logFile, cancellationToken); + LocateAndRegisterMSBuild(logger, sourceDirectory: null); + await GenerateFromBinaryLogAsync(binLog, lsifWriter, logger, cancellationToken); } } catch (Exception e) { // If it failed, write out to the logs, but propagate the error too var message = "Unhandled exception: " + e.ToString(); - await logFile.WriteLineAsync(message); + logger.LogCritical(e, message); // System.CommandLine is going to catch the exception and log it in standard error throw; } (lsifWriter as IDisposable)?.Dispose(); - await logFile.WriteLineAsync($"Generation complete. Total execution time: {totalExecutionTime.Elapsed.ToDisplayString()}"); + logger.LogInformation($"Generation complete. Total execution time: {totalExecutionTime.Elapsed.ToDisplayString()}"); } - private static async Task LocateAndRegisterMSBuild(TextWriter logFile, DirectoryInfo? sourceDirectory) + private static void LocateAndRegisterMSBuild(ILogger logger, DirectoryInfo? sourceDirectory) { // Make sure we pick the highest version var options = VisualStudioInstanceQueryOptions.Default; @@ -161,17 +171,17 @@ private static async Task LocateAndRegisterMSBuild(TextWriter logFile, Directory } else { - await logFile.WriteLineAsync($"Using the MSBuild instance located at {msBuildInstance.MSBuildPath}."); + logger.LogInformation($"Using the MSBuild instance located at {msBuildInstance.MSBuildPath}."); } MSBuildLocator.RegisterInstance(msBuildInstance); } private static async Task GenerateFromProjectAsync( - FileInfo projectFile, ILsifJsonWriter lsifWriter, TextWriter logFile, CancellationToken cancellationToken) + FileInfo projectFile, ILsifJsonWriter lsifWriter, ILogger logger, CancellationToken cancellationToken) { await GenerateWithMSBuildWorkspaceAsync( - projectFile, lsifWriter, logFile, + projectFile, lsifWriter, logger, async (workspace, cancellationToken) => { var project = await workspace.OpenProjectAsync(projectFile.FullName, cancellationToken: cancellationToken); @@ -181,10 +191,10 @@ await GenerateWithMSBuildWorkspaceAsync( } private static async Task GenerateFromSolutionAsync( - FileInfo solutionFile, ILsifJsonWriter lsifWriter, TextWriter logFile, CancellationToken cancellationToken) + FileInfo solutionFile, ILsifJsonWriter lsifWriter, ILogger logger, CancellationToken cancellationToken) { await GenerateWithMSBuildWorkspaceAsync( - solutionFile, lsifWriter, logFile, + solutionFile, lsifWriter, logger, (workspace, cancellationToken) => workspace.OpenSolutionAsync(solutionFile.FullName, cancellationToken: cancellationToken), cancellationToken); } @@ -195,23 +205,23 @@ await GenerateWithMSBuildWorkspaceAsync( private static async Task GenerateWithMSBuildWorkspaceAsync( FileInfo solutionOrProjectFile, ILsifJsonWriter lsifWriter, - TextWriter logFile, + ILogger logger, Func> openAsync, CancellationToken cancellationToken) { - await logFile.WriteLineAsync($"Loading {solutionOrProjectFile.FullName}..."); + logger.LogInformation($"Loading {solutionOrProjectFile.FullName}..."); var solutionLoadStopwatch = Stopwatch.StartNew(); using var msbuildWorkspace = MSBuildWorkspace.Create(await Composition.CreateHostServicesAsync()); - msbuildWorkspace.WorkspaceFailed += (s, e) => logFile.WriteLine("Error while loading: " + e.Diagnostic.Message); + msbuildWorkspace.WorkspaceFailed += (s, e) => logger.Log(e.Diagnostic.Kind == WorkspaceDiagnosticKind.Failure ? LogLevel.Error : LogLevel.Warning, "Problem while loading: " + e.Diagnostic.Message); var solution = await openAsync(msbuildWorkspace, cancellationToken); var options = GeneratorOptions.Default; - await logFile.WriteLineAsync($"Load completed in {solutionLoadStopwatch.Elapsed.ToDisplayString()}."); - var lsifGenerator = Generator.CreateAndWriteCapabilitiesVertex(lsifWriter, logFile); + logger.LogInformation($"Load completed in {solutionLoadStopwatch.Elapsed.ToDisplayString()}."); + var lsifGenerator = Generator.CreateAndWriteCapabilitiesVertex(lsifWriter, logger); var totalTimeInGenerationAndCompilationFetchStopwatch = Stopwatch.StartNew(); var totalTimeInGenerationPhase = TimeSpan.Zero; @@ -223,7 +233,7 @@ private static async Task GenerateWithMSBuildWorkspaceAsync( var compilationCreationStopwatch = Stopwatch.StartNew(); var compilation = await project.GetRequiredCompilationAsync(cancellationToken); - await logFile.WriteLineAsync($"Fetch of compilation for {project.FilePath} completed in {compilationCreationStopwatch.Elapsed.ToDisplayString()}."); + logger.LogInformation($"Fetch of compilation for {project.FilePath} completed in {compilationCreationStopwatch.Elapsed.ToDisplayString()}."); var generationForProjectStopwatch = Stopwatch.StartNew(); await lsifGenerator.GenerateForProjectAsync(project, options, cancellationToken); @@ -231,41 +241,41 @@ private static async Task GenerateWithMSBuildWorkspaceAsync( totalTimeInGenerationPhase += generationForProjectStopwatch.Elapsed; - await logFile.WriteLineAsync($"Generation for {project.FilePath} completed in {generationForProjectStopwatch.Elapsed.ToDisplayString()}."); + logger.LogInformation($"Generation for {project.FilePath} completed in {generationForProjectStopwatch.Elapsed.ToDisplayString()}."); } } - await logFile.WriteLineAsync($"Total time spent in the generation phase for all projects, excluding compilation fetch time: {totalTimeInGenerationPhase.ToDisplayString()}"); - await logFile.WriteLineAsync($"Total time spent in the generation phase for all projects, including compilation fetch time: {totalTimeInGenerationAndCompilationFetchStopwatch.Elapsed.ToDisplayString()}"); + logger.LogInformation($"Total time spent in the generation phase for all projects, excluding compilation fetch time: {totalTimeInGenerationPhase.ToDisplayString()}"); + logger.LogInformation($"Total time spent in the generation phase for all projects, including compilation fetch time: {totalTimeInGenerationAndCompilationFetchStopwatch.Elapsed.ToDisplayString()}"); } private static async Task GenerateFromCompilerInvocationAsync( - FileInfo compilerInvocationFile, ILsifJsonWriter lsifWriter, TextWriter logFile, CancellationToken cancellationToken) + FileInfo compilerInvocationFile, ILsifJsonWriter lsifWriter, ILogger logger, CancellationToken cancellationToken) { - await logFile.WriteLineAsync($"Processing compiler invocation from {compilerInvocationFile.FullName}..."); + logger.LogInformation($"Processing compiler invocation from {compilerInvocationFile.FullName}..."); var compilerInvocationLoadStopwatch = Stopwatch.StartNew(); var project = await CompilerInvocation.CreateFromJsonAsync(File.ReadAllText(compilerInvocationFile.FullName)); - await logFile.WriteLineAsync($"Load of the project completed in {compilerInvocationLoadStopwatch.Elapsed.ToDisplayString()}."); + logger.LogInformation($"Load of the project completed in {compilerInvocationLoadStopwatch.Elapsed.ToDisplayString()}."); var generationStopwatch = Stopwatch.StartNew(); - var lsifGenerator = Generator.CreateAndWriteCapabilitiesVertex(lsifWriter, logFile); + var lsifGenerator = Generator.CreateAndWriteCapabilitiesVertex(lsifWriter, logger); await lsifGenerator.GenerateForProjectAsync(project, GeneratorOptions.Default, cancellationToken); - await logFile.WriteLineAsync($"Generation for {project.FilePath} completed in {generationStopwatch.Elapsed.ToDisplayString()}."); + logger.LogInformation($"Generation for {project.FilePath} completed in {generationStopwatch.Elapsed.ToDisplayString()}."); } // This method can't be loaded until we've registered MSBuild with MSBuildLocator, as otherwise we might load a type prematurely. [MethodImpl(MethodImplOptions.NoInlining)] private static async Task GenerateFromBinaryLogAsync( - FileInfo binLog, ILsifJsonWriter lsifWriter, TextWriter logFile, CancellationToken cancellationToken) + FileInfo binLog, ILsifJsonWriter lsifWriter, ILogger logger, CancellationToken cancellationToken) { - await logFile.WriteLineAsync($"Reading binlog {binLog.FullName}..."); + logger.LogInformation($"Reading binlog {binLog.FullName}..."); var msbuildInvocations = CompilerInvocationsReader.ReadInvocations(binLog.FullName).ToImmutableArray(); - await logFile.WriteLineAsync($"Load of the binlog complete; {msbuildInvocations.Length} invocations were found."); + logger.LogInformation($"Load of the binlog complete; {msbuildInvocations.Length} invocations were found."); - var lsifGenerator = Generator.CreateAndWriteCapabilitiesVertex(lsifWriter, logFile); + var lsifGenerator = Generator.CreateAndWriteCapabilitiesVertex(lsifWriter, logger); using var workspace = new AdhocWorkspace(await Composition.CreateHostServicesAsync()); foreach (var msbuildInvocation in msbuildInvocations) @@ -284,7 +294,7 @@ private static async Task GenerateFromBinaryLogAsync( var generationStopwatch = Stopwatch.StartNew(); await lsifGenerator.GenerateForProjectAsync(project, GeneratorOptions.Default, cancellationToken); - await logFile.WriteLineAsync($"Generation for {project.FilePath} completed in {generationStopwatch.Elapsed.ToDisplayString()}."); + logger.LogInformation($"Generation for {project.FilePath} completed in {generationStopwatch.Elapsed.ToDisplayString()}."); // Remove the project from the workspace; we reuse the same workspace object to ensure that some workspace-level services (like the IMetadataService // or IDocumentationProviderService) are kept around allowing their caches to be reused. diff --git a/src/Features/Lsif/GeneratorTest/Utilities/TestLsifOutput.vb b/src/Features/Lsif/GeneratorTest/Utilities/TestLsifOutput.vb index 9605606563494..312a34fb0910b 100644 --- a/src/Features/Lsif/GeneratorTest/Utilities/TestLsifOutput.vb +++ b/src/Features/Lsif/GeneratorTest/Utilities/TestLsifOutput.vb @@ -2,16 +2,17 @@ ' The .NET Foundation licenses this file to you under the MIT license. ' See the LICENSE file in the project root for more information. +Imports System.Collections.Concurrent Imports System.Collections.Immutable +Imports System.Threading Imports Microsoft.CodeAnalysis.Editor.UnitTests.Workspaces Imports Microsoft.CodeAnalysis.LanguageServerIndexFormat.Generator.Graph Imports Microsoft.CodeAnalysis.LanguageServerIndexFormat.Generator.Writing +Imports Microsoft.CodeAnalysis.Test.Utilities Imports Microsoft.CodeAnalysis.Text -Imports LSP = Microsoft.VisualStudio.LanguageServer.Protocol +Imports Microsoft.Extensions.Logging Imports Roslyn.Utilities -Imports Microsoft.CodeAnalysis.Test.Utilities -Imports System.Threading -Imports System.IO +Imports LSP = Microsoft.VisualStudio.LanguageServer.Protocol Namespace Microsoft.CodeAnalysis.LanguageServerIndexFormat.Generator.UnitTests.Utilities Friend Class TestLsifOutput @@ -46,8 +47,8 @@ Namespace Microsoft.CodeAnalysis.LanguageServerIndexFormat.Generator.UnitTests.U ' world function of the indexer. Assert.Equal(workspace.Composition, TestComposition) - Dim log = New StringWriter() - Dim lsifGenerator = Generator.CreateAndWriteCapabilitiesVertex(jsonWriter, log) + Dim logger = New TestLogger() + Dim lsifGenerator = Generator.CreateAndWriteCapabilitiesVertex(jsonWriter, logger) For Each project In workspace.CurrentSolution.Projects Dim compilation = Await project.GetCompilationAsync() @@ -59,9 +60,28 @@ Namespace Microsoft.CodeAnalysis.LanguageServerIndexFormat.Generator.UnitTests.U Next ' The only things would have logged were an error, so this should be empty - Assert.Empty(log.ToString()) + Assert.Empty(logger.LoggedMessages) End Function + Private Class TestLogger + Implements ILogger + + Public ReadOnly LoggedMessages As New ConcurrentBag(Of String) + + Public Sub Log(Of TState)(logLevel As LogLevel, eventId As EventId, state As TState, exception As Exception, formatter As Func(Of TState, Exception, String)) Implements ILogger.Log + Dim message = formatter(state, exception) + LoggedMessages.Add(message) + End Sub + + Public Function IsEnabled(logLevel As LogLevel) As Boolean Implements ILogger.IsEnabled + Return True + End Function + + Public Function BeginScope(Of TState)(state As TState) As IDisposable Implements ILogger.BeginScope + Throw New NotImplementedException() + End Function + End Class + Public Function GetElementById(Of T As Element)(id As Id(Of T)) As T Return _testLsifJsonWriter.GetElementById(id) End Function From 3f27bc1786300f014697522ab73fce2b66491956 Mon Sep 17 00:00:00 2001 From: Jason Malinowski Date: Thu, 21 Sep 2023 20:31:19 -0700 Subject: [PATCH 2/2] Implement support in the LSIF generator for logging to stderr LSIF tools can implement an optional logging format which is written to stderr; this implements that format. --- .../Generator/Logging/LsifFormatLogger.cs | 58 +++++++++++++++++++ src/Features/Lsif/Generator/Program.cs | 2 + .../Writing/LineModeLsifJsonWriter.cs | 22 +++---- .../GeneratorTest/LsifFormatLoggerTests.vb | 35 +++++++++++ 4 files changed, 106 insertions(+), 11 deletions(-) create mode 100644 src/Features/Lsif/Generator/Logging/LsifFormatLogger.cs create mode 100644 src/Features/Lsif/GeneratorTest/LsifFormatLoggerTests.vb diff --git a/src/Features/Lsif/Generator/Logging/LsifFormatLogger.cs b/src/Features/Lsif/Generator/Logging/LsifFormatLogger.cs new file mode 100644 index 0000000000000..f320c41d69b1a --- /dev/null +++ b/src/Features/Lsif/Generator/Logging/LsifFormatLogger.cs @@ -0,0 +1,58 @@ +// 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; +using System.IO; +using Microsoft.CodeAnalysis.LanguageServerIndexFormat.Generator.Writing; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json; +using Roslyn.Utilities; + +namespace Microsoft.CodeAnalysis.LanguageServerIndexFormat.Generator.Logging +{ + internal sealed class LsifFormatLogger : ILogger + { + private readonly TextWriter _writer; + private readonly object _writerGate = new object(); + + public LsifFormatLogger(TextWriter writer) + { + _writer = writer; + } + + public IDisposable BeginScope(TState state) + { + throw new NotImplementedException(); + } + + public bool IsEnabled(LogLevel logLevel) + { + return logLevel >= LogLevel.Information; + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + var message = formatter(state, exception); + + var severity = logLevel switch { LogLevel.Information => "Info", LogLevel.Warning => "Warning", LogLevel.Error => "Error", LogLevel.Critical => "Critical", _ => throw ExceptionUtilities.UnexpectedValue(logLevel) }; + + var command = new CommandWithParameters("log", + new LogCommandParameters(severity, message, exception?.Message, exception?.GetType().ToString(), exception?.StackTrace)); + var serializedCommand = JsonConvert.SerializeObject(command, LineModeLsifJsonWriter.SerializerSettings); + + lock (_writerGate) + { + _writer.Write(serializedCommand); + } + } + + private sealed record CommandWithParameters(string Command, object Parameters); + private sealed record LogCommandParameters( + string Severity, + string Message, + string? ExceptionMessage, + string? ExceptionType, + string? CallStack); + } +} diff --git a/src/Features/Lsif/Generator/Program.cs b/src/Features/Lsif/Generator/Program.cs index 88e6b4b4bea5e..0ff28997c60c7 100644 --- a/src/Features/Lsif/Generator/Program.cs +++ b/src/Features/Lsif/Generator/Program.cs @@ -101,6 +101,8 @@ private static async Task GenerateAsync( ILogger logger; if (logFile is not null) logger = new PlainTextLogger(logFile); + else if (logFileName == "stderr") + logger = new LsifFormatLogger(Console.Error); else logger = NullLogger.Instance; diff --git a/src/Features/Lsif/Generator/Writing/LineModeLsifJsonWriter.cs b/src/Features/Lsif/Generator/Writing/LineModeLsifJsonWriter.cs index 6a863cabac99f..4b59f548d5fcc 100644 --- a/src/Features/Lsif/Generator/Writing/LineModeLsifJsonWriter.cs +++ b/src/Features/Lsif/Generator/Writing/LineModeLsifJsonWriter.cs @@ -15,26 +15,26 @@ namespace Microsoft.CodeAnalysis.LanguageServerIndexFormat.Generator.Writing /// internal sealed partial class LineModeLsifJsonWriter : ILsifJsonWriter { + public static readonly JsonSerializerSettings SerializerSettings = new JsonSerializerSettings + { + Formatting = Newtonsoft.Json.Formatting.None, + NullValueHandling = NullValueHandling.Ignore, + ContractResolver = new CamelCasePropertyNamesContractResolver(), + TypeNameHandling = TypeNameHandling.None, + Converters = new[] { new LsifConverter() } + }; + private readonly object _writeGate = new object(); private readonly TextWriter _outputWriter; - private readonly JsonSerializerSettings _settings; public LineModeLsifJsonWriter(TextWriter outputWriter) { - _settings = new JsonSerializerSettings - { - Formatting = Newtonsoft.Json.Formatting.None, - NullValueHandling = NullValueHandling.Ignore, - ContractResolver = new CamelCasePropertyNamesContractResolver(), - TypeNameHandling = TypeNameHandling.None, - Converters = new[] { new LsifConverter() } - }; _outputWriter = outputWriter; } public void Write(Element element) { - var line = JsonConvert.SerializeObject(element, _settings); + var line = JsonConvert.SerializeObject(element, SerializerSettings); lock (_writeGate) { @@ -46,7 +46,7 @@ public void WriteAll(List elements) { var lines = new List(); foreach (var element in elements) - lines.Add(JsonConvert.SerializeObject(element, _settings)); + lines.Add(JsonConvert.SerializeObject(element, SerializerSettings)); lock (_writeGate) { diff --git a/src/Features/Lsif/GeneratorTest/LsifFormatLoggerTests.vb b/src/Features/Lsif/GeneratorTest/LsifFormatLoggerTests.vb new file mode 100644 index 0000000000000..221a623226b34 --- /dev/null +++ b/src/Features/Lsif/GeneratorTest/LsifFormatLoggerTests.vb @@ -0,0 +1,35 @@ +' 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. + +Imports System.IO +Imports Microsoft.CodeAnalysis.LanguageServerIndexFormat.Generator.Logging +Imports Microsoft.Extensions.Logging + +Namespace Microsoft.CodeAnalysis.LanguageServerIndexFormat.Generator.UnitTests + Public Class LsifFormatLoggerTests + + + + + + Public Sub TestSimpleMessage(logLevel As LogLevel, expectedSeverity As String) + Dim writer = New StringWriter + Dim logger = New LsifFormatLogger(writer) + + logger.Log(logLevel, "Test message") + + Assert.Equal("{""command"":""log"",""parameters"":{""severity"":""" + expectedSeverity + """,""message"":""Test message""}}", writer.ToString()) + End Sub + + + Public Sub TestException() + Dim writer = New StringWriter + Dim logger = New LsifFormatLogger(writer) + + logger.LogError(New Exception("Exception!"), "An exception was thrown") + + Assert.Equal("{""command"":""log"",""parameters"":{""severity"":""Error"",""message"":""An exception was thrown"",""exceptionMessage"":""Exception!"",""exceptionType"":""System.Exception""}}", writer.ToString()) + End Sub + End Class +End Namespace