diff --git a/src/WebJobs.Script.WebHost/WebJobsApplicationBuilderExtension.cs b/src/WebJobs.Script.WebHost/WebJobsApplicationBuilderExtension.cs
index 336fcbc925..e65a12444f 100644
--- a/src/WebJobs.Script.WebHost/WebJobsApplicationBuilderExtension.cs
+++ b/src/WebJobs.Script.WebHost/WebJobsApplicationBuilderExtension.cs
@@ -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,
+ });
});
}
}
diff --git a/src/WebJobs.Script/Diagnostics/HealthChecks/ConnectivityHealthCheck.cs b/src/WebJobs.Script/Diagnostics/HealthChecks/ConnectivityHealthCheck.cs
new file mode 100644
index 0000000000..7bf0b1df3a
--- /dev/null
+++ b/src/WebJobs.Script/Diagnostics/HealthChecks/ConnectivityHealthCheck.cs
@@ -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
+{
+ ///
+ /// DRAFT host adapter (Flex Consumption Network Troubleshooter) that surfaces extension-provided
+ /// 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.
+ ///
+ ///
+ /// This keeps the Microsoft.Extensions.Diagnostics.HealthChecks dependency in the host (here)
+ /// and out of every extension: extensions register a plain ,
+ /// and this one adapter turns them into a connectivity-tagged .
+ ///
+ internal sealed class ConnectivityHealthCheck : IHealthCheck
+ {
+ private readonly IEnumerable _validators;
+ private readonly IFunctionMetadataManager _metadataManager;
+
+ public ConnectivityHealthCheck(
+ IEnumerable validators,
+ IFunctionMetadataManager metadataManager)
+ {
+ _validators = validators ?? throw new ArgumentNullException(nameof(validators));
+ _metadataManager = metadataManager ?? throw new ArgumentNullException(nameof(metadataManager));
+ }
+
+ public async Task CheckHealthAsync(
+ HealthCheckContext context, CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(context);
+
+ Dictionary 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(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);
+ }
+ }
+}
diff --git a/src/WebJobs.Script/Diagnostics/HealthChecks/DnsConnectivityHealthCheck.cs b/src/WebJobs.Script/Diagnostics/HealthChecks/DnsConnectivityHealthCheck.cs
new file mode 100644
index 0000000000..4f7d628a4c
--- /dev/null
+++ b/src/WebJobs.Script/Diagnostics/HealthChecks/DnsConnectivityHealthCheck.cs
@@ -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
+{
+ ///
+ /// SDK-free connectivity check that verifies DNS resolution for a configured set of hosts.
+ ///
+ ///
+ /// Prototype for the Network Troubleshooter (DRAFT). Hosts are read from the
+ /// NETWORK_CHECK_DNS_HOSTS 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.
+ ///
+ 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 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();
+ }
+ }
+}
diff --git a/src/WebJobs.Script/Diagnostics/HealthChecks/HealthCheckExtensions.cs b/src/WebJobs.Script/Diagnostics/HealthChecks/HealthCheckExtensions.cs
index 15abb4ffc2..14b1c2666e 100644
--- a/src/WebJobs.Script/Diagnostics/HealthChecks/HealthCheckExtensions.cs
+++ b/src/WebJobs.Script/Diagnostics/HealthChecks/HealthCheckExtensions.cs
@@ -29,6 +29,7 @@ public static IHealthChecksBuilder AddWebJobsScriptHealthChecks(this IHealthChec
.AddWebHostHealthCheck()
.AddScriptHostHealthCheck()
.AddWebJobsStorageHealthCheck()
+ .AddDnsConnectivityHealthCheck()
.AddTelemetryPublisher(HealthCheckTags.Liveness, HealthCheckTags.Readiness)
.UseDynamicHealthCheckService();
return builder;
@@ -146,6 +147,21 @@ public static IHealthChecksBuilder AddWebJobsStorageHealthCheck(this IHealthChec
return builder;
}
+ ///
+ /// Adds an SDK-free DNS connectivity check (Network Troubleshooter prototype).
+ ///
+ /// The builder to register health checks with.
+ /// The original builder, for call chaining.
+ public static IHealthChecksBuilder AddDnsConnectivityHealthCheck(this IHealthChecksBuilder builder)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ builder.AddCheck(
+ HealthCheckNames.DnsConnectivity,
+ tags: [HealthCheckTags.Connectivity],
+ timeout: TimeSpan.FromSeconds(10));
+ return builder;
+ }
+
///
/// Filters a health report to include only specified entries.
///
diff --git a/src/WebJobs.Script/Diagnostics/HealthChecks/HealthCheckNames.cs b/src/WebJobs.Script/Diagnostics/HealthChecks/HealthCheckNames.cs
index 2a5022eed6..3290b2fbbb 100644
--- a/src/WebJobs.Script/Diagnostics/HealthChecks/HealthCheckNames.cs
+++ b/src/WebJobs.Script/Diagnostics/HealthChecks/HealthCheckNames.cs
@@ -24,5 +24,10 @@ internal static class HealthCheckNames
/// The 'azure.functions.webjobs.storage' check monitors connectivity to the WebJobs storage account.
///
public const string WebJobsStorage = Prefix + "webjobs.storage";
+
+ ///
+ /// The 'azure.functions.connectivity.dns' check verifies DNS resolution for configured hosts (Network Troubleshooter prototype).
+ ///
+ public const string DnsConnectivity = Prefix + "connectivity.dns";
}
}
diff --git a/src/WebJobs.Script/Diagnostics/HealthChecks/IConnectivityValidator.cs b/src/WebJobs.Script/Diagnostics/HealthChecks/IConnectivityValidator.cs
new file mode 100644
index 0000000000..0654da9c98
--- /dev/null
+++ b/src/WebJobs.Script/Diagnostics/HealthChecks/IConnectivityValidator.cs
@@ -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
+{
+ ///
+ /// 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 Microsoft.Extensions.Diagnostics.HealthChecks. The host
+ /// adapts all registered validators into a single connectivity .
+ ///
+ ///
+ /// Final home is Microsoft.Azure.WebJobs (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.
+ ///
+ internal interface IConnectivityValidator
+ {
+ /// Gets the trigger binding type this validator handles, e.g. "eventHubTrigger".
+ string TriggerType { get; }
+
+ /// Performs a non-mutating connectivity + auth probe for a single trigger binding.
+ Task ValidateAsync(ConnectivityContext context, CancellationToken cancellationToken);
+ }
+
+ /// The target of a connectivity probe, supplied by the host from a trigger binding.
+ internal sealed class ConnectivityContext
+ {
+ public ConnectivityContext(string triggerType, string connection, IReadOnlyDictionary properties)
+ {
+ TriggerType = triggerType;
+ Connection = connection;
+ Properties = properties;
+ }
+
+ /// Gets the trigger binding type, e.g. "eventHubTrigger".
+ public string TriggerType { get; }
+
+ /// Gets the connection setting name from the binding; the extension resolves it.
+ public string Connection { get; }
+
+ /// Gets the raw binding settings (e.g. eventHubName, queueName); the extension reads what it needs.
+ public IReadOnlyDictionary Properties { get; }
+ }
+
+ /// The result of a connectivity probe.
+ 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);
+ }
+}
diff --git a/test/WebJobs.Script.Tests/Diagnostics/HealthChecks/ConnectivityHealthCheckTests.cs b/test/WebJobs.Script.Tests/Diagnostics/HealthChecks/ConnectivityHealthCheckTests.cs
new file mode 100644
index 0000000000..f293e054c3
--- /dev/null
+++ b/test/WebJobs.Script.Tests/Diagnostics/HealthChecks/ConnectivityHealthCheckTests.cs
@@ -0,0 +1,133 @@
+// 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.Immutable;
+using System.Threading;
+using System.Threading.Tasks;
+using AwesomeAssertions;
+using Microsoft.Azure.WebJobs.Script.Description;
+using Microsoft.Azure.WebJobs.Script.Diagnostics.HealthChecks;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+using Moq;
+using Xunit;
+
+namespace Microsoft.Azure.WebJobs.Script.Tests.Diagnostics.HealthChecks
+{
+ // Demonstrates the extension-owned connectivity abstraction end-to-end (Network Troubleshooter):
+ // an extension registers a plain IConnectivityValidator (no HealthChecks dependency); the host
+ // adapter enumerates the app's triggers, matches the validator by trigger type, and hands it the
+ // binding's connection + settings — turning it into a connectivity health check result.
+ public class ConnectivityHealthCheckTests
+ {
+ [Fact]
+ public async Task CheckHealthAsync_InvokesMatchingValidator_WithBindingConnectionAndSettings()
+ {
+ // Arrange: an app with one Event Hub trigger.
+ BindingMetadata trigger = new()
+ {
+ Name = "events",
+ Type = "eventHubTrigger",
+ Connection = "MyEventHubConnection",
+ Direction = BindingDirection.In,
+ };
+ trigger.Properties["eventHubName"] = "my-hub";
+ FunctionMetadata function = new() { Name = "ProcessEvents" };
+ function.Bindings.Add(trigger);
+
+ Mock metadataManager = new();
+ metadataManager
+ .Setup(m => m.GetFunctionMetadata(It.IsAny(), It.IsAny(), It.IsAny()))
+ .Returns(ImmutableArray.Create(function));
+
+ RecordingValidator validator = new("eventHubTrigger");
+ ConnectivityHealthCheck check = new(new[] { validator }, metadataManager.Object);
+
+ // Act
+ HealthCheckResult result = await check.CheckHealthAsync(new HealthCheckContext());
+
+ // Assert: the adapter found the Event Hub trigger, matched the validator by trigger type,
+ // and handed it the binding's connection + settings.
+ result.Status.Should().Be(HealthStatus.Healthy);
+ validator.Invoked.Should().BeTrue();
+ validator.LastContext.Connection.Should().Be("MyEventHubConnection");
+ validator.LastContext.Properties["eventHubName"].Should().Be("my-hub");
+ }
+
+ [Fact]
+ public async Task CheckHealthAsync_UnhealthyValidator_ReportsUnhealthy()
+ {
+ BindingMetadata trigger = new()
+ {
+ Name = "events",
+ Type = "eventHubTrigger",
+ Connection = "MyEventHubConnection",
+ Direction = BindingDirection.In,
+ };
+ FunctionMetadata function = new() { Name = "ProcessEvents" };
+ function.Bindings.Add(trigger);
+
+ Mock metadataManager = new();
+ metadataManager
+ .Setup(m => m.GetFunctionMetadata(It.IsAny(), It.IsAny(), It.IsAny()))
+ .Returns(ImmutableArray.Create(function));
+
+ RecordingValidator validator = new("eventHubTrigger")
+ {
+ Result = ConnectivityResult.Unhealthy("Auth failed"),
+ };
+ ConnectivityHealthCheck check = new(new[] { validator }, metadataManager.Object);
+
+ HealthCheckResult result = await check.CheckHealthAsync(new HealthCheckContext());
+
+ result.Status.Should().Be(HealthStatus.Unhealthy);
+ }
+
+ [Fact]
+ public async Task CheckHealthAsync_NoValidatorForTrigger_SkipsAndReportsHealthy()
+ {
+ BindingMetadata trigger = new()
+ {
+ Name = "events",
+ Type = "serviceBusTrigger",
+ Connection = "MyServiceBusConnection",
+ Direction = BindingDirection.In,
+ };
+ FunctionMetadata function = new() { Name = "ProcessMessages" };
+ function.Bindings.Add(trigger);
+
+ Mock metadataManager = new();
+ metadataManager
+ .Setup(m => m.GetFunctionMetadata(It.IsAny(), It.IsAny(), It.IsAny()))
+ .Returns(ImmutableArray.Create(function));
+
+ // Only an Event Hubs validator is registered; the Service Bus trigger has no validator yet.
+ RecordingValidator validator = new("eventHubTrigger");
+ ConnectivityHealthCheck check = new(new[] { validator }, metadataManager.Object);
+
+ HealthCheckResult result = await check.CheckHealthAsync(new HealthCheckContext());
+
+ validator.Invoked.Should().BeFalse();
+ result.Status.Should().Be(HealthStatus.Healthy);
+ }
+
+ private sealed class RecordingValidator : IConnectivityValidator
+ {
+ public RecordingValidator(string triggerType) => TriggerType = triggerType;
+
+ public string TriggerType { get; }
+
+ public bool Invoked { get; private set; }
+
+ public ConnectivityContext LastContext { get; private set; }
+
+ public ConnectivityResult Result { get; set; } = ConnectivityResult.Healthy();
+
+ public Task ValidateAsync(ConnectivityContext context, CancellationToken cancellationToken)
+ {
+ Invoked = true;
+ LastContext = context;
+ return Task.FromResult(Result);
+ }
+ }
+ }
+}
diff --git a/test/WebJobs.Script.Tests/Diagnostics/HealthChecks/DynamicHealthCheckServiceTests.cs b/test/WebJobs.Script.Tests/Diagnostics/HealthChecks/DynamicHealthCheckServiceTests.cs
index 04d236199f..6e23af85a2 100644
--- a/test/WebJobs.Script.Tests/Diagnostics/HealthChecks/DynamicHealthCheckServiceTests.cs
+++ b/test/WebJobs.Script.Tests/Diagnostics/HealthChecks/DynamicHealthCheckServiceTests.cs
@@ -7,6 +7,7 @@
using System.Threading.Tasks;
using AwesomeAssertions;
using Microsoft.Azure.WebJobs.Script.Diagnostics.HealthChecks;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
@@ -197,5 +198,59 @@ private void SetupScriptHostHealthService(HealthReport report)
_mockServices.Setup(s => s.GetService(typeof(HealthCheckService)))
.Returns(mockScriptHostHealthService.Object);
}
+
+ // Demonstrates the extension-owned connectivity model (Network Troubleshooter): an extension
+ // registers a connectivity IHealthCheck in the ScriptHost (JobHost) DI scope—exactly as its
+ // IWebJobsStartup.Configure would via services.AddHealthChecks().AddCheck(...)—and the check
+ // flows through the WebHost/ScriptHost merge and the /admin/health/connectivity tag filter,
+ // with no host dependency on the extension's SDK.
+ [Fact]
+ public async Task CheckHealthAsync_ExtensionRegistersConnectivityCheckInScriptHostScope_FlowsThroughConnectivityFilter()
+ {
+ // WebHost scope: a host-owned connectivity check plus a non-connectivity (liveness) check.
+ ServiceCollection webHostServices = new();
+ webHostServices.AddLogging();
+ webHostServices.AddHealthChecks()
+ .AddCheck("host.dns.connectivity", tags: [HealthCheckTags.Connectivity])
+ .AddCheck("host.liveness", tags: [HealthCheckTags.Liveness]);
+ using ServiceProvider webHostProvider = webHostServices.BuildServiceProvider();
+ HealthCheckService webHostHealth = webHostProvider.GetRequiredService();
+
+ // ScriptHost scope: an extension registers its own connectivity check.
+ ServiceCollection scriptHostServices = new();
+ scriptHostServices.AddLogging();
+ scriptHostServices.AddHealthChecks()
+ .AddCheck("ext.eventhubs.connectivity", tags: [HealthCheckTags.Connectivity]);
+ using ServiceProvider scriptHostProvider = scriptHostServices.BuildServiceProvider();
+ _mockManager.Setup(m => m.Services).Returns(scriptHostProvider);
+
+ DynamicHealthCheckService service = new(webHostHealth, _mockManager.Object, _logger);
+
+ // Query with the same predicate the /admin/health/connectivity route uses.
+ HealthReport result = await service.CheckHealthAsync(r => r.Tags.Contains(HealthCheckTags.Connectivity));
+
+ // Both connectivity checks flow through the merge; the non-connectivity check is filtered out.
+ result.Entries.Should().ContainKey("host.dns.connectivity");
+ result.Entries.Should().ContainKey("ext.eventhubs.connectivity");
+ result.Entries.Should().NotContainKey("host.liveness");
+ }
+
+ private sealed class FakeHostConnectivityCheck : IHealthCheck
+ {
+ public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
+ => Task.FromResult(HealthCheckResult.Healthy());
+ }
+
+ private sealed class FakeExtensionConnectivityCheck : IHealthCheck
+ {
+ public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
+ => Task.FromResult(HealthCheckResult.Healthy());
+ }
+
+ private sealed class FakeLivenessCheck : IHealthCheck
+ {
+ public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
+ => Task.FromResult(HealthCheckResult.Healthy());
+ }
}
}
\ No newline at end of file