将HealthCheck端点集成到dotnet核心上的swagger(开放API)UI中

时间:2019-01-25 09:21:05

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

我正在使用here中所述的Dotnet Core健康检查。简而言之,它看起来像这样:

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

class User {
    use YourTrait {
        roles as protected traitRoles;
    }
}

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

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

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

是否可以将其添加到控制器方法中,以便Swagger自动获取端点,或者可以通过其他方式将其与swagger集成?

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

7 个答案:

答案 0 :(得分:4)

由于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);
    }
}

答案 1 :(得分:2)

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

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

答案 2 :(得分:1)

将健康检查端点集成到 .NET 5 上的 Swagger(开放 API)用户界面

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

答案 3 :(得分:0)

没有内置的支持,您是手动开发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文档中。

答案 4 :(得分:0)

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

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

答案 5 :(得分:0)

我使用了这种方法,对我来说效果很好: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中。

答案 6 :(得分:0)

我将穷人的解决方案升级为更具描述性的文档,它将在 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>();