Skip to content

Commit f0c347a

Browse files
authored
Introduce dedicated error collector for serve command (#167)
1 parent 085b9fe commit f0c347a

File tree

14 files changed

+226
-134
lines changed

14 files changed

+226
-134
lines changed

src/Elastic.Markdown/Diagnostics/DiagnosticsChannel.cs

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -58,23 +58,9 @@ public interface IDiagnosticsOutput
5858
public void Write(Diagnostic diagnostic);
5959
}
6060

61-
public class LogDiagnosticOutput(ILogger logger) : IDiagnosticsOutput
62-
{
63-
public void Write(Diagnostic diagnostic)
64-
{
65-
if (diagnostic.Severity == Severity.Error)
66-
logger.LogError($"{diagnostic.Message} ({diagnostic.File}:{diagnostic.Line})");
67-
else
68-
logger.LogWarning($"{diagnostic.Message} ({diagnostic.File}:{diagnostic.Line})");
69-
}
70-
}
71-
72-
public class DiagnosticsCollector(ILoggerFactory loggerFactory, IReadOnlyCollection<IDiagnosticsOutput> outputs)
61+
public class DiagnosticsCollector(IReadOnlyCollection<IDiagnosticsOutput> outputs)
7362
: IHostedService
7463
{
75-
private readonly IReadOnlyCollection<IDiagnosticsOutput> _outputs =
76-
[new LogDiagnosticOutput(loggerFactory.CreateLogger<LogDiagnosticOutput>()), .. outputs];
77-
7864
public DiagnosticsChannel Channel { get; } = new();
7965

8066
private int _errors;
@@ -117,7 +103,7 @@ void Drain()
117103
IncrementSeverityCount(item);
118104
HandleItem(item);
119105
OffendingFiles.Add(item.File);
120-
foreach (var output in _outputs)
106+
foreach (var output in outputs)
121107
output.Write(item);
122108
}
123109
}

src/docs-builder/Cli/Commands.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using Actions.Core.Services;
66
using ConsoleAppFramework;
77
using Documentation.Builder.Diagnostics;
8+
using Documentation.Builder.Diagnostics.Console;
89
using Documentation.Builder.Http;
910
using Elastic.Markdown;
1011
using Elastic.Markdown.IO;
@@ -27,6 +28,8 @@ public async Task Serve(string? path = null, Cancel ctx = default)
2728
{
2829
var host = new DocumentationWebHost(path, logger, new FileSystem());
2930
await host.RunAsync(ctx);
31+
await host.StopAsync(ctx);
32+
3033
}
3134

3235
/// <summary>
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
using Actions.Core.Services;
6+
using Elastic.Markdown.Diagnostics;
7+
using Microsoft.Extensions.Logging;
8+
using Spectre.Console;
9+
using Diagnostic = Elastic.Markdown.Diagnostics.Diagnostic;
10+
11+
namespace Documentation.Builder.Diagnostics.Console;
12+
13+
public class ConsoleDiagnosticsCollector(ILoggerFactory loggerFactory, ICoreService? githubActions = null)
14+
: DiagnosticsCollector([new Log(loggerFactory.CreateLogger<Log>()), new GithubAnnotationOutput(githubActions)]
15+
)
16+
{
17+
private readonly List<Diagnostic> _items = new();
18+
19+
protected override void HandleItem(Diagnostic diagnostic) => _items.Add(diagnostic);
20+
21+
public override async Task StopAsync(Cancel ctx)
22+
{
23+
var repository = new ErrataFileSourceRepository();
24+
repository.WriteDiagnosticsToConsole(_items);
25+
26+
AnsiConsole.WriteLine();
27+
AnsiConsole.Write(new Markup($" [bold red]{Errors} Errors[/] / [bold blue]{Warnings} Warnings[/]"));
28+
AnsiConsole.WriteLine();
29+
AnsiConsole.WriteLine();
30+
31+
await Task.CompletedTask;
32+
}
33+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
using System.Diagnostics.CodeAnalysis;
6+
using System.Text;
7+
using Cysharp.IO;
8+
using Elastic.Markdown.Diagnostics;
9+
using Errata;
10+
using Spectre.Console;
11+
using Diagnostic = Elastic.Markdown.Diagnostics.Diagnostic;
12+
13+
namespace Documentation.Builder.Diagnostics.Console;
14+
15+
public class ErrataFileSourceRepository : ISourceRepository
16+
{
17+
public bool TryGet(string id, [NotNullWhen(true)] out Source? source)
18+
{
19+
using var reader = new Utf8StreamReader(id);
20+
var text = Encoding.UTF8.GetString(reader.ReadToEndAsync().GetAwaiter().GetResult());
21+
source = new Source(id, text);
22+
return true;
23+
}
24+
25+
public void WriteDiagnosticsToConsole(IReadOnlyCollection<Diagnostic> items)
26+
{
27+
var report = new Report(this);
28+
foreach (var item in items)
29+
{
30+
var d = item.Severity switch
31+
{
32+
Severity.Error => Errata.Diagnostic.Error(item.Message),
33+
Severity.Warning => Errata.Diagnostic.Warning(item.Message),
34+
_ => Errata.Diagnostic.Info(item.Message)
35+
};
36+
if (item is { Line: not null, Column: not null })
37+
{
38+
var location = new Location(item.Line ?? 0, item.Column ?? 0);
39+
d = d.WithLabel(new Label(item.File, location, "")
40+
.WithLength(item.Length == null ? 1 : Math.Clamp(item.Length.Value, 1, item.Length.Value + 3))
41+
.WithPriority(1)
42+
.WithColor(item.Severity == Severity.Error ? Color.Red : Color.Blue));
43+
}
44+
else
45+
d = d.WithNote(item.File);
46+
47+
report.AddDiagnostic(d);
48+
}
49+
50+
// Render the report
51+
report.Render(AnsiConsole.Console);
52+
}
53+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
using Actions.Core;
6+
using Actions.Core.Services;
7+
using Elastic.Markdown.Diagnostics;
8+
9+
namespace Documentation.Builder.Diagnostics.Console;
10+
11+
public class GithubAnnotationOutput(ICoreService? githubActions) : IDiagnosticsOutput
12+
{
13+
public void Write(Diagnostic diagnostic)
14+
{
15+
if (githubActions == null)
16+
return;
17+
if (string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("GITHUB_ACTION")))
18+
return;
19+
var properties = new AnnotationProperties
20+
{
21+
File = diagnostic.File,
22+
StartColumn = diagnostic.Column,
23+
StartLine = diagnostic.Line,
24+
EndColumn = diagnostic.Column + diagnostic.Length ?? 1
25+
};
26+
if (diagnostic.Severity == Severity.Error)
27+
githubActions.WriteError(diagnostic.Message, properties);
28+
if (diagnostic.Severity == Severity.Warning)
29+
githubActions.WriteWarning(diagnostic.Message, properties);
30+
}
31+
}

src/docs-builder/Diagnostics/ErrorCollector.cs

Lines changed: 0 additions & 92 deletions
This file was deleted.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
using Elastic.Markdown.Diagnostics;
6+
using Microsoft.Extensions.Logging;
7+
using Diagnostic = Elastic.Markdown.Diagnostics.Diagnostic;
8+
9+
namespace Documentation.Builder.Diagnostics.LiveMode;
10+
11+
public class LiveModeDiagnosticsCollector(ILoggerFactory loggerFactory)
12+
: DiagnosticsCollector([new Log(loggerFactory.CreateLogger<Log>())])
13+
{
14+
protected override void HandleItem(Diagnostic diagnostic) { }
15+
16+
public override async Task StopAsync(Cancel ctx) => await Task.CompletedTask;
17+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
using Elastic.Markdown.Diagnostics;
6+
using Microsoft.Extensions.Logging;
7+
8+
// ReSharper disable once CheckNamespace
9+
namespace Documentation.Builder;
10+
11+
// named Log for terseness on console output
12+
public class Log(ILogger logger) : IDiagnosticsOutput
13+
{
14+
public void Write(Diagnostic diagnostic)
15+
{
16+
if (diagnostic.Severity == Severity.Error)
17+
logger.LogError($"{diagnostic.Message} ({diagnostic.File}:{diagnostic.Line})");
18+
else
19+
logger.LogWarning($"{diagnostic.Message} ({diagnostic.File}:{diagnostic.Line})");
20+
}
21+
}
22+

src/docs-builder/Http/DocumentationWebHost.cs

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
using System.Diagnostics.CodeAnalysis;
55
using System.IO.Abstractions;
66
using Documentation.Builder.Diagnostics;
7+
using Documentation.Builder.Diagnostics.Console;
8+
using Documentation.Builder.Diagnostics.LiveMode;
79
using Elastic.Markdown;
810
using Elastic.Markdown.Diagnostics;
911
using Elastic.Markdown.IO;
@@ -25,25 +27,35 @@ public class DocumentationWebHost
2527
private readonly WebApplication _webApplication;
2628

2729
private readonly string _staticFilesDirectory;
30+
private readonly BuildContext _context;
2831

2932
public DocumentationWebHost(string? path, ILoggerFactory logger, IFileSystem fileSystem)
3033
{
3134
var builder = WebApplication.CreateSlimBuilder();
32-
var context = new BuildContext(fileSystem, fileSystem, path, null)
35+
36+
builder.Logging.ClearProviders();
37+
builder.Logging.SetMinimumLevel(LogLevel.Warning)
38+
.AddFilter("Microsoft.AspNetCore.Hosting.Diagnostics", LogLevel.Error)
39+
.AddFilter("Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware", LogLevel.Error)
40+
.AddFilter("Microsoft.Hosting.Lifetime", LogLevel.Information)
41+
42+
.AddSimpleConsole(o => o.SingleLine = true);
43+
44+
_context = new BuildContext(fileSystem, fileSystem, path, null)
3345
{
34-
Collector = new ConsoleDiagnosticsCollector(logger)
46+
Collector = new LiveModeDiagnosticsCollector(logger)
3547
};
3648
builder.Services.AddAotLiveReload(s =>
3749
{
38-
s.FolderToMonitor = context.SourcePath.FullName;
50+
s.FolderToMonitor = _context.SourcePath.FullName;
3951
s.ClientFileExtensions = ".md,.yml";
4052
});
41-
builder.Services.AddSingleton<ReloadableGeneratorState>(_ => new ReloadableGeneratorState(context.SourcePath, null, context, logger));
53+
builder.Services.AddSingleton<ReloadableGeneratorState>(_ => new ReloadableGeneratorState(_context.SourcePath, null, _context, logger));
4254
builder.Services.AddHostedService<ReloadGeneratorService>();
43-
builder.Services.AddSingleton(logger);
44-
builder.Logging.SetMinimumLevel(LogLevel.Warning);
4555

46-
_staticFilesDirectory = Path.Combine(context.SourcePath.FullName, "_static");
56+
//builder.Services.AddSingleton(logger);
57+
58+
_staticFilesDirectory = Path.Combine(_context.SourcePath.FullName, "_static");
4759
#if DEBUG
4860
// this attempts to serve files directly from their source rather than the embedded resourses during development.
4961
// this allows us to change js/css files without restarting the webserver
@@ -57,7 +69,17 @@ public DocumentationWebHost(string? path, ILoggerFactory logger, IFileSystem fil
5769
}
5870

5971

60-
public async Task RunAsync(Cancel ctx) => await _webApplication.RunAsync(ctx);
72+
public async Task RunAsync(Cancel ctx)
73+
{
74+
_ = _context.Collector.StartAsync(ctx);
75+
await _webApplication.RunAsync(ctx);
76+
}
77+
78+
public async Task StopAsync(Cancel ctx)
79+
{
80+
_context.Collector.Channel.TryComplete();
81+
await _context.Collector.StopAsync(ctx);
82+
}
6183

6284
private void SetUpRoutes()
6385
{
@@ -87,8 +109,8 @@ private static async Task<IResult> ServeDocumentationFile(ReloadableGeneratorSta
87109
{
88110
case MarkdownFile markdown:
89111
{
90-
await markdown.ParseFullAsync(ctx);
91112
var rendered = await generator.RenderLayout(markdown, ctx);
113+
92114
return Results.Content(rendered, "text/html");
93115
}
94116
case ImageFile image:

0 commit comments

Comments
 (0)