Skip to content

Commit 88360e7

Browse files
committed
HostHealthMonitor improvements / reactivation
1 parent b3a8816 commit 88360e7

27 files changed

+763
-366
lines changed

sample/host.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,12 @@
2222
"prefetchCount": 1000,
2323
"batchCheckpointFrequency": 1
2424
},
25+
"healthMonitor": {
26+
"enabled": true,
27+
"healthCheckInterval": "00:00:10",
28+
"healthCheckWindow": "00:02:00",
29+
"healthCheckThreshold": 6,
30+
"counterThreshold": 0.80
31+
},
2532
"functionTimeout": "00:05:00"
2633
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the MIT License. See License.txt in the project root for license information.
3+
4+
using System;
5+
6+
namespace Microsoft.Azure.WebJobs.Script
7+
{
8+
public class HostHealthMonitorConfiguration
9+
{
10+
internal const float DefaultCounterThreshold = 0.80F;
11+
12+
public HostHealthMonitorConfiguration()
13+
{
14+
Enabled = true;
15+
16+
// these default numbers translate to a 50% health rating
17+
// over the window.
18+
HealthCheckInterval = TimeSpan.FromSeconds(10);
19+
HealthCheckWindow = TimeSpan.FromMinutes(2);
20+
HealthCheckThreshold = 6;
21+
CounterThreshold = DefaultCounterThreshold;
22+
}
23+
24+
/// <summary>
25+
/// Gets or sets a value indicating whether host health monitoring
26+
/// is enabled.
27+
/// </summary>
28+
public bool Enabled { get; set; }
29+
30+
/// <summary>
31+
/// Gets or sets the interval at which host health will be checked
32+
/// for threshold overages.
33+
/// </summary>
34+
public TimeSpan HealthCheckInterval { get; set; }
35+
36+
/// <summary>
37+
/// Gets or sets a value defining the sliding time window
38+
/// that <see cref="HealthCheckThreshold"/> applies to.
39+
/// </summary>
40+
public TimeSpan HealthCheckWindow { get; set; }
41+
42+
/// <summary>
43+
/// Gets or sets the threshold for the <see cref="HealthCheckWindow"/>.
44+
/// When the host has been unhealthy a number of times exceeding this
45+
/// threshold, the host app domain will be recycled in an attempt to recover.
46+
/// </summary>
47+
public int HealthCheckThreshold { get; set; }
48+
49+
/// <summary>
50+
/// Gets or sets the counter threshold for all counters.
51+
/// </summary>
52+
public float CounterThreshold { get; set; }
53+
}
54+
}

src/WebJobs.Script/Config/ScriptHostConfiguration.cs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public ScriptHostConfiguration()
2424
LogFilter = new LogCategoryFilter();
2525
RootExtensionsPath = ConfigurationManager.AppSettings[EnvironmentSettingNames.AzureWebJobsExtensionsPath];
2626
LoggerFactoryBuilder = new DefaultLoggerFactoryBuilder();
27-
HostHealthMonitorEnabled = true;
27+
HostHealthMonitor = new HostHealthMonitorConfiguration();
2828
}
2929

3030
/// <summary>
@@ -95,12 +95,6 @@ public ScriptHostConfiguration()
9595
/// </summary>
9696
public bool SwaggerEnabled { get; set; }
9797

98-
/// <summary>
99-
/// Gets or sets a value indicating whether the hosting environment will be monitored
100-
/// for health (e.g. socket thresholds, etc.). Default is true.
101-
/// </summary>
102-
public bool HostHealthMonitorEnabled { get; set; }
103-
10498
/// <summary>
10599
/// Gets or sets a value indicating whether the host is running
106100
/// outside of the normal Azure hosting environment. E.g. when running
@@ -135,5 +129,10 @@ public ScriptHostConfiguration()
135129
/// Gets or sets a test hook for modifying the configuration after host.json has been processed.
136130
/// </summary>
137131
internal Action<ScriptHostConfiguration> OnConfigurationApplied { get; set; }
132+
133+
/// <summary>
134+
/// Gets the <see cref="HostHealthMonitorConfiguration"/> to use.
135+
/// </summary>
136+
public HostHealthMonitorConfiguration HostHealthMonitor { get; }
138137
}
139138
}

src/WebJobs.Script/GlobalSuppressions.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,4 +238,5 @@
238238
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "Io", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.Scale.ApplicationPerformanceCounters.#WriteIoBytes")]
239239
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1709:IdentifiersShouldBeCasedCorrectly", MessageId = "Io", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.Scale.ApplicationPerformanceCounters.#OtherIoBytes")]
240240
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.ScriptHostManager.#.ctor(Microsoft.Azure.WebJobs.Script.ScriptHostConfiguration,Microsoft.Azure.WebJobs.Script.Config.ScriptSettingsManager,Microsoft.Azure.WebJobs.Script.IScriptHostFactory,Microsoft.Azure.WebJobs.Script.Eventing.IScriptEventManager,Microsoft.Azure.WebJobs.Script.IScriptHostEnvironment,Microsoft.Azure.WebJobs.Script.Scale.HostPerformanceManager)")]
241-
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", MessageId = "_hostHealthCheckTimer", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.ScriptHostManager.#Dispose(System.Boolean)")]
241+
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", MessageId = "_hostHealthCheckTimer", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.ScriptHostManager.#Dispose(System.Boolean)")]
242+
[assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Scope = "member", Target = "Microsoft.Azure.WebJobs.Script.ScriptHostManager.#RunAndBlock(System.Threading.CancellationToken)")]

src/WebJobs.Script/Host/ScriptHost.cs

Lines changed: 70 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public class ScriptHost : JobHost
4848
private static readonly string[] WellKnownHostJsonProperties = new[]
4949
{
5050
"id", "functionTimeout", "http", "watchDirectories", "functions", "queues", "serviceBus",
51-
"eventHub", "tracing", "singleton", "logger", "aggregator", "applicationInsights"
51+
"eventHub", "tracing", "singleton", "logger", "aggregator", "applicationInsights", "healthMonitor"
5252
};
5353

5454
private string _instanceId;
@@ -100,6 +100,8 @@ protected internal ScriptHost(IScriptHostEnvironment environment,
100100
_proxyClient = proxyClient;
101101
}
102102

103+
public event EventHandler HostInitializing;
104+
103105
public event EventHandler HostInitialized;
104106

105107
public event EventHandler HostStarted;
@@ -269,7 +271,10 @@ internal static void AddFunctionError(Dictionary<string, Collection<string>> fun
269271
await base.CallAsync(method, arguments, cancellationToken);
270272
}
271273

272-
protected virtual void Initialize()
274+
/// <summary>
275+
/// Performs all required initialization on the host. Must be called before the host is started.
276+
/// </summary>
277+
public void Initialize()
273278
{
274279
FileUtility.EnsureDirectoryExists(ScriptConfig.RootScriptPath);
275280
string hostLogPath = Path.Combine(ScriptConfig.RootLogPath, "Host");
@@ -298,19 +303,6 @@ protected virtual void Initialize()
298303
_hostConfig.UseDevelopmentSettings();
299304
}
300305

301-
// Ensure we always have an ILoggerFactory,
302-
// regardless of whether AppInsights is registered or not
303-
if (_hostConfig.LoggerFactory == null)
304-
{
305-
_hostConfig.LoggerFactory = new LoggerFactory();
306-
307-
// If we've created the LoggerFactory, then we are responsible for
308-
// disposing. Store this locally for disposal later. We can't rely
309-
// on accessing this directly from ScriptConfig.HostConfig as the
310-
// ScriptConfig is re-used for every host.
311-
_loggerFactory = _hostConfig.LoggerFactory;
312-
}
313-
314306
Func<string, FunctionDescriptor> funcLookup = (name) => this.GetFunctionOrNull(name);
315307
_hostConfig.AddService(funcLookup);
316308

@@ -349,6 +341,16 @@ protected virtual void Initialize()
349341
TraceWriter = new ConsoleTraceWriter(hostTraceLevel);
350342
}
351343

344+
// Before configuration has been fully read, configure a default logger factory
345+
// to ensure we can log any configuration errors. There's no filters at this point,
346+
// but that's okay since we can't build filters until we apply configuration below.
347+
// We'll recreate the loggers after config is read. We initialize the public logger
348+
// to the startup logger until we've read configuration settings and can create the real logger.
349+
// The "startup" logger is used in this class for startup related logs. The public logger is used
350+
// for all other logging after startup.
351+
ConfigureLoggerFactory();
352+
Logger = _startupLogger = _hostConfig.LoggerFactory.CreateLogger(LogCategories.Startup);
353+
352354
string readingFileMessage = string.Format(CultureInfo.InvariantCulture, "Reading host configuration file '{0}'", hostConfigFilePath);
353355
TraceWriter.Info(readingFileMessage);
354356

@@ -360,41 +362,29 @@ protected virtual void Initialize()
360362
}
361363
catch (JsonException ex)
362364
{
363-
// If there's a parsing error, set up the logger and write out the previous messages so that they're
364-
// discoverable in Application Insights. There's no filter, but that's okay since we cannot parse host.json to
365-
// determine how to build the filtler.
366-
ConfigureDefaultLoggerFactory();
367-
ILogger startupErrorLogger = _hostConfig.LoggerFactory.CreateLogger(LogCategories.Startup);
368-
startupErrorLogger.LogInformation(readingFileMessage);
369-
365+
// If there's a parsing error, write out the previous messages without filters to ensure
366+
// they're logged
367+
_startupLogger.LogInformation(readingFileMessage);
370368
throw new FormatException(string.Format("Unable to parse {0} file.", ScriptConstants.HostMetadataFileName), ex);
371369
}
372370

373371
string sanitizedJson = SanitizeHostJson(hostConfigObject);
374372
string readFileMessage = $"Host configuration file read:{Environment.NewLine}{sanitizedJson}";
375373
TraceWriter.Info(readFileMessage);
376374

377-
try
378-
{
379-
ApplyConfiguration(hostConfigObject, ScriptConfig);
380-
}
381-
catch (Exception)
382-
{
383-
// If we have an error applying the configuration (for example, a value is invalid),
384-
// make sure we have a default LoggerFactory so that the error log can be written.
385-
ConfigureDefaultLoggerFactory();
386-
throw;
387-
}
375+
ApplyConfiguration(hostConfigObject, ScriptConfig);
376+
377+
// now the configuration has been read and applied re-create the logger
378+
// factory and loggers ensuring that filters and settings have been applied
379+
ConfigureLoggerFactory(recreate: true);
380+
_startupLogger = _hostConfig.LoggerFactory.CreateLogger(LogCategories.Startup);
381+
Logger = _hostConfig.LoggerFactory.CreateLogger(ScriptConstants.LogCategoryHostGeneral);
388382

389383
// Allow tests to modify anything initialized by host.json
390384
ScriptConfig.OnConfigurationApplied?.Invoke(ScriptConfig);
391385

392-
ConfigureDefaultLoggerFactory();
393-
394-
// Use the startupLogger in this class as it is concerned with startup. The public Logger is used
395-
// for all other logging after startup.
396-
_startupLogger = _hostConfig.LoggerFactory.CreateLogger(LogCategories.Startup);
397-
Logger = _hostConfig.LoggerFactory.CreateLogger(ScriptConstants.LogCategoryHostGeneral);
386+
// only after configuration has been applied and loggers have been created, raise the initializing event
387+
HostInitializing?.Invoke(this, EventArgs.Empty);
398388

399389
// Do not log these until after all the configuration is done so the proper filters are applied.
400390
_startupLogger.LogInformation(readingFileMessage);
@@ -645,8 +635,21 @@ private IMetricsLogger CreateMetricsLogger()
645635
return metricsLogger;
646636
}
647637

648-
private void ConfigureDefaultLoggerFactory()
638+
private void ConfigureLoggerFactory(bool recreate = false)
649639
{
640+
// Ensure we always have an ILoggerFactory,
641+
// regardless of whether AppInsights is registered or not
642+
if (recreate || _hostConfig.LoggerFactory == null)
643+
{
644+
_hostConfig.LoggerFactory = new LoggerFactory();
645+
646+
// If we've created the LoggerFactory, then we are responsible for
647+
// disposing. Store this locally for disposal later. We can't rely
648+
// on accessing this directly from ScriptConfig.HostConfig as the
649+
// ScriptConfig is re-used for every host.
650+
_loggerFactory = _hostConfig.LoggerFactory;
651+
}
652+
650653
ConfigureLoggerFactory(ScriptConfig, FunctionTraceWriterFactory, _settingsManager, () => FileLoggingEnabled);
651654
}
652655

@@ -801,28 +804,6 @@ private void PurgeOldLogDirectories()
801804
}
802805
}
803806

804-
public static ScriptHost Create(IScriptHostEnvironment environment, IScriptEventManager eventManager,
805-
ScriptHostConfiguration scriptConfig = null, ScriptSettingsManager settingsManager = null, ProxyClientExecutor proxyClient = null)
806-
{
807-
ScriptHost scriptHost = new ScriptHost(environment, eventManager, scriptConfig, settingsManager, proxyClient);
808-
try
809-
{
810-
scriptHost.Initialize();
811-
}
812-
catch (Exception ex)
813-
{
814-
string errorMsg = "ScriptHost initialization failed";
815-
scriptHost.TraceWriter?.Error(errorMsg, ex);
816-
817-
ILogger logger = scriptConfig?.HostConfig?.LoggerFactory?.CreateLogger(LogCategories.Startup);
818-
logger?.LogError(0, ex, errorMsg);
819-
820-
throw;
821-
}
822-
823-
return scriptHost;
824-
}
825-
826807
private static Collection<ScriptBindingProvider> LoadBindingProviders(ScriptHostConfiguration config, JObject hostMetadata, TraceWriter traceWriter, ILogger logger, IEnumerable<string> usedBindingTypes)
827808
{
828809
JobHostConfiguration hostConfig = config.HostConfig;
@@ -1401,12 +1382,6 @@ internal static void ApplyConfiguration(JObject config, ScriptHostConfiguration
14011382
}
14021383
}
14031384

1404-
JToken hostHealthMonitorEnabled = (JToken)config["hostHealthMonitorEnabled"];
1405-
if (hostHealthMonitorEnabled != null && hostHealthMonitorEnabled.Type == JTokenType.Boolean)
1406-
{
1407-
scriptConfig.HostHealthMonitorEnabled = (bool)hostHealthMonitorEnabled;
1408-
}
1409-
14101385
// Apply Singleton configuration
14111386
JObject configSection = (JObject)config["singleton"];
14121387
JToken value = null;
@@ -1434,6 +1409,33 @@ internal static void ApplyConfiguration(JObject config, ScriptHostConfiguration
14341409
}
14351410
}
14361411

1412+
// Apply Host Health Montitor configuration
1413+
configSection = (JObject)config["healthMonitor"];
1414+
value = null;
1415+
if (configSection != null)
1416+
{
1417+
if (configSection.TryGetValue("enabled", out value) && value.Type == JTokenType.Boolean)
1418+
{
1419+
scriptConfig.HostHealthMonitor.Enabled = (bool)value;
1420+
}
1421+
if (configSection.TryGetValue("healthCheckInterval", out value))
1422+
{
1423+
scriptConfig.HostHealthMonitor.HealthCheckInterval = TimeSpan.Parse((string)value, CultureInfo.InvariantCulture);
1424+
}
1425+
if (configSection.TryGetValue("healthCheckWindow", out value))
1426+
{
1427+
scriptConfig.HostHealthMonitor.HealthCheckWindow = TimeSpan.Parse((string)value, CultureInfo.InvariantCulture);
1428+
}
1429+
if (configSection.TryGetValue("healthCheckThreshold", out value))
1430+
{
1431+
scriptConfig.HostHealthMonitor.HealthCheckThreshold = (int)value;
1432+
}
1433+
if (configSection.TryGetValue("counterThreshold", out value))
1434+
{
1435+
scriptConfig.HostHealthMonitor.CounterThreshold = (float)value;
1436+
}
1437+
}
1438+
14371439
// Apply Tracing/Logging configuration
14381440
configSection = (JObject)config["tracing"];
14391441
if (configSection != null)

src/WebJobs.Script/Host/ScriptHostFactory.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ public sealed class ScriptHostFactory : IScriptHostFactory
1010
{
1111
public ScriptHost Create(IScriptHostEnvironment environment, IScriptEventManager eventManager, ScriptSettingsManager settingsManager, ScriptHostConfiguration config)
1212
{
13-
return ScriptHost.Create(environment, eventManager, config, settingsManager);
13+
return new ScriptHost(environment, eventManager, config, settingsManager);
1414
}
1515
}
1616
}

0 commit comments

Comments
 (0)