Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,12 @@ static bool Predicate(HttpContext context)
Predicate = r => r.Tags.Contains(HealthCheckTags.Readiness),
ResponseWriter = HealthCheckResponseWriter.WriteResponseAsync,
});

app.UseHealthChecks($"{healthPrefix}/connectivity", new HealthCheckOptions
{
Predicate = r => r.Tags.Contains(HealthCheckTags.Connectivity),
ResponseWriter = HealthCheckResponseWriter.WriteResponseAsync,
});
});
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs.Script.Description;
using Microsoft.Extensions.Diagnostics.HealthChecks;

namespace Microsoft.Azure.WebJobs.Script.Diagnostics.HealthChecks
{
/// <summary>
/// DRAFT host adapter (Flex Consumption Network Troubleshooter) that surfaces extension-provided
/// <see cref="IConnectivityValidator"/> implementations as a single connectivity health check.
/// It enumerates the app's triggers, matches each to a registered validator by trigger type,
/// invokes it with the binding's connection + settings, and aggregates the results.
/// </summary>
/// <remarks>
/// This keeps the <c>Microsoft.Extensions.Diagnostics.HealthChecks</c> dependency in the host (here)
/// and out of every extension: extensions register a plain <see cref="IConnectivityValidator"/>,
/// and this one adapter turns them into a <c>connectivity</c>-tagged <see cref="IHealthCheck"/>.
/// </remarks>
internal sealed class ConnectivityHealthCheck : IHealthCheck
{
private readonly IEnumerable<IConnectivityValidator> _validators;
private readonly IFunctionMetadataManager _metadataManager;

public ConnectivityHealthCheck(
IEnumerable<IConnectivityValidator> validators,
IFunctionMetadataManager metadataManager)
{
_validators = validators ?? throw new ArgumentNullException(nameof(validators));
_metadataManager = metadataManager ?? throw new ArgumentNullException(nameof(metadataManager));
}

public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(context);

Dictionary<string, object> data = new();
bool anyUnhealthy = false;

foreach (FunctionMetadata function in _metadataManager.GetFunctionMetadata())
{
BindingMetadata trigger = function.Trigger;
if (trigger is null || string.IsNullOrEmpty(trigger.Connection))
{
// Not a remote trigger (e.g. HTTP/timer) or nothing to probe.
continue;
}

IConnectivityValidator validator = _validators.FirstOrDefault(
v => string.Equals(v.TriggerType, trigger.Type, StringComparison.OrdinalIgnoreCase));
if (validator is null)
{
// No validator shipped for this trigger's extension (bundle-versioned coverage).
continue;
}

ConnectivityContext probeContext = new(
trigger.Type,
trigger.Connection,
new Dictionary<string, object>(trigger.Properties));

ConnectivityResult result = await validator
.ValidateAsync(probeContext, cancellationToken)
.ConfigureAwait(false);

data[function.Name] = result.IsHealthy ? "Healthy" : $"Unhealthy: {result.Details}";
anyUnhealthy |= !result.IsHealthy;
}

return anyUnhealthy
? HealthCheckResult.Unhealthy("One or more trigger dependencies are unreachable.", data: data)
: HealthCheckResult.Healthy(data: data);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs.Script.Extensions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Diagnostics.HealthChecks;

namespace Microsoft.Azure.WebJobs.Script.Diagnostics.HealthChecks
{
/// <summary>
/// SDK-free connectivity check that verifies DNS resolution for a configured set of hosts.
/// </summary>
/// <remarks>
/// Prototype for the Network Troubleshooter (DRAFT). Hosts are read from the
/// <c>NETWORK_CHECK_DNS_HOSTS</c> setting (comma-separated); the production check will derive
/// targets from trigger binding metadata. Registered on the WebHost scope so it runs in
/// validation mode as well as on a normal worker.
/// </remarks>
internal sealed class DnsConnectivityHealthCheck : IHealthCheck
{
private const string DnsHostsSetting = "NETWORK_CHECK_DNS_HOSTS";

private readonly IConfiguration _configuration;

public DnsConnectivityHealthCheck(IConfiguration configuration)
{
_configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
}

public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(context);

string hostsSetting = _configuration[DnsHostsSetting];
if (string.IsNullOrWhiteSpace(hostsSetting))
{
return HealthCheckResult.Healthy("No DNS hosts configured; connectivity check skipped.");
}

foreach (string host in hostsSetting.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
{
try
{
await Dns.GetHostEntryAsync(host, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex) when (!ex.IsFatal())
{
HealthCheckData data = new() { Area = HealthCheckData.Areas.Connectivity };
data.SetExceptionDetails(ex);
return HealthCheckResult.Unhealthy($"DNS resolution failed for '{host}'.", ex, data);
}
}

return HealthCheckResult.Healthy();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public static IHealthChecksBuilder AddWebJobsScriptHealthChecks(this IHealthChec
.AddWebHostHealthCheck()
.AddScriptHostHealthCheck()
.AddWebJobsStorageHealthCheck()
.AddDnsConnectivityHealthCheck()
.AddTelemetryPublisher(HealthCheckTags.Liveness, HealthCheckTags.Readiness)
.UseDynamicHealthCheckService();
return builder;
Expand Down Expand Up @@ -146,6 +147,21 @@ public static IHealthChecksBuilder AddWebJobsStorageHealthCheck(this IHealthChec
return builder;
}

/// <summary>
/// Adds an SDK-free DNS connectivity check (Network Troubleshooter prototype).
/// </summary>
/// <param name="builder">The builder to register health checks with.</param>
/// <returns>The original builder, for call chaining.</returns>
public static IHealthChecksBuilder AddDnsConnectivityHealthCheck(this IHealthChecksBuilder builder)
{
ArgumentNullException.ThrowIfNull(builder);
builder.AddCheck<DnsConnectivityHealthCheck>(
HealthCheckNames.DnsConnectivity,
tags: [HealthCheckTags.Connectivity],
timeout: TimeSpan.FromSeconds(10));
return builder;
}

/// <summary>
/// Filters a health report to include only specified entries.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,10 @@ internal static class HealthCheckNames
/// The 'azure.functions.webjobs.storage' check monitors connectivity to the WebJobs storage account.
/// </summary>
public const string WebJobsStorage = Prefix + "webjobs.storage";

/// <summary>
/// The 'azure.functions.connectivity.dns' check verifies DNS resolution for configured hosts (Network Troubleshooter prototype).
/// </summary>
public const string DnsConnectivity = Prefix + "connectivity.dns";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.

using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace Microsoft.Azure.WebJobs.Script.Diagnostics.HealthChecks
{
/// <summary>
/// DRAFT abstraction for the Flex Consumption Network Troubleshooter. An extension implements this to
/// validate connectivity to its trigger dependency using its own SDK and connection resolution,
/// without taking a dependency on <c>Microsoft.Extensions.Diagnostics.HealthChecks</c>. The host
/// adapts all registered validators into a single connectivity <see cref="Microsoft.Extensions.Diagnostics.HealthChecks.IHealthCheck"/>.
/// </summary>
/// <remarks>
/// Final home is <c>Microsoft.Azure.WebJobs</c> (the WebJobs SDK), which both the host and the
/// extensions already reference — so extensions add no new package dependency. Defined here in the
/// host only to prototype the mechanism end-to-end.
/// </remarks>
internal interface IConnectivityValidator
{
/// <summary>Gets the trigger binding type this validator handles, e.g. <c>"eventHubTrigger"</c>.</summary>
string TriggerType { get; }

/// <summary>Performs a non-mutating connectivity + auth probe for a single trigger binding.</summary>
Task<ConnectivityResult> ValidateAsync(ConnectivityContext context, CancellationToken cancellationToken);
}

/// <summary>The target of a connectivity probe, supplied by the host from a trigger binding.</summary>
internal sealed class ConnectivityContext
{
public ConnectivityContext(string triggerType, string connection, IReadOnlyDictionary<string, object> properties)
{
TriggerType = triggerType;
Connection = connection;
Properties = properties;
}

/// <summary>Gets the trigger binding type, e.g. <c>"eventHubTrigger"</c>.</summary>
public string TriggerType { get; }

/// <summary>Gets the connection setting name from the binding; the extension resolves it.</summary>
public string Connection { get; }

/// <summary>Gets the raw binding settings (e.g. <c>eventHubName</c>, <c>queueName</c>); the extension reads what it needs.</summary>
public IReadOnlyDictionary<string, object> Properties { get; }
}

/// <summary>The result of a connectivity probe.</summary>
internal sealed class ConnectivityResult
{
private ConnectivityResult(bool isHealthy, string details)
{
IsHealthy = isHealthy;
Details = details;
}

public bool IsHealthy { get; }

public string Details { get; }

public static ConnectivityResult Healthy(string details = null) => new(true, details);

public static ConnectivityResult Unhealthy(string details) => new(false, details);
}
}
Loading
Loading