Skip to content

Commit

Permalink
Merge pull request #1209 from riganti/health-checks
Browse files Browse the repository at this point in the history
Add IHeathCheck implementation to Asp.Net Core hosting
  • Loading branch information
exyi committed Apr 19, 2022
2 parents 63c5c59 + 169b752 commit 4bbc325
Show file tree
Hide file tree
Showing 7 changed files with 121 additions and 5 deletions.
Expand Up @@ -60,6 +60,7 @@ public DefaultControlBuilderFactory(DotvvmConfiguration configuration, IMarkupFi
/// </summary>
private (ControlBuilderDescriptor, Lazy<IControlBuilder>) CreateControlBuilder(MarkupFile file)
{
var compilationService = configuration.ServiceProvider.GetService<IDotvvmViewCompilationService>();
void editCompilationException(DotvvmCompilationException ex)
{
if (ex.FileName == null)
Expand Down Expand Up @@ -90,11 +91,18 @@ void editCompilationException(DotvvmCompilationException ex)
configuration.Resources.RegisterViewModuleResources(import, init);
}
compilationService.RegisterCompiledView(file.FileName, descriptor, null);
return result;
}
catch (DotvvmCompilationException ex)
{
editCompilationException(ex);
compilationService.RegisterCompiledView(file.FileName, descriptor, ex);
throw;
}
catch (Exception ex)
{
compilationService.RegisterCompiledView(file.FileName, descriptor, ex);
throw;
}
});
Expand All @@ -111,6 +119,12 @@ void editCompilationException(DotvvmCompilationException ex)
catch (DotvvmCompilationException ex)
{
editCompilationException(ex);
compilationService.RegisterCompiledView(file.FileName, null, ex);
throw;
}
catch (Exception ex)
{
compilationService.RegisterCompiledView(file.FileName, null, ex);
throw;
}
}
Expand Down
Expand Up @@ -9,6 +9,7 @@
using DotVVM.Framework.Configuration;
using DotVVM.Framework.Controls.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
using DotVVM.Framework.Utils;

namespace DotVVM.Framework.Compilation
{
Expand Down Expand Up @@ -155,10 +156,9 @@ public bool BuildView(DotHtmlFileInfo file, out DotHtmlFileInfo? masterPage)
using var scopedServiceProvider = dotvvmConfiguration.ServiceProvider.CreateScope(); // dependencies that are configured as scoped cannot be resolved from root service provider
var compiledControl = pageBuilder.builder.Value.BuildControl(controlBuilderFactory, scopedServiceProvider.ServiceProvider);

if (compiledControl is DotvvmView view &&
view.Directives!.TryGetValue(ParserConstants.MasterPageDirective, out var masterPagePath))
if (pageBuilder.descriptor.MasterPage is { FileName: {} masterPagePath })
{
masterPage = masterPages.Value.GetOrAdd(masterPagePath, new DotHtmlFileInfo(masterPagePath));
masterPage = masterPages.Value.GetOrAdd(masterPagePath, path => new DotHtmlFileInfo(path));
}

file.Status = CompilationState.CompletedSuccessfully;
Expand All @@ -173,5 +173,30 @@ public bool BuildView(DotHtmlFileInfo file, out DotHtmlFileInfo? masterPage)
}
return true;
}

/// <summary> Callback from the compiler which adds the view compilation result to the status page. </summary>
public void RegisterCompiledView(string file, ViewCompiler.ControlBuilderDescriptor? descriptor, Exception? exception)
{
var fileInfo =
routes.Value.FirstOrDefault(t => t.VirtualPath == file) ??
controls.Value.FirstOrDefault(t => t.VirtualPath == file) ??
masterPages.Value.GetOrAdd(file, path => new DotHtmlFileInfo(path));

if (exception is null)
{
fileInfo.Status = CompilationState.CompletedSuccessfully;
fileInfo.Exception = null;
}
else
{
fileInfo.Status = CompilationState.CompilationFailed;
fileInfo.Exception = exception.Message;
}

if (descriptor?.MasterPage is { FileName: {} masterPagePath })
{
masterPages.Value.GetOrAdd(masterPagePath, path => new DotHtmlFileInfo(path));
}
}
}
}
@@ -1,4 +1,5 @@
using System.Collections.Concurrent;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Threading.Tasks;
Expand Down Expand Up @@ -43,5 +44,7 @@ public interface IDotvvmViewCompilationService
/// <param name="forceRecompile">If set, than everything will be recompiled.</param>
/// <returns>Returns whether compilation was successful.</returns>
Task<bool> CompileAll(bool buildInParallel=true, bool forceRecompile = false);

void RegisterCompiledView(string filePath, ViewCompiler.ControlBuilderDescriptor? descriptor, Exception? exception);
}
}
Expand Up @@ -32,5 +32,7 @@
<PackageReference Include="Microsoft.AspNetCore.Localization" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Localization" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.HealthChecks" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions" Version="2.2.0" />
</ItemGroup>
</Project>
65 changes: 65 additions & 0 deletions src/Framework/Hosting.AspNetCore/Hosting/DotvvmHealthCheck.cs
@@ -0,0 +1,65 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using DotVVM.Framework.Compilation;
using DotVVM.Framework.Configuration;
using DotVVM.Framework.Utils;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;

namespace DotVVM.Framework.Hosting
{
public class DotvvmHealthCheck : IHealthCheck
{
private readonly IDotvvmViewCompilationService compilationService;
private readonly DotvvmConfiguration configuration;

public DotvvmHealthCheck(
IDotvvmViewCompilationService compilationService,
DotvvmConfiguration configuration
)
{
this.compilationService = compilationService;
this.configuration = configuration;
}

public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
{
var c = compilationService.GetRoutes().AddRange(compilationService.GetControls().AddRange(compilationService.GetMasterPages()));

if (!c.Any(p => p.Status == CompilationState.CompilationFailed))
{
return Task.FromResult(new HealthCheckResult(HealthStatus.Healthy));
}

// if nothing compiles, we say unhealthy. If at least something works, we say only degraded
var state =
c.Any(p => p.Status is CompilationState.CompletedSuccessfully or CompilationState.CompilationWarning)
? HealthStatus.Degraded
: HealthStatus.Unhealthy;

var views = c.Where(p => p.Status == CompilationState.CompilationFailed)
.Select(c => c.VirtualPath);
var statusPageUrl = configuration.Diagnostics.CompilationPage.Url;
return Task.FromResult(new HealthCheckResult(state, $"Dothtml pages can not be compiled: {views.Take(10).StringJoin(", ")}. See {statusPageUrl} for more information."));
}

public static void RegisterHealthCheck(IServiceCollection services)
{
services.ConfigureWithServices<HealthCheckServiceOptions>((options, s) => {
if (options.Registrations.Any(c => c.Name == "DotVVM"))
return;
options.Registrations.Add(
new HealthCheckRegistration(
"DotVVM",
ActivatorUtilities.CreateInstance<DotvvmHealthCheck>(s),
null,
new [] { "dotvvm" }
)
);
});
}
}
}
Expand Up @@ -88,6 +88,8 @@ private static void AddDotVVMServices(IServiceCollection services)
services.AddTransient<IDotvvmWarningSink, AspNetCoreLoggerWarningSink>();

services.TryAddSingleton<IStartupTracer>(startupTracer);

DotvvmHealthCheck.RegisterHealthCheck(services);
}
}
}
7 changes: 6 additions & 1 deletion src/Samples/AspNetCoreLatest/Startup.cs
Expand Up @@ -50,6 +50,8 @@ public void ConfigureServices(IServiceCollection services)
services.AddAuthentication("Scheme3")
.AddCookie("Scheme3");

services.AddHealthChecks();

services.AddLocalization(o => o.ResourcesPath = "Resources");

services.AddDotVVM<DotvvmServiceConfigurator>();
Expand Down Expand Up @@ -78,11 +80,14 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerF
}
});


#if AssertConfiguration
// this compilation symbol is set by CI server
config.AssertConfigurationIsValid();
#endif
app.UseRouting();
app.UseEndpoints(endpoints => {
endpoints.MapHealthChecks("/health");
});
app.UseStaticFiles();

app.UseEndpoints(endpoints => {
Expand Down

0 comments on commit 4bbc325

Please sign in to comment.