【问题标题】:Integrating HealthCheck endpoint into swagger (open API) UI on dotnet core在 dotnet core 上的 swagger (openAPI) UI 中集成运行状况检查端点
【发布时间】: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),但维护起来并不好。

【问题讨论】:

    标签: swagger swagger-ui openapi asp.net-core-2.2


    【解决方案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 文档中。

            【讨论】:

              猜你喜欢
              • 2020-04-25
              • 1970-01-01
              • 2021-03-05
              • 1970-01-01
              • 1970-01-01
              • 2018-07-01
              • 2016-11-30
              • 1970-01-01
              • 2020-10-08
              相关资源
              最近更新 更多