diff --git a/src/Elastic.Markdown/Diagnostics/DiagnosticsChannel.cs b/src/Elastic.Markdown/Diagnostics/DiagnosticsChannel.cs index 69b3f5f1..fc5737ec 100644 --- a/src/Elastic.Markdown/Diagnostics/DiagnosticsChannel.cs +++ b/src/Elastic.Markdown/Diagnostics/DiagnosticsChannel.cs @@ -58,23 +58,9 @@ public interface IDiagnosticsOutput public void Write(Diagnostic diagnostic); } -public class LogDiagnosticOutput(ILogger logger) : IDiagnosticsOutput -{ - public void Write(Diagnostic diagnostic) - { - if (diagnostic.Severity == Severity.Error) - logger.LogError($"{diagnostic.Message} ({diagnostic.File}:{diagnostic.Line})"); - else - logger.LogWarning($"{diagnostic.Message} ({diagnostic.File}:{diagnostic.Line})"); - } -} - -public class DiagnosticsCollector(ILoggerFactory loggerFactory, IReadOnlyCollection outputs) +public class DiagnosticsCollector(IReadOnlyCollection outputs) : IHostedService { - private readonly IReadOnlyCollection _outputs = - [new LogDiagnosticOutput(loggerFactory.CreateLogger()), .. outputs]; - public DiagnosticsChannel Channel { get; } = new(); private int _errors; @@ -117,7 +103,7 @@ void Drain() IncrementSeverityCount(item); HandleItem(item); OffendingFiles.Add(item.File); - foreach (var output in _outputs) + foreach (var output in outputs) output.Write(item); } } diff --git a/src/docs-builder/Cli/Commands.cs b/src/docs-builder/Cli/Commands.cs index 88dd4b2a..522e07cd 100644 --- a/src/docs-builder/Cli/Commands.cs +++ b/src/docs-builder/Cli/Commands.cs @@ -5,6 +5,7 @@ using Actions.Core.Services; using ConsoleAppFramework; using Documentation.Builder.Diagnostics; +using Documentation.Builder.Diagnostics.Console; using Documentation.Builder.Http; using Elastic.Markdown; using Elastic.Markdown.IO; @@ -27,6 +28,8 @@ public async Task Serve(string? path = null, Cancel ctx = default) { var host = new DocumentationWebHost(path, logger, new FileSystem()); await host.RunAsync(ctx); + await host.StopAsync(ctx); + } /// diff --git a/src/docs-builder/Diagnostics/Console/ConsoleDiagnosticsCollector.cs b/src/docs-builder/Diagnostics/Console/ConsoleDiagnosticsCollector.cs new file mode 100644 index 00000000..6edb423d --- /dev/null +++ b/src/docs-builder/Diagnostics/Console/ConsoleDiagnosticsCollector.cs @@ -0,0 +1,33 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using Actions.Core.Services; +using Elastic.Markdown.Diagnostics; +using Microsoft.Extensions.Logging; +using Spectre.Console; +using Diagnostic = Elastic.Markdown.Diagnostics.Diagnostic; + +namespace Documentation.Builder.Diagnostics.Console; + +public class ConsoleDiagnosticsCollector(ILoggerFactory loggerFactory, ICoreService? githubActions = null) + : DiagnosticsCollector([new Log(loggerFactory.CreateLogger()), new GithubAnnotationOutput(githubActions)] + ) +{ + private readonly List _items = new(); + + protected override void HandleItem(Diagnostic diagnostic) => _items.Add(diagnostic); + + public override async Task StopAsync(Cancel ctx) + { + var repository = new ErrataFileSourceRepository(); + repository.WriteDiagnosticsToConsole(_items); + + AnsiConsole.WriteLine(); + AnsiConsole.Write(new Markup($" [bold red]{Errors} Errors[/] / [bold blue]{Warnings} Warnings[/]")); + AnsiConsole.WriteLine(); + AnsiConsole.WriteLine(); + + await Task.CompletedTask; + } +} diff --git a/src/docs-builder/Diagnostics/Console/ErrataFileSourceRepository.cs b/src/docs-builder/Diagnostics/Console/ErrataFileSourceRepository.cs new file mode 100644 index 00000000..5448dc18 --- /dev/null +++ b/src/docs-builder/Diagnostics/Console/ErrataFileSourceRepository.cs @@ -0,0 +1,53 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Diagnostics.CodeAnalysis; +using System.Text; +using Cysharp.IO; +using Elastic.Markdown.Diagnostics; +using Errata; +using Spectre.Console; +using Diagnostic = Elastic.Markdown.Diagnostics.Diagnostic; + +namespace Documentation.Builder.Diagnostics.Console; + +public class ErrataFileSourceRepository : ISourceRepository +{ + public bool TryGet(string id, [NotNullWhen(true)] out Source? source) + { + using var reader = new Utf8StreamReader(id); + var text = Encoding.UTF8.GetString(reader.ReadToEndAsync().GetAwaiter().GetResult()); + source = new Source(id, text); + return true; + } + + public void WriteDiagnosticsToConsole(IReadOnlyCollection items) + { + var report = new Report(this); + foreach (var item in items) + { + var d = item.Severity switch + { + Severity.Error => Errata.Diagnostic.Error(item.Message), + Severity.Warning => Errata.Diagnostic.Warning(item.Message), + _ => Errata.Diagnostic.Info(item.Message) + }; + if (item is { Line: not null, Column: not null }) + { + var location = new Location(item.Line ?? 0, item.Column ?? 0); + d = d.WithLabel(new Label(item.File, location, "") + .WithLength(item.Length == null ? 1 : Math.Clamp(item.Length.Value, 1, item.Length.Value + 3)) + .WithPriority(1) + .WithColor(item.Severity == Severity.Error ? Color.Red : Color.Blue)); + } + else + d = d.WithNote(item.File); + + report.AddDiagnostic(d); + } + + // Render the report + report.Render(AnsiConsole.Console); + } +} diff --git a/src/docs-builder/Diagnostics/Console/GithubAnnotationOutput.cs b/src/docs-builder/Diagnostics/Console/GithubAnnotationOutput.cs new file mode 100644 index 00000000..e03a4839 --- /dev/null +++ b/src/docs-builder/Diagnostics/Console/GithubAnnotationOutput.cs @@ -0,0 +1,30 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using Actions.Core; +using Actions.Core.Services; +using Elastic.Markdown.Diagnostics; + +namespace Documentation.Builder.Diagnostics.Console; + +public class GithubAnnotationOutput(ICoreService? githubActions) : IDiagnosticsOutput +{ + public void Write(Diagnostic diagnostic) + { + if (githubActions == null) return; + if (string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("GITHUB_ACTION"))) + return; + var properties = new AnnotationProperties + { + File = diagnostic.File, + StartColumn = diagnostic.Column, + StartLine = diagnostic.Line, + EndColumn = diagnostic.Column + diagnostic.Length ?? 1 + }; + if (diagnostic.Severity == Severity.Error) + githubActions.WriteError(diagnostic.Message, properties); + if (diagnostic.Severity == Severity.Warning) + githubActions.WriteWarning(diagnostic.Message, properties); + } +} diff --git a/src/docs-builder/Diagnostics/ErrorCollector.cs b/src/docs-builder/Diagnostics/ErrorCollector.cs deleted file mode 100644 index c1207501..00000000 --- a/src/docs-builder/Diagnostics/ErrorCollector.cs +++ /dev/null @@ -1,92 +0,0 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -using System.Diagnostics.CodeAnalysis; -using System.Text; -using Actions.Core; -using Actions.Core.Services; -using Cysharp.IO; -using Elastic.Markdown.Diagnostics; -using Errata; -using Microsoft.Extensions.Logging; -using Spectre.Console; -using Diagnostic = Elastic.Markdown.Diagnostics.Diagnostic; - -namespace Documentation.Builder.Diagnostics; - -public class FileSourceRepository : ISourceRepository -{ - public bool TryGet(string id, [NotNullWhen(true)] out Source? source) - { - using var reader = new Utf8StreamReader(id); - var text = Encoding.UTF8.GetString(reader.ReadToEndAsync().GetAwaiter().GetResult()); - source = new Source(id, text); - return true; - } -} - -public class GithubAnnotationOutput(ICoreService githubActions) : IDiagnosticsOutput -{ - public void Write(Diagnostic diagnostic) - { - if (string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("GITHUB_ACTION"))) - return; - var properties = new AnnotationProperties - { - File = diagnostic.File, - StartColumn = diagnostic.Column, - StartLine = diagnostic.Line, - EndColumn = diagnostic.Column + diagnostic.Length ?? 1 - }; - if (diagnostic.Severity == Severity.Error) - githubActions.WriteError(diagnostic.Message, properties); - if (diagnostic.Severity == Severity.Warning) - githubActions.WriteWarning(diagnostic.Message, properties); - } -} - -public class ConsoleDiagnosticsCollector(ILoggerFactory loggerFactory, ICoreService? githubActions = null) - : DiagnosticsCollector(loggerFactory, githubActions != null ? [new GithubAnnotationOutput(githubActions)] : []) -{ - private readonly List _items = new(); - - private readonly ILogger _logger = - loggerFactory.CreateLogger(); - - protected override void HandleItem(Diagnostic diagnostic) => _items.Add(diagnostic); - - public override async Task StopAsync(Cancel ctx) - { - var report = new Report(new FileSourceRepository()); - foreach (var item in _items) - { - var d = item.Severity switch - { - Severity.Error => Errata.Diagnostic.Error(item.Message), - Severity.Warning => Errata.Diagnostic.Warning(item.Message), - _ => Errata.Diagnostic.Info(item.Message) - }; - if (item is { Line: not null, Column: not null }) - { - var location = new Location(item.Line ?? 0, item.Column ?? 0); - d = d.WithLabel(new Label(item.File, location, "") - .WithLength(item.Length == null ? 1 : Math.Clamp(item.Length.Value, 1, item.Length.Value + 3)) - .WithPriority(1) - .WithColor(item.Severity == Severity.Error ? Color.Red : Color.Blue)); - } - else - d = d.WithNote(item.File); - - report.AddDiagnostic(d); - } - - // Render the report - report.Render(AnsiConsole.Console); - AnsiConsole.WriteLine(); - AnsiConsole.Write(new Markup($" [bold red]{Errors} Errors[/] / [bold blue]{Warnings} Warnings[/]")); - AnsiConsole.WriteLine(); - AnsiConsole.WriteLine(); - await Task.CompletedTask; - } -} diff --git a/src/docs-builder/Diagnostics/LiveMode/LiveModeDiagnosticsCollector.cs b/src/docs-builder/Diagnostics/LiveMode/LiveModeDiagnosticsCollector.cs new file mode 100644 index 00000000..3cc9e302 --- /dev/null +++ b/src/docs-builder/Diagnostics/LiveMode/LiveModeDiagnosticsCollector.cs @@ -0,0 +1,17 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using Elastic.Markdown.Diagnostics; +using Microsoft.Extensions.Logging; +using Diagnostic = Elastic.Markdown.Diagnostics.Diagnostic; + +namespace Documentation.Builder.Diagnostics.LiveMode; + +public class LiveModeDiagnosticsCollector(ILoggerFactory loggerFactory) + : DiagnosticsCollector([new Log(loggerFactory.CreateLogger())]) +{ + protected override void HandleItem(Diagnostic diagnostic) { } + + public override async Task StopAsync(Cancel ctx) => await Task.CompletedTask; +} diff --git a/src/docs-builder/Diagnostics/Log.cs b/src/docs-builder/Diagnostics/Log.cs new file mode 100644 index 00000000..5803d261 --- /dev/null +++ b/src/docs-builder/Diagnostics/Log.cs @@ -0,0 +1,22 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using Elastic.Markdown.Diagnostics; +using Microsoft.Extensions.Logging; + +// ReSharper disable once CheckNamespace +namespace Documentation.Builder; + +// named Log for terseness on console output +public class Log(ILogger logger) : IDiagnosticsOutput +{ + public void Write(Diagnostic diagnostic) + { + if (diagnostic.Severity == Severity.Error) + logger.LogError($"{diagnostic.Message} ({diagnostic.File}:{diagnostic.Line})"); + else + logger.LogWarning($"{diagnostic.Message} ({diagnostic.File}:{diagnostic.Line})"); + } +} + diff --git a/src/docs-builder/Http/DocumentationWebHost.cs b/src/docs-builder/Http/DocumentationWebHost.cs index d231f22e..12b7539b 100644 --- a/src/docs-builder/Http/DocumentationWebHost.cs +++ b/src/docs-builder/Http/DocumentationWebHost.cs @@ -4,6 +4,8 @@ using System.Diagnostics.CodeAnalysis; using System.IO.Abstractions; using Documentation.Builder.Diagnostics; +using Documentation.Builder.Diagnostics.Console; +using Documentation.Builder.Diagnostics.LiveMode; using Elastic.Markdown; using Elastic.Markdown.Diagnostics; using Elastic.Markdown.IO; @@ -25,25 +27,35 @@ public class DocumentationWebHost private readonly WebApplication _webApplication; private readonly string _staticFilesDirectory; + private readonly BuildContext _context; public DocumentationWebHost(string? path, ILoggerFactory logger, IFileSystem fileSystem) { var builder = WebApplication.CreateSlimBuilder(); - var context = new BuildContext(fileSystem, fileSystem, path, null) + + builder.Logging.ClearProviders(); + builder.Logging.SetMinimumLevel(LogLevel.Warning) + .AddFilter("Microsoft.AspNetCore.Hosting.Diagnostics", LogLevel.Error) + .AddFilter("Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware", LogLevel.Error) + .AddFilter("Microsoft.Hosting.Lifetime", LogLevel.Information) + + .AddSimpleConsole(o => o.SingleLine = true); + + _context = new BuildContext(fileSystem, fileSystem, path, null) { - Collector = new ConsoleDiagnosticsCollector(logger) + Collector = new LiveModeDiagnosticsCollector(logger) }; builder.Services.AddAotLiveReload(s => { - s.FolderToMonitor = context.SourcePath.FullName; + s.FolderToMonitor = _context.SourcePath.FullName; s.ClientFileExtensions = ".md,.yml"; }); - builder.Services.AddSingleton(_ => new ReloadableGeneratorState(context.SourcePath, null, context, logger)); + builder.Services.AddSingleton(_ => new ReloadableGeneratorState(_context.SourcePath, null, _context, logger)); builder.Services.AddHostedService(); - builder.Services.AddSingleton(logger); - builder.Logging.SetMinimumLevel(LogLevel.Warning); - _staticFilesDirectory = Path.Combine(context.SourcePath.FullName, "_static"); + //builder.Services.AddSingleton(logger); + + _staticFilesDirectory = Path.Combine(_context.SourcePath.FullName, "_static"); #if DEBUG // this attempts to serve files directly from their source rather than the embedded resourses during development. // this allows us to change js/css files without restarting the webserver @@ -57,7 +69,17 @@ public DocumentationWebHost(string? path, ILoggerFactory logger, IFileSystem fil } - public async Task RunAsync(Cancel ctx) => await _webApplication.RunAsync(ctx); + public async Task RunAsync(Cancel ctx) + { + _ = _context.Collector.StartAsync(ctx); + await _webApplication.RunAsync(ctx); + } + + public async Task StopAsync(Cancel ctx) + { + _context.Collector.Channel.TryComplete(); + await _context.Collector.StopAsync(ctx); + } private void SetUpRoutes() { @@ -87,8 +109,8 @@ private static async Task ServeDocumentationFile(ReloadableGeneratorSta { case MarkdownFile markdown: { - await markdown.ParseFullAsync(ctx); var rendered = await generator.RenderLayout(markdown, ctx); + return Results.Content(rendered, "text/html"); } case ImageFile image: diff --git a/tests/Elastic.Markdown.Tests/Directives/DirectiveBaseTests.cs b/tests/Elastic.Markdown.Tests/Directives/DirectiveBaseTests.cs index 8bdd2990..4f05dc76 100644 --- a/tests/Elastic.Markdown.Tests/Directives/DirectiveBaseTests.cs +++ b/tests/Elastic.Markdown.Tests/Directives/DirectiveBaseTests.cs @@ -2,7 +2,6 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information using System.IO.Abstractions.TestingHelpers; -using Elastic.Markdown.Diagnostics; using Elastic.Markdown.IO; using Elastic.Markdown.Myst; using Elastic.Markdown.Myst.Directives; @@ -31,17 +30,6 @@ public override async Task InitializeAsync() [Fact] public void BlockIsNotNull() => Block.Should().NotBeNull(); - -} - -public class TestDiagnosticsCollector(ILoggerFactory logger) - : DiagnosticsCollector(logger, []) -{ - private readonly List _diagnostics = new(); - - public IReadOnlyCollection Diagnostics => _diagnostics; - - protected override void HandleItem(Diagnostic diagnostic) => _diagnostics.Add(diagnostic); } public abstract class DirectiveTest : IAsyncLifetime @@ -53,7 +41,6 @@ public abstract class DirectiveTest : IAsyncLifetime protected TestDiagnosticsCollector Collector { get; } protected DocumentationSet Set { get; set; } - protected DirectiveTest(ITestOutputHelper output, [LanguageInjection("markdown")] string content) { var logger = new TestLoggerFactory(output); @@ -80,7 +67,7 @@ protected DirectiveTest(ITestOutputHelper output, [LanguageInjection("markdown") var root = FileSystem.DirectoryInfo.New(Path.Combine(Paths.Root.FullName, "docs/source")); FileSystem.GenerateDocSetYaml(root); - Collector = new TestDiagnosticsCollector(logger); + Collector = new TestDiagnosticsCollector(output); var context = new BuildContext(FileSystem) { Collector = Collector diff --git a/tests/Elastic.Markdown.Tests/DocSet/NavigationTestsBase.cs b/tests/Elastic.Markdown.Tests/DocSet/NavigationTestsBase.cs index e34b41e9..fc001308 100644 --- a/tests/Elastic.Markdown.Tests/DocSet/NavigationTestsBase.cs +++ b/tests/Elastic.Markdown.Tests/DocSet/NavigationTestsBase.cs @@ -21,11 +21,12 @@ protected NavigationTestsBase(ITestOutputHelper output) { CurrentDirectory = Paths.Root.FullName }); + var collector = new TestDiagnosticsCollector(output); var context = new BuildContext(ReadFileSystem, writeFs) { Force = false, UrlPathPrefix = null, - Collector = new DiagnosticsCollector(logger, []) + Collector = collector }; Set = new DocumentationSet(context); diff --git a/tests/Elastic.Markdown.Tests/Inline/InlneBaseTests.cs b/tests/Elastic.Markdown.Tests/Inline/InlneBaseTests.cs index bc6d69db..aa6ae26c 100644 --- a/tests/Elastic.Markdown.Tests/Inline/InlneBaseTests.cs +++ b/tests/Elastic.Markdown.Tests/Inline/InlneBaseTests.cs @@ -107,7 +107,7 @@ protected InlineTest( var root = FileSystem.DirectoryInfo.New(Path.Combine(Paths.Root.FullName, "docs/source")); FileSystem.GenerateDocSetYaml(root, globalVariables); - Collector = new TestDiagnosticsCollector(logger); + Collector = new TestDiagnosticsCollector(output); var context = new BuildContext(FileSystem) { Collector = Collector diff --git a/tests/Elastic.Markdown.Tests/OutputDirectoryTests.cs b/tests/Elastic.Markdown.Tests/OutputDirectoryTests.cs index 01cfb171..c362669f 100644 --- a/tests/Elastic.Markdown.Tests/OutputDirectoryTests.cs +++ b/tests/Elastic.Markdown.Tests/OutputDirectoryTests.cs @@ -26,7 +26,7 @@ public async Task CreatesDefaultOutputDirectory() }); var context = new BuildContext(fileSystem) { - Collector = new DiagnosticsCollector(logger, []) + Collector = new DiagnosticsCollector([]) }; var set = new DocumentationSet(context); var generator = new DocumentationGenerator(set, logger); diff --git a/tests/Elastic.Markdown.Tests/TestDiagnosticsCollector.cs b/tests/Elastic.Markdown.Tests/TestDiagnosticsCollector.cs new file mode 100644 index 00000000..aeeb9c9e --- /dev/null +++ b/tests/Elastic.Markdown.Tests/TestDiagnosticsCollector.cs @@ -0,0 +1,29 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using Elastic.Markdown.Diagnostics; +using Xunit.Abstractions; + +namespace Elastic.Markdown.Tests; + +public class TestDiagnosticsOutput(ITestOutputHelper output) : IDiagnosticsOutput +{ + public void Write(Diagnostic diagnostic) + { + if (diagnostic.Severity == Severity.Error) + output.WriteLine($"Error: {diagnostic.Message} ({diagnostic.File}:{diagnostic.Line})"); + else + output.WriteLine($"Warn : {diagnostic.Message} ({diagnostic.File}:{diagnostic.Line})"); + } +} + +public class TestDiagnosticsCollector(ITestOutputHelper output) + : DiagnosticsCollector([new TestDiagnosticsOutput(output)]) +{ + private readonly List _diagnostics = new(); + + public IReadOnlyCollection Diagnostics => _diagnostics; + + protected override void HandleItem(Diagnostic diagnostic) => _diagnostics.Add(diagnostic); +}