将运行状况检查端点集成到dotnet核心上的swagger(开放API)UI中

ve7v8dk2  于 2022-11-06  发布在  其他
关注(0)|答案(7)|浏览(127)

我正在使用here所描述的Dotnet Core健康检查。简而言之,它看起来像这样:
首先,按如下方式配置服务:

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

然后,按如下方式注册端点:

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

我们还使用了Swagger(也称为开放API),我们通过Swagger UI看到所有端点,但不包括运行状况检查端点。
有没有办法把它添加到控制器方法中,以便Swagger自动拾取端点,或者用另一种方式把它与Swagger集成在一起?
到目前为止,我找到的最好的解决方案是添加一个自定义硬编码端点(like described here),但是维护起来并不好。

pengsaosao

pengsaosao1#

我使用了这种方法,效果很好:https://www.codit.eu/blog/documenting-asp-net-core-health-checks-with-openapi
添加一个新的控制器,例如HealthController,并将HealthCheckService注入到构造函数中。当您在Startup中调用AddHealthChecks时,HealthCheckService将作为依赖项添加。cs:
当您重建时,健康控制器应该出现在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中。

oogrdqng

oogrdqng2#

由于Swagger已更新,.NET 2.x和3.1/Swagger 4.0.0和5.0.0之间有一个突破性的变化
下面是一个适用于5.0.0的穷人解决方案(参见eddyP 23答案)。

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);
        }
    }
jrcvhitl

jrcvhitl3#

仍然在寻找更好的解决办法,但一个穷人对这个问题的解决办法看起来是这样的:

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);
}
wbgh16ku

wbgh16ku4#

没有内置的支持,你要么手动开发一个poor man's solution like in the accepted answer,要么开发一个扩展,就像GitHub中提到的:NetCore 2.2 -运行状况检查支持
Swashbuckle是在ApiExplorer(API元数据组件)之上构建的,该组件随ASP.NET核心一起提供。
如果健康检查的端点没有被暴露出来,那么它们也不会被Swashbuckle暴露出来。这是SB设计的一个基本方面,不太可能在短期内改变。
我觉得,这听起来像是一个社区附加包的完美候选者(请参见https://github.com/domaindrivendev/Swashbuckle.AspNetCore#community-packages)。
如果有愿意参与的人,他们可以开始一个名为Swashbuckle.AspNetCore.HealthChecks的新项目,该项目公开了SwaggerGenOptions上的一个扩展方法,以启用该功能-例如EnableHealthCheckDescriptions。然后,在后台,这可以实现为一个文档过滤器(请参阅自述文件),该过滤器将相关的操作描述添加到由Swashbuckle生成的Swagger/OAI文档中。

mwyxok5s

mwyxok5s5#

将运行状况检查端点集成到.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>();
});
des4xlb0

des4xlb06#

我将poor man的解决方案升级为更具描述性的文档,它将在Swashbuckle 5中正确显示响应类型。我在Swagger UI中获得了端点,但Open API Specification中的描述很笨拙。然后我将特定的healthcheck数据类型添加到了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>();
lhcgjxsq

lhcgjxsq7#

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

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("");
    }
}

相关问题