Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Health Endpoint for DAB Engine #2515

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion src/Config/Converters/DataSourceConverterFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public DataSourceConverter(bool replaceEnvVar)

public override DataSource? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
DataSource dataSource = new(DatabaseType.MSSQL, string.Empty, null);
DataSource dataSource = new(DatabaseType.MSSQL, string.Empty, null, null);
sezal98 marked this conversation as resolved.
Show resolved Hide resolved
if (reader.TokenType is JsonTokenType.StartObject)
{

Expand Down Expand Up @@ -114,6 +114,18 @@ public DataSourceConverter(bool replaceEnvVar)
dataSource = dataSource with { Options = optionsDict };
break;
}
case "health":
if (reader.TokenType == JsonTokenType.Null)
{
dataSource = dataSource with { Health = null };
}
else
{
DabHealthCheckConfig health = JsonSerializer.Deserialize<DabHealthCheckConfig>(ref reader, options) ?? new();
dataSource = dataSource with { Health = health };
}

break;
default:
throw new JsonException($"Unexpected property {propertyName} while deserializing DataSource.");
}
Expand Down
7 changes: 6 additions & 1 deletion src/Config/ObjectModel/DataSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ namespace Azure.DataApiBuilder.Config.ObjectModel;
/// <param name="DatabaseType">Type of database to use.</param>
/// <param name="ConnectionString">Connection string to access the database.</param>
/// <param name="Options">Custom options for the specific database. If there are no options, this could be null.</param>
public record DataSource(DatabaseType DatabaseType, string ConnectionString, Dictionary<string, object?>? Options)
/// <param name="Health">Health check configuration for the database. In case null, follow old format of health check.</param>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't this be updated? since we now want to have both old format on the original address and new format on\health?

public record DataSource(
DatabaseType DatabaseType,
string ConnectionString,
Dictionary<string, object?>? Options,
DabHealthCheckConfig? Health = null)
{
/// <summary>
/// Converts the <c>Options</c> dictionary into a typed options object.
Expand Down
4 changes: 4 additions & 0 deletions src/Config/ObjectModel/Entity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ namespace Azure.DataApiBuilder.Config.ObjectModel;
/// <param name="Mappings">Defines mappings between database fields and GraphQL and REST fields.</param>
/// <param name="Cache">Defines whether to allow caching for a read operation's response and
/// how long that response should be valid in the cache.</param>
/// <param name="Health">Defines the health check configuration for the entity.</param>
public record Entity
{
public const string PROPERTY_PATH = "path";
Expand All @@ -31,6 +32,7 @@ public record Entity
public Dictionary<string, string>? Mappings { get; init; }
public Dictionary<string, EntityRelationship>? Relationships { get; init; }
public EntityCacheOptions? Cache { get; init; }
public DabHealthCheckConfig? Health { get; init; }

[JsonIgnore]
public bool IsLinkingEntity { get; init; }
Expand All @@ -44,6 +46,7 @@ public Entity(
Dictionary<string, string>? Mappings,
Dictionary<string, EntityRelationship>? Relationships,
EntityCacheOptions? Cache = null,
DabHealthCheckConfig? Health = null,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the name could be simplified to just say HealthCheck in alignment with other options like Mappings. Since this is already in the context of Dab and its a config, the prefix Dab and suffix Config are not needed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why was this resolved? i don't see an update.

bool IsLinkingEntity = false)
{
this.Source = Source;
Expand All @@ -53,6 +56,7 @@ public Entity(
this.Mappings = Mappings;
this.Relationships = Relationships;
this.Cache = Cache;
this.Health = Health;
this.IsLinkingEntity = IsLinkingEntity;
}

Expand Down
18 changes: 18 additions & 0 deletions src/Config/ObjectModel/HealthCheck/DabConfigurationDetails.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Azure.DataApiBuilder.Config.ObjectModel
{
/// <summary>
/// The health report of the DAB Enigne.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Copy paste issue, fix description

/// </summary>
public record DabConfigurationDetails
sezal98 marked this conversation as resolved.
Show resolved Hide resolved
{
public bool Rest { get; init; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is bool sufficient or do we need to provide the complete details of the Rest runtime options? same for GraphQL

public bool GraphQL { get; init; }
public bool Caching { get; init; }
public bool Telemetry { get; init; }
public HostMode Mode { get; init; }
public required string DabSchema { get; init; }
}
}
20 changes: 20 additions & 0 deletions src/Config/ObjectModel/HealthCheck/DabHealthCheckConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Text.Json.Serialization;

namespace Azure.DataApiBuilder.Config.ObjectModel;

public record DabHealthCheckConfig
{
public bool Enabled { get; set; } // Default value: false
public string? Moniker { get; set; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought we are updating this and not use Moniker.

public string? Query { get; set; } // "query: "SELECT TOP 1 1"

[JsonPropertyName("threshold-ms")]
public int? ThresholdMs { get; set; } // (Default: 10000ms)
public string Role { get; set; } = "*"; // "roles": ["anonymous", "authenticated"] (Default: *)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: be more descriptive of what this Role is for, could be RoleAuthorizedToViewHealth e.g. Set JsonPropertyName to be Role


[JsonPropertyName("max-dop")]
public int MaxDop { get; set; } = 1; // Parallelized streams to run Health Check (Default: 1)
}
45 changes: 45 additions & 0 deletions src/Config/ObjectModel/HealthCheck/DabHealthCheckReport.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Text.Json.Serialization;

namespace Azure.DataApiBuilder.Config.ObjectModel
{
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum HealthStatus
{
Healthy,
Unhealthy
}

/// <summary>
/// The health report of the DAB Enigne.
/// </summary>
public record DabHealthCheckReport
{
/// <summary>
/// The health status of the service.
/// </summary>
public HealthStatus HealthStatus { get; init; }

/// <summary>
/// The version of the service.
/// </summary>
public string? Version { get; set; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would this ever be null? Wont we have a version always


/// <summary>
/// The name of the dab service.
/// </summary>
public string? AppName { get; set; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same: AppName should always be present


/// <summary>
/// The configuration details of the dab service.
/// </summary>
public DabConfigurationDetails? DabConfigurationDetails { get; set; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dab cannot start without configuration, this has to be non-null


/// <summary>
/// The health check results of the dab service.
/// </summary>
public DabHealthCheckResults? HealthCheckResults { get; set; }
}
}
29 changes: 29 additions & 0 deletions src/Config/ObjectModel/HealthCheck/DabHealthCheckResults.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Azure.DataApiBuilder.Config.ObjectModel
{
/// <summary>
/// The health report of the DAB Enigne.
sezal98 marked this conversation as resolved.
Show resolved Hide resolved
/// </summary>
public record DabHealthCheckResults
{
public List<HealthCheckResultEntry>? DataSourceHealthCheckResults { get; init; }
public List<HealthCheckResultEntry>? EntityHealthCheckResults { get; init; }
}

public class HealthCheckResultEntry
{
public string? Name { get; init; }
sezal98 marked this conversation as resolved.
Show resolved Hide resolved
public HealthStatus HealthStatus { get; init; }
public string? Description { get; init; }
public string? Exception { get; init; }
public ResponseTimeData? ResponseTimeData { get; init; }
}

public class ResponseTimeData
{
public int? ResponseTimeMs { get; init; }
public int? MaxAllowedResponseTimeMs { get; init; }
}
}
3 changes: 3 additions & 0 deletions src/Config/ObjectModel/HostMode.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Text.Json.Serialization;

namespace Azure.DataApiBuilder.Config.ObjectModel;

[JsonConverter(typeof(JsonStringEnumConverter))]
public enum HostMode
{
Development,
Expand Down
1 change: 1 addition & 0 deletions src/Config/ObjectModel/RuntimeOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public record RuntimeOptions
public TelemetryOptions? Telemetry { get; init; }
public EntityCacheOptions? Cache { get; init; }
public PaginationOptions? Pagination { get; init; }
public DabHealthCheckConfig? Health { get; init; }

[JsonPropertyName("log-level")]
public LogLevelOptions? LoggerLevel { get; init; }
Expand Down
2 changes: 1 addition & 1 deletion src/Service.Tests/dab-config.MsSql.json
Original file line number Diff line number Diff line change
Expand Up @@ -3728,4 +3728,4 @@
}
}
}
}
}
sezal98 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.IO;
using System.Text.Json;
using System.Threading.Tasks;
using Azure.DataApiBuilder.Config.ObjectModel;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Logging;

namespace Azure.DataApiBuilder.Service.HealthCheck
{
/// <summary>
/// Creates a JSON response for the health check endpoint using the provided health report.
/// If the response has already been created, it will be reused.
/// </summary>
public class EnhancedHealthReportResponseWriter
{
// Dependencies
private ILogger? _logger;
private HealthCheckUtlity _healthCheckUtlity;

// Constants
private const string JSON_CONTENT_TYPE = "application/json; charset=utf-8";

public EnhancedHealthReportResponseWriter(ILogger<HealthReportResponseWriter>? logger, HealthCheckUtlity healthCheckUtlity)
{
_logger = logger;
_healthCheckUtlity = healthCheckUtlity;
}

/* {
"runtime" : {
"health" : {
"enabled": true, (default: true)
"cache-ttl": 5, (optional default: 5)
"max-dop": 5, (optional default: 1)
"roles": ["anonymous", "authenticated"] (optional default: *)
}
},
{
"data-source" : {
"health" : {
"moniker": "sqlserver", (optional default: NULL)
"enabled": true, (default: true)
"query": "SELECT TOP 1 1", (option)
"threshold-ms": 100 (optional default: 10000)
}
},
{
"<entity-name>": {
"health": {
"enabled": true, (default: true)
"filter": "Id eq 1" (optional default: null),
"first": 1 (optional default: 1),
"threshold-ms": 100 (optional default: 10000)
}
} */
/// <summary>
/// Function provided to the health check middleware to write the response.
/// </summary>
/// <param name="context">HttpContext for writing the response.</param>
/// <param name="healthReport">Result of health check(s).</param>
/// <param name="config">Result of health check(s).</param>
/// <returns>Writes the http response to the http context.</returns>
public Task WriteResponse(HttpContext context, HealthReport healthReport, RuntimeConfig config)
{
context.Response.ContentType = JSON_CONTENT_TYPE;
LogTrace("Writing health report response.");
DabHealthCheckReport dabHealthCheckReport = _healthCheckUtlity.GetHealthCheckResponse(healthReport, config);
return context.Response.WriteAsync(JsonSerializer.Serialize(dabHealthCheckReport, options: new JsonSerializerOptions { WriteIndented = true, DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull }));
}

/// <summary>
/// Logs a trace message if a logger is present and the logger is enabled for trace events.
/// </summary>
/// <param name="message">Message to emit.</param>
private void LogTrace(string message)
{
if (_logger is not null && _logger.IsEnabled(LogLevel.Trace))
{
_logger.LogTrace(message);
}
}
}
}
Loading