在 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 错误