在 dotnet core 上的 swagger (openAPI) UI 中集成运行状况检查端点

Posted

技术标签:

【中文标题】在 dotnet core 上的 swagger (openAPI) UI 中集成运行状况检查端点【英文标题】:Integrating HealthCheck endpoint into swagger (open API) UI on dotnet core 【发布时间】:2019-06-19 02:35:45 【问题描述】:

我正在使用 here 所述的 Dotnet Core 运行状况检查。简而言之,它看起来像这样:

首先,您像这样配置服务:

services.AddHealthChecks()
    .AddSqlServer("connectionString", name: "SQlServerHealthCheck")
    ... // Add multiple other checks

然后,您像这样注册一个端点:

app.UseHealthChecks("/my/healthCheck/endpoint");

我们也在使用 Swagger(又名 Open API),我们通过 Swagger UI 查看所有端点,但看不到运行状况检查端点。

有没有办法将此添加到控制器方法中,以便 Swagger 自动拾取端点,或者以其他方式将其与 swagger 集成?

目前我发现的最佳解决方案是添加自定义硬编码端点 (like described here),但维护起来并不好。

【问题讨论】:

【参考方案1】:

将运行状况检查端点集成到 .NET 5 上的 Swagger(开放 API)UI

namespace <Some-Namespace>

    using global::HealthChecks.UI.Core;
    using global::HealthChecks.UI.Core.Data;

    using Microsoft.AspNetCore.Http;
    using Microsoft.Extensions.Options;
    using Microsoft.OpenApi.Any;
    using Microsoft.OpenApi.Models;

    using Swashbuckle.AspNetCore.SwaggerGen;

    using System;
    using System.Collections.Generic;

    using static System.Text.Json.JsonNamingPolicy;

    /// <summary>
    /// 
    /// </summary>
    public class HealthCheckEndpointDocumentFilter : IDocumentFilter
    
        /// <summary>
        /// 
        /// </summary>
        private readonly global::HealthChecks.UI.Configuration.Options Options;

        /// <summary>
        /// 
        /// </summary>
        /// <param name="Options"></param>
        public HealthCheckEndpointDocumentFilter(IOptions<global::HealthChecks.UI.Configuration.Options> Options)
        
            this.Options = Options?.Value ?? throw new ArgumentNullException(nameof(Options));
        

        /// <summary>
        /// 
        /// </summary>
        /// <param name="SwaggerDoc"></param>
        /// <param name="Context"></param>
        public void Apply(OpenApiDocument SwaggerDoc, DocumentFilterContext Context)
        
            var PathItem = new OpenApiPathItem
            
                Operations = new Dictionary<OperationType, OpenApiOperation>
                
                    [OperationType.Get] = new OpenApiOperation
                    
                        Description = "Returns all the health states used by this Microservice",
                        Tags =
                        
                            new OpenApiTag
                            
                                Name = "HealthCheck"
                            
                        ,
                        Responses =
                        
                            [StatusCodes.Status200OK.ToString()] = new OpenApiResponse
                            
                                Description = "API is healthy",
                                Content =
                                
                                    ["application/json"] = new OpenApiMediaType
                                    
                                        Schema = new OpenApiSchema
                                        
                                            Reference = new OpenApiReference
                                            
                                                Id = nameof(HealthCheckExecution),
                                                Type = ReferenceType.Schema,
                                            
                                        
                                    
                                
                            ,
                            [StatusCodes.Status503ServiceUnavailable.ToString()] = new OpenApiResponse
                            
                                Description = "API is not healthy"
                            
                        
                    
                
            ;

            var HealthCheckSchema = new OpenApiSchema
            
                Type = "object",
                Properties =
                
                    [CamelCase.ConvertName(nameof(HealthCheckExecution.Id))] = new OpenApiSchema
                    
                        Type = "integer",
                        Format = "int32"
                    ,
                    [CamelCase.ConvertName(nameof(HealthCheckExecution.Status))] = new OpenApiSchema
                    
                        Type = "string"
                    ,
                    [CamelCase.ConvertName(nameof(HealthCheckExecution.OnStateFrom))] = new OpenApiSchema
                    
                        Type = "string",
                        Format = "date-time"
                    ,
                    [CamelCase.ConvertName(nameof(HealthCheckExecution.LastExecuted))] = new OpenApiSchema
                    
                        Type = "string",
                        Format = "date-time"
                    ,
                    [CamelCase.ConvertName(nameof(HealthCheckExecution.Uri))] = new OpenApiSchema
                    
                        Type = "string"
                    ,
                    [CamelCase.ConvertName(nameof(HealthCheckExecution.Name))] = new OpenApiSchema
                    
                        Type = "string"
                    ,
                    [CamelCase.ConvertName(nameof(HealthCheckExecution.DiscoveryService))] = new OpenApiSchema
                    
                        Type = "string",
                        Nullable = true
                    ,
                    [CamelCase.ConvertName(nameof(HealthCheckExecution.Entries))] = new OpenApiSchema
                    
                        Type = "array",
                        Items = new OpenApiSchema
                        
                            Reference = new OpenApiReference
                            
                                Id = nameof(HealthCheckExecutionEntry),
                                Type = ReferenceType.Schema,
                            
                        
                    ,
                    [CamelCase.ConvertName(nameof(HealthCheckExecution.History))] = new OpenApiSchema
                    
                        Type = "array",
                        Items = new OpenApiSchema
                        
                            Reference = new OpenApiReference
                            
                                Id = nameof(HealthCheckExecutionHistory),
                                Type = ReferenceType.Schema,
                            
                        
                    
                
            ;

            var HealthCheckEntrySchema = new OpenApiSchema
            
                Type = "object",

                Properties =
                
                    [CamelCase.ConvertName(nameof(HealthCheckExecutionEntry.Id))] = new OpenApiSchema
                    
                        Type = "integer",
                        Format = "int32"
                    ,
                    [CamelCase.ConvertName(nameof(HealthCheckExecutionEntry.Name))] = new OpenApiSchema
                    
                        Type = "string"
                    ,
                    [CamelCase.ConvertName(nameof(HealthCheckExecutionEntry.Status))] = new OpenApiSchema
                    
                        Reference = new OpenApiReference
                        
                            Id = nameof(UIHealthStatus),
                            Type = ReferenceType.Schema,
                        
                    ,
                    [CamelCase.ConvertName(nameof(HealthCheckExecutionEntry.Description))] = new OpenApiSchema
                    
                        Type = "string"
                    ,
                    [CamelCase.ConvertName(nameof(HealthCheckExecutionEntry.Duration))] = new OpenApiSchema
                    
                        Type = "string",
                        Format = "[-][d'.']hh':'mm':'ss['.'fffffff]"
                    ,
                    [CamelCase.ConvertName(nameof(HealthCheckExecutionEntry.Tags))] = new OpenApiSchema
                    
                        Type = "array",
                        Items = new OpenApiSchema
                        
                            Type = "string"
                        
                    ,
                
            ;

            var HealthCheckHistorySchema = new OpenApiSchema
            
                Type = "object",

                Properties =
                
                    [CamelCase.ConvertName(nameof(HealthCheckExecutionHistory.Id))] = new OpenApiSchema
                    
                        Type = "integer",
                        Format = "int32"
                    ,
                    [CamelCase.ConvertName(nameof(HealthCheckExecutionHistory.Name))] = new OpenApiSchema
                    
                        Type = "string"
                    ,
                    [CamelCase.ConvertName(nameof(HealthCheckExecutionHistory.Description))] = new OpenApiSchema
                    
                        Type = "string"
                    ,
                    [CamelCase.ConvertName(nameof(HealthCheckExecutionHistory.Status))] = new OpenApiSchema
                    
                        Reference = new OpenApiReference
                        
                            Id = nameof(UIHealthStatus),
                            Type = ReferenceType.Schema,
                        
                    ,
                    [CamelCase.ConvertName(nameof(HealthCheckExecutionHistory.On))] = new OpenApiSchema
                    
                        Type = "string",
                        Format = "date-time"
                    ,
                
            ;

            var UIHealthStatusSchema = new OpenApiSchema
            
                Type = "string",

                Enum =
                
                    new OpenApiString(UIHealthStatus.Healthy.ToString()),
                    new OpenApiString(UIHealthStatus.Unhealthy.ToString()),
                    new OpenApiString(UIHealthStatus.Degraded.ToString())
                
            ;

            SwaggerDoc.Paths.Add(Options.ApiPath, PathItem);
            SwaggerDoc.Components.Schemas.Add(nameof(HealthCheckExecution), HealthCheckSchema);
            SwaggerDoc.Components.Schemas.Add(nameof(HealthCheckExecutionEntry), HealthCheckEntrySchema);
            SwaggerDoc.Components.Schemas.Add(nameof(HealthCheckExecutionHistory), HealthCheckHistorySchema);
            SwaggerDoc.Components.Schemas.Add(nameof(UIHealthStatus), UIHealthStatusSchema);
        
    

过滤器设置

Services.AddSwaggerGen(Options =>

    Options.SwaggerDoc("v1", new OpenApiInfo
    
        Version     = "v1",
        Title       = "<Name Api> Api",
        Description = "<Description> HTTP API."
    );

    Options.DocumentFilter<HealthCheckEndpointDocumentFilter>();
);

【讨论】:

【参考方案2】:

我将穷人的解决方案升级为更具描述性的文档,它将在 Swashbuckle 5 中正确显示响应类型。我在 Swagger UI 中获取端点,但 Open API 规范中的描述很笨拙。然后,我将特定的运行状况检查数据类型添加到了 swagger 文档中。我的解决方案是使用自定义响应编写器。

假设您覆盖了响应:

app.UseEndpoints(endpoints =>
        
            endpoints.MapControllers();
            endpoints.MapHealthChecks("/heartbeat", new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions()
            
                ResponseWriter = HeartbeatUtility.WriteResponse
            ) ;
        );

假设您有以下健康检查响应作者:

public static class HeartbeatUtility

    public const string Path = "/heartbeat";

    public const string ContentType = "application/json; charset=utf-8";
    public const string Status = "status";
    public const string TotalTime = "totalTime";
    public const string Results = "results";
    public const string Name = "Name";
    public const string Description = "description";
    public const string Data = "data";

    public static Task WriteResponse(HttpContext context, HealthReport healthReport)
    
        context.Response.ContentType = ContentType;

        using (var stream = new MemoryStream())
        
            using (var writer = new Utf8JsonWriter(stream, CreateJsonOptions()))
            
                writer.WriteStartObject();

                writer.WriteString(Status, healthReport.Status.ToString("G"));
                writer.WriteString(TotalTime, healthReport.TotalDuration.ToString("c"));

                if (healthReport.Entries.Count > 0)
                    writer.WriteEntries(healthReport.Entries);

                writer.WriteEndObject();
            

            var json = Encoding.UTF8.GetString(stream.ToArray());

            return context.Response.WriteAsync(json);
        
    

    private static JsonWriterOptions CreateJsonOptions()
    
        return new JsonWriterOptions
        
            Indented = true
        ;
    

    private static void WriteEntryData(this Utf8JsonWriter writer, IReadOnlyDictionary<string, object> data)
    
        writer.WriteStartObject(Data);

        foreach (var item in data)
        
            writer.WritePropertyName(item.Key);

            var type = item.Value?.GetType() ?? typeof(object);
            JsonSerializer.Serialize(writer, item.Value, type);
        

        writer.WriteEndObject();
    

    private static void WriteEntries(this Utf8JsonWriter writer, IReadOnlyDictionary<string, HealthReportEntry> healthReportEntries)
    
        writer.WriteStartArray(Results);

        foreach (var entry in healthReportEntries)
        
            writer.WriteStartObject();

            writer.WriteString(Name, entry.Key);
            writer.WriteString(Status, entry.Value.Status.ToString("G"));

            if (entry.Value.Description != null)
                writer.WriteString(Description, entry.Value.Description);

            if (entry.Value.Data.Count > 0)
                writer.WriteEntryData(entry.Value.Data);

            writer.WriteEndObject();
        

        writer.WriteEndArray();
    

那么你可以有如下的 IDocumentFilter 实现:

public class HealthChecksDocumentFilter : IDocumentFilter

    private const string _name = "Heartbeat";
    private const string _operationId = "GetHeartbeat";
    private const string _summary = "Get System Heartbeat";
    private const string _description = "Get the heartbeat of the system. If the system is OK, status 200 will be returned, else status 503.";

    private const string _okCode = "200";
    private const string _okDescription = "Healthy";
    private const string _notOkCode = "503";
    private const string _notOkDescription = "Not Healthy";

    private const string _typeString = "string";
    private const string _typeArray = "array";
    private const string _typeObject = "object";
    private const string _applicationJson = "application/json";
    private const string _timespanFormat = "[-][d'.']hh':'mm':'ss['.'fffffff]";
    

    public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
    
        ApplyComponentHealthStatus(swaggerDoc);
        ApplyComponentHealthReportEntry(swaggerDoc);
        ApplyComponentHealthReport(swaggerDoc);

        ApplyPathHeartbeat(swaggerDoc);
    

    private IList<IOpenApiAny> GetHealthStatusValues()
    
        return typeof(HealthStatus)
            .GetEnumValues()
            .Cast<object>()
            .Select(value => (IOpenApiAny)new OpenApiString(value.ToString()))
            .ToList();
    

    private void ApplyComponentHealthStatus(OpenApiDocument swaggerDoc)
    
        swaggerDoc?.Components.Schemas.Add(nameof(HealthStatus), new OpenApiSchema
        
            Type = _typeString,
            Enum = GetHealthStatusValues()
        );
    

    private void ApplyComponentHealthReportEntry(OpenApiDocument swaggerDoc)
    
        swaggerDoc?.Components.Schemas.Add(nameof(HealthReportEntry), new OpenApiSchema
        
            Type = _typeObject,
            Properties = new Dictionary<string, OpenApiSchema>
            
                
                    HeartbeatUtility.Name,
                    new OpenApiSchema
                    
                        Type = _typeString
                    
                ,
                
                    HeartbeatUtility.Status,
                    new OpenApiSchema
                    
                        Reference = new OpenApiReference
                        
                            Type = ReferenceType.Schema,
                            Id = nameof(HealthStatus)
                        
                    
                ,
                
                    HeartbeatUtility.Description,
                    new OpenApiSchema
                    
                        Type = _typeString,
                        Nullable = true
                    
                ,
                
                    HeartbeatUtility.Data,
                    new OpenApiSchema
                    
                        Type = _typeObject,
                        Nullable = true,
                        AdditionalProperties = new OpenApiSchema()
                    
                
            
        );
    

    private void ApplyComponentHealthReport(OpenApiDocument swaggerDoc)
    
        swaggerDoc?.Components.Schemas.Add(nameof(HealthReport), new OpenApiSchema()
        
            Type = _typeObject,
            Properties = new Dictionary<string, OpenApiSchema>
            
                
                    HeartbeatUtility.Status,
                    new OpenApiSchema
                    
                        Reference = new OpenApiReference
                        
                            Type = ReferenceType.Schema,
                            Id = nameof(HealthStatus)
                        
                    
                ,
                
                    HeartbeatUtility.TotalTime,
                    new OpenApiSchema
                    
                        Type = _typeString,
                        Format = _timespanFormat,
                        Nullable = true
                    
                ,
                
                    HeartbeatUtility.Results,
                    new OpenApiSchema
                    
                        Type = _typeArray,
                        Nullable = true,
                        Items = new OpenApiSchema
                        
                            Reference = new OpenApiReference
                            
                                Type = ReferenceType.Schema,
                                Id = nameof(HealthReportEntry)
                            
                        
                    
                
            
        );

    

    private void ApplyPathHeartbeat(OpenApiDocument swaggerDoc)
    
        swaggerDoc?.Paths.Add(HeartbeatUtility.Path, new OpenApiPathItem
        
            Operations = new Dictionary<OperationType, OpenApiOperation>
            
                
                    OperationType.Get,
                    new OpenApiOperation
                    
                        Summary = _summary,
                        Description = _description,
                        OperationId = _operationId,
                        Tags = new List<OpenApiTag>
                        
                            new OpenApiTag
                            
                                Name = _name
                            
                        ,
                        Responses = new OpenApiResponses
                        
                            
                                _okCode,
                                new OpenApiResponse
                                
                                    Description = _okDescription,
                                    Content = new Dictionary<string, OpenApiMediaType>
                                    
                                        
                                            _applicationJson,
                                            new OpenApiMediaType
                                            
                                                Schema = new OpenApiSchema
                                                
                                                    Reference = new OpenApiReference
                                                    
                                                        Type = ReferenceType.Schema,
                                                        Id = nameof(HealthReport)
                                                    
                                                
                                            
                                        
                                    
                                
                            ,
                            
                                _notOkCode,
                                new OpenApiResponse
                                
                                    Description = _notOkDescription,
                                    Content = new Dictionary<string, OpenApiMediaType>
                                    
                                        
                                            _applicationJson,
                                            new OpenApiMediaType
                                            
                                                Schema = new OpenApiSchema
                                                
                                                    Reference = new OpenApiReference
                                                    
                                                        Type = ReferenceType.Schema,
                                                        Id = nameof(HealthReport)
                                                    
                                                
                                            
                                        
                                    
                                
                            
                        
                    
                
            
        );
    

添加到您的 swaggergen 选项中

options.DocumentFilter<HealthChecksDocumentFilter>();

【讨论】:

【参考方案3】:

我的解决方法是添加以下虚拟控制器。

using HealthChecks.UI.Client;
using Microsoft.AspNetCore.Mvc;
using System;

[Route("[controller]")]
[ApiController]
[Produces("application/json")]
public class HealthController: ControllerBase

    [HttpGet("")]
    public UIHealthReport Health()
    
        throw new NotImplementedException("");
    

【讨论】:

【参考方案4】:

我使用了这种方法,它对我很有效:https://www.codit.eu/blog/documenting-asp-net-core-health-checks-with-openapi

添加一个新的控制器,例如HealthController 并将 HealthCheckService 注入到构造函数中。当您在 Startup.cs 中调用 AddHealthChecks 时,会将 HealthCheckService 作为依赖项添加:

当你重建时,HealthController 应该出现在 Swagger 中:

[Route("api/v1/health")]
public class HealthController : Controller

    private readonly HealthCheckService _healthCheckService;
    public HealthController(HealthCheckService healthCheckService)
    
        _healthCheckService = healthCheckService;
    
     
    /// <summary>
    /// Get Health
    /// </summary>
    /// <remarks>Provides an indication about the health of the API</remarks>
    /// <response code="200">API is healthy</response>
    /// <response code="503">API is unhealthy or in degraded state</response>
    [HttpGet]
    [ProducesResponseType(typeof(HealthReport), (int)HttpStatusCode.OK)]
    [SwaggerOperation(OperationId = "Health_Get")]
    public async Task<IActionResult> Get()
    
        var report = await _healthCheckService.CheckHealthAsync();

        return report.Status == HealthStatus.Healthy ? Ok(report) : StatusCode((int)HttpStatusCode.ServiceUnavailable, report);
    

我注意到的一件事是端点仍然是“/health”(或您在 Startup.cs 中设置的任何内容)而不是“/api/vxx/health”,但它仍会在 Swagger 中正确显示。

【讨论】:

这是 IMO 的最佳答案。我不确定为什么微软没有在官方文档中记录这种用法,因为在很多情况下“官方”方法并没有削减它。【参考方案5】:

仍在寻找更好的解决方案,但穷人对这个问题的解决方案看起来像这样:

public const string HealthCheckEndpoint = "/my/healthCheck/endpoint";

public void Apply(SwaggerDocument swaggerDoc, DocumentFilterContext context)

    var pathItem = new PathItem();
    pathItem.Get = new Operation()
    
        Tags = new[]  "ApiHealth" ,
        Produces = new[]  "application/json" 
    ;

    var properties = new Dictionary<string, Schema>();
    properties.Add("status", new Schema() Type = "string" );
    properties.Add("errors", new Schema() Type = "array" );
    
    var exampleObject = new  status = "Healthy", errors = new List<string>();

    pathItem.Get.Responses = new Dictionary<string, Response>();
    pathItem.Get.Responses.Add("200", new Response() 
        Description = "OK",
        Schema = new Schema() 
            Properties = properties,
            Example = exampleObject );

    swaggerDoc.Paths.Add(HealthCheckEndpoint, pathItem);

【讨论】:

是否有适用于 Swagger 5.0.0 的版本(使用 OpenApi 对象)? @penny,我看到你解决了,谢谢分享!【参考方案6】:

由于 Swagger 已更新,.NET 2.x 和 3.1/Swagger 4.0.0 和 5.0.0 之间发生了重大变化

以下是适用于 5.0.0 的穷人解决方案的一个版本(参见 eddyP23 答案)。

public class HealthChecksFilter : IDocumentFilter

    public const string HealthCheckEndpoint = @"/healthcheck";

    public void Apply(OpenApiDocument openApiDocument, DocumentFilterContext context)
    
        var pathItem = new OpenApiPathItem();

        var operation = new OpenApiOperation();
        operation.Tags.Add(new OpenApiTag  Name = "ApiHealth" );

        var properties = new Dictionary<string, OpenApiSchema>();
        properties.Add("status", new OpenApiSchema()  Type = "string" );
        properties.Add("errors", new OpenApiSchema()  Type = "array" );

        var response = new OpenApiResponse();
        response.Content.Add("application/json", new OpenApiMediaType
        
            Schema = new OpenApiSchema
            
                Type = "object",
                AdditionalPropertiesAllowed = true,
                Properties = properties,
            
        );

        operation.Responses.Add("200", response);
        pathItem.AddOperation(OperationType.Get, operation);
        openApiDocument?.Paths.Add(HealthCheckEndpoint, pathItem);
    

【讨论】:

【参考方案7】:

没有内置支持,您可以手动开发 poor man's solution like in the accepted answer 或开发类似 this GitHub issue: NetCore 2.2 - Health Check Support 中提到的扩展

Swashbuckle 构建在 ApiExplorer 之上,这是 ASP.NET Core 附带的 API 元数据组件。

如果运行状况检查端点没有由此出现,那么它们将不会被 Swashbuckle 出现。这是 SB 设计的一个基本方面,不太可能很快改变。

IMO,这听起来像是社区附加包的完美候选者(请参阅https://github.com/domaindrivendev/Swashbuckle.AspNetCore#community-packages)。

如果有愿意的贡献者,他们可以启动一个名为 Swashbuckle.AspNetCore.HealthChecks 的新项目,该项目在 SwaggerGenOptions 上公开一个扩展方法以启用该功能 - 例如EnableHealthCheckDescriptions。然后在幕后,这可以实现为一个文档过滤器(参见自述文件),它将相关的操作描述添加到 Swashbuckle 生成的Swagger/OAI 文档中。

【讨论】:

以上是关于在 dotnet core 上的 swagger (openAPI) UI 中集成运行状况检查端点的主要内容,如果未能解决你的问题,请参考以下文章

如何在ASP.Net Core的生产环境中保护swagger ui

如何在ASP.Net Core的生产环境中保护swagger ui

Linux 上的 C# DotNet Entity Framework Core Migrations 错误

OAuth 2.0、Swagger 和 DotNet 核心

如何正确停止运行 dotnet core web 应用程序?

一文掌握Dotnet Core的中间件