Skip to content

Commit c7192ec

Browse files
authored
add warning log for end of support extension bundles versions (#11075)
* compare loaded bundle version with latest major bundle version * update message * use resx * update message * update bitness for x86 * update the message * move to scriptstartup * add test cases * remove new line * handle null check * move the logic to extension bundle manager * change to warning * move to function app validation service * revert some changes * make validator extensible * fix the test case * Update resource * update the message as per PM review * change the date format * update tests * code review comments + remove diagnostic event * remove extra space in tests * change to warning logs * peformant logging and updated date * update the date to Aug 4, 2026 * remove resx files * register validators always * review comment * review comments * use LogCategoryHostGeneral * add null check
1 parent 649c38e commit c7192ec

16 files changed

+277
-57
lines changed

release_notes.md

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
### Release notes
2-
3-
<!-- Please add your release notes in the following format:
4-
- My change description (#PR)
5-
-->
6-
- Adding activity sources for Durable and WebJobs (Kafka and RabbitMQ) (#11137)
7-
- Add JitTrace Files for v4.1041
8-
- Fix startup deadlock on transient exceptions (#11142)
1+
### Release notes
2+
3+
<!-- Please add your release notes in the following format:
4+
- My change description (#PR)
5+
-->
6+
- Adding activity sources for Durable and WebJobs (Kafka and RabbitMQ) (#11137)
7+
- Add JitTrace Files for v4.1041
8+
- Fix startup deadlock on transient exceptions (#11142)
9+
- Add warning log for end of support bundle version, any bundle version < 4 (#11075)

src/WebJobs.Script.SiteExtension/New-PrivateSiteExtension.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ param (
4242
$normalizeBitness = @{
4343
'x64' = '64bit'
4444
'64bit' = '64bit'
45-
'x86' = '64bit'
45+
'x86' = '32bit'
4646
'32bit' = '32bit'
4747
}
4848

src/WebJobs.Script/Diagnostics/Extensions/LoggerExtension.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,16 @@ internal static class LoggerExtension
206206
private static readonly Action<ILogger, string, Exception> _publishingMetrics =
207207
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(338, nameof(PublishingMetrics)), "{metrics}");
208208

209+
private static readonly Action<ILogger, string, int, int, Exception> _outdatedExtensionBundleFuture =
210+
LoggerMessage.Define<string, int, int>(LogLevel.Warning,
211+
new EventId(342, nameof(OutdatedExtensionBundle)),
212+
"Your current bundle version {currentVersion} will reach end of support on Aug 4, 2026. Upgrade to [{suggestedMinVersion}.*, {suggestedMaxVersion}.0.0). For more information, see https://aka.ms/functions-outdated-bundles");
213+
214+
private static readonly Action<ILogger, string, int, int, Exception> _outdatedExtensionBundlePast =
215+
LoggerMessage.Define<string, int, int>(LogLevel.Warning,
216+
new EventId(342, nameof(OutdatedExtensionBundle)),
217+
"Your current bundle version {currentVersion} has reached end of support on Aug 4, 2026. Upgrade to [{suggestedMinVersion}.*, {suggestedMaxVersion}.0.0). For more information, see https://aka.ms/functions-outdated-bundles");
218+
209219
public static void PublishingMetrics(this ILogger logger, string metrics)
210220
{
211221
_publishingMetrics(logger, metrics, null);
@@ -407,5 +417,20 @@ public static void IncorrectAzureFunctionsFolderPath(this ILogger logger, string
407417
{
408418
_incorrectAzureFunctionsFolderPath(logger, path, EnvironmentSettingNames.FunctionWorkerRuntime, null);
409419
}
420+
421+
public static void OutdatedExtensionBundle(this ILogger logger, string currentVersion, int suggestedMinVersion, int suggestedMaxVersion)
422+
{
423+
var currentTime = DateTime.UtcNow;
424+
var deprecationDate = new DateTime(2026, 8, 5, 0, 0, 0, DateTimeKind.Utc);
425+
426+
if (currentTime >= deprecationDate)
427+
{
428+
_outdatedExtensionBundlePast(logger, currentVersion, suggestedMinVersion, suggestedMaxVersion, null);
429+
}
430+
else
431+
{
432+
_outdatedExtensionBundleFuture(logger, currentVersion, suggestedMinVersion, suggestedMaxVersion, null);
433+
}
434+
}
410435
}
411436
}

src/WebJobs.Script/ExtensionBundle/ExtensionBundleManager.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@
1010
using System.Threading.Tasks;
1111
using Microsoft.Azure.WebJobs.Script.Config;
1212
using Microsoft.Azure.WebJobs.Script.Configuration;
13+
using Microsoft.Azure.WebJobs.Script.Diagnostics;
1314
using Microsoft.Azure.WebJobs.Script.Diagnostics.Extensions;
1415
using Microsoft.Azure.WebJobs.Script.Models;
16+
using Microsoft.Azure.WebJobs.Script.Properties;
1517
using Microsoft.Extensions.Logging;
1618
using Newtonsoft.Json;
1719
using NuGet.Versioning;
@@ -375,5 +377,33 @@ public async Task<string> GetExtensionBundleBinPathAsync()
375377
// if no bin directory is present something is wrong
376378
return FileUtility.DirectoryExists(binPath) ? binPath : null;
377379
}
380+
381+
public string GetOutdatedBundleVersion()
382+
{
383+
// If the extension bundle version is not set or if the extension bundle is not the default one,
384+
// return empty string
385+
if (string.IsNullOrEmpty(_extensionBundleVersion) ||
386+
!string.Equals(_options?.Id, ScriptConstants.DefaultExtensionBundleId, StringComparison.OrdinalIgnoreCase))
387+
{
388+
return null;
389+
}
390+
391+
// Extract the major version number from the version string
392+
int dotIndex = _extensionBundleVersion.IndexOf('.');
393+
if (dotIndex <= 0 || !int.TryParse(_extensionBundleVersion.AsSpan(0, dotIndex), out var majorVersion) || majorVersion == 0)
394+
{
395+
return null;
396+
}
397+
398+
int latestMajorVersion = ScriptConstants.ExtensionBundleV4MajorVersion;
399+
400+
// Return the version if it's outdated
401+
if (majorVersion < latestMajorVersion)
402+
{
403+
return _extensionBundleVersion;
404+
}
405+
406+
return null;
407+
}
378408
}
379409
}

src/WebJobs.Script/ExtensionBundle/IExtensionBundleManager.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,7 @@ public interface IExtensionBundleManager
2020
bool IsLegacyExtensionBundle();
2121

2222
Task<ExtensionBundleDetails> GetExtensionBundleDetails();
23+
24+
string GetOutdatedBundleVersion();
2325
}
2426
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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 Microsoft.Azure.WebJobs.Script.Diagnostics.Extensions;
5+
using Microsoft.Azure.WebJobs.Script.ExtensionBundle;
6+
using Microsoft.Extensions.Logging;
7+
8+
namespace Microsoft.Azure.WebJobs.Script.Host
9+
{
10+
internal sealed class ExtensionBundleManagerValidator : IFunctionAppValidator
11+
{
12+
private readonly IExtensionBundleManager _extensionBundleManager;
13+
14+
public ExtensionBundleManagerValidator(IExtensionBundleManager extensionBundleManager)
15+
{
16+
_extensionBundleManager = extensionBundleManager;
17+
}
18+
19+
public void Validate(ScriptJobHostOptions options, IEnvironment environment, ILogger logger)
20+
{
21+
if (!logger.IsEnabled(LogLevel.Warning))
22+
{
23+
return;
24+
}
25+
26+
string outdatedBundleVersion = _extensionBundleManager.GetOutdatedBundleVersion();
27+
if (!string.IsNullOrEmpty(outdatedBundleVersion))
28+
{
29+
int latestMajorVersion = ScriptConstants.ExtensionBundleV4MajorVersion;
30+
logger.OutdatedExtensionBundle(outdatedBundleVersion, latestMajorVersion, latestMajorVersion + 1);
31+
}
32+
}
33+
}
34+
}

src/WebJobs.Script/Host/FunctionAppValidationService.cs

Lines changed: 14 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,8 @@
33

44
using System;
55
using System.Collections.Generic;
6-
using System.IO;
7-
using System.Linq;
86
using System.Threading;
97
using System.Threading.Tasks;
10-
using Microsoft.Azure.WebJobs.Script.Diagnostics.Extensions;
118
using Microsoft.Extensions.Hosting;
129
using Microsoft.Extensions.Logging;
1310
using Microsoft.Extensions.Options;
@@ -20,17 +17,20 @@ namespace Microsoft.Azure.WebJobs.Script.Host
2017
internal sealed class FunctionAppValidationService : BackgroundService
2118
{
2219
private readonly IEnvironment _environment;
23-
private readonly ILogger<FunctionAppValidationService> _logger;
20+
private readonly ILogger _logger;
2421
private readonly IOptions<ScriptJobHostOptions> _scriptOptions;
22+
private readonly IEnumerable<IFunctionAppValidator> _validators;
2523

2624
public FunctionAppValidationService(
27-
ILogger<FunctionAppValidationService> logger,
25+
ILoggerFactory loggerFactory,
2826
IOptions<ScriptJobHostOptions> scriptOptions,
29-
IEnvironment environment)
27+
IEnvironment environment,
28+
IEnumerable<IFunctionAppValidator> validators)
3029
{
3130
_scriptOptions = scriptOptions ?? throw new ArgumentNullException(nameof(scriptOptions));
32-
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
31+
_logger = loggerFactory?.CreateLogger(ScriptConstants.LogCategoryHostGeneral) ?? throw new ArgumentNullException(nameof(loggerFactory));
3332
_environment = environment ?? throw new ArgumentNullException(nameof(environment));
33+
_validators = validators ?? throw new ArgumentNullException(nameof(validators));
3434
}
3535

3636
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
@@ -40,39 +40,21 @@ protected override async Task ExecuteAsync(CancellationToken cancellationToken)
4040
// Adding a delay to ensure that this validation does not impact the cold start performance
4141
Utility.ExecuteAfterColdStartDelay(_environment, Validate, cancellationToken);
4242
}
43-
4443
await Task.CompletedTask;
4544
}
4645

4746
private void Validate()
4847
{
49-
try
48+
foreach (var validator in _validators)
5049
{
51-
string azureFunctionsDirPath = Path.Combine(_scriptOptions.Value.RootScriptPath, ScriptConstants.AzureFunctionsSystemDirectoryName);
52-
53-
if (_scriptOptions.Value.RootScriptPath is not null &&
54-
!_scriptOptions.Value.IsDefaultHostConfig &&
55-
Utility.IsDotnetIsolatedApp(environment: _environment) &&
56-
!Directory.Exists(azureFunctionsDirPath))
50+
try
5751
{
58-
// Search for the .azurefunctions directory within nested directories to verify scenarios where it isn't located at the root. This situation occurs when a function app has been improperly zipped.
59-
IEnumerable<string> azureFunctionsDirectories = Directory.GetDirectories(_scriptOptions.Value.RootScriptPath, ScriptConstants.AzureFunctionsSystemDirectoryName, SearchOption.AllDirectories)
60-
.Where(dir => !dir.Equals(azureFunctionsDirPath, StringComparison.OrdinalIgnoreCase));
61-
62-
if (azureFunctionsDirectories.Any())
63-
{
64-
string azureFunctionsDirectoriesPath = string.Join(", ", azureFunctionsDirectories).Replace(_scriptOptions.Value.RootScriptPath, string.Empty);
65-
_logger.IncorrectAzureFunctionsFolderPath(azureFunctionsDirectoriesPath);
66-
}
67-
else
68-
{
69-
_logger.MissingAzureFunctionsFolder();
70-
}
52+
validator.Validate(_scriptOptions.Value, _environment, _logger);
53+
}
54+
catch (Exception ex)
55+
{
56+
_logger.LogTrace(ex, "Validator {ValidatorType} failed", validator.GetType().Name);
7157
}
72-
}
73-
catch (Exception ex)
74-
{
75-
_logger.LogTrace("Unable to validate deployed function app payload", ex);
7658
}
7759
}
7860
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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 Microsoft.Extensions.Logging;
5+
6+
namespace Microsoft.Azure.WebJobs.Script.Host
7+
{
8+
/// <summary>
9+
/// Defines a validator interface for Function App validation.
10+
/// </summary>
11+
internal interface IFunctionAppValidator
12+
{
13+
/// <summary>
14+
/// Validates aspects of the function app and reports any issues through the provided logger.
15+
/// </summary>
16+
/// <param name="options">The script host options containing function app configuration.</param>
17+
/// <param name="environment">The environment in which the function app is running.</param>
18+
/// <param name="logger">The logger to report validation issues.</param>
19+
void Validate(ScriptJobHostOptions options, IEnvironment environment, ILogger logger);
20+
}
21+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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+
using System.Collections.Generic;
6+
using System.IO;
7+
using System.Linq;
8+
using Microsoft.Azure.WebJobs.Script.Diagnostics.Extensions;
9+
using Microsoft.Extensions.Logging;
10+
11+
namespace Microsoft.Azure.WebJobs.Script.Host
12+
{
13+
internal sealed class MissingAzureFunctionsFolderValidator : IFunctionAppValidator
14+
{
15+
public void Validate(ScriptJobHostOptions options, IEnvironment environment, ILogger logger)
16+
{
17+
string azureFunctionsDirPath = Path.Combine(options.RootScriptPath, ScriptConstants.AzureFunctionsSystemDirectoryName);
18+
19+
if (options.RootScriptPath is not null &&
20+
!options.IsDefaultHostConfig &&
21+
Utility.IsDotnetIsolatedApp(environment: environment) &&
22+
!Directory.Exists(azureFunctionsDirPath))
23+
{
24+
IEnumerable<string> azureFunctionsDirectories = Directory.GetDirectories(options.RootScriptPath, ScriptConstants.AzureFunctionsSystemDirectoryName, SearchOption.AllDirectories)
25+
.Where(dir => !dir.Equals(azureFunctionsDirPath, StringComparison.OrdinalIgnoreCase));
26+
27+
if (azureFunctionsDirectories.Any())
28+
{
29+
string azureFunctionsDirectoriesPath = string.Join(", ", azureFunctionsDirectories).Replace(options.RootScriptPath, string.Empty);
30+
logger.IncorrectAzureFunctionsFolderPath(azureFunctionsDirectoriesPath);
31+
}
32+
else
33+
{
34+
logger.MissingAzureFunctionsFolder();
35+
}
36+
}
37+
}
38+
}
39+
}

src/WebJobs.Script/ScriptHostBuilderExtensions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,11 @@ public static IHostBuilder AddScriptHostCore(this IHostBuilder builder, ScriptAp
172172
{
173173
if (!SystemEnvironment.Instance.IsPlaceholderModeEnabled())
174174
{
175+
// Register all validators here
176+
services.AddSingleton<IFunctionAppValidator, ExtensionBundleManagerValidator>();
177+
services.AddSingleton<IFunctionAppValidator, MissingAzureFunctionsFolderValidator>();
178+
179+
// Add more validators as needed
175180
services.AddHostedService<FunctionAppValidationService>();
176181
}
177182

0 commit comments

Comments
 (0)