Skip to content

add diagnostics warning log for deprecating extension bundles #11075

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

Draft
wants to merge 17 commits into
base: dev
Choose a base branch
from
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
19 changes: 10 additions & 9 deletions release_notes.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
### Release notes

<!-- Please add your release notes in the following format:
- My change description (#PR)
-->
- Memory allocation optimizations in `ScriptStartupTypeLocator.GetExtensionsStartupTypesAsync` (#11012)
- Fix invocation timeout when incoming request contains "x-ms-invocation-id" header (#10980)
- Warn if .azurefunctions folder does not exist (#10967)
- Memory allocation & CPU optimizations in `GrpcMessageExtensionUtilities.ConvertFromHttpMessageToExpando` (#11054)
### Release notes

<!-- Please add your release notes in the following format:
- My change description (#PR)
-->
- Memory allocation optimizations in `ScriptStartupTypeLocator.GetExtensionsStartupTypesAsync` (#11012)
- Fix invocation timeout when incoming request contains "x-ms-invocation-id" header (#10980)
- Warn if .azurefunctions folder does not exist (#10967)
- Memory allocation & CPU optimizations in `GrpcMessageExtensionUtilities.ConvertFromHttpMessageToExpando` (#11054)
- Add information diagnostics event for outdated bundle version, any bundle version < 4 (#)
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ param (
$normalizeBitness = @{
'x64' = '64bit'
'64bit' = '64bit'
'x86' = '64bit'
'x86' = '32bit'
'32bit' = '32bit'
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ namespace Microsoft.Azure.WebJobs.Script.DependencyInjection
public sealed class ScriptStartupTypeLocator : IWebJobsStartupTypeLocator
{
private const string ApplicationInsightsStartupType = "Microsoft.Azure.WebJobs.Extensions.ApplicationInsights.ApplicationInsightsWebJobsStartup, Microsoft.Azure.WebJobs.Extensions.ApplicationInsights, Version=1.0.0.0, Culture=neutral, PublicKeyToken=9475d07f10cb09df";

private readonly string _rootScriptPath;
private readonly ILogger _logger;
private readonly IExtensionBundleManager _extensionBundleManager;
Expand Down
3 changes: 3 additions & 0 deletions src/WebJobs.Script/Diagnostics/DiagnosticEventConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,8 @@ internal static class DiagnosticEventConstants

public const string WorkerRuntimeDoesNotMatchWithFunctionMetadataErrorCode = "AZFD0013";
public const string WorkerRuntimeDoesNotMatchWithFunctionMetadataHelpLink = "https://aka.ms/functions-invalid-worker-runtime";

public const string OutdatedBundlesVersionErrorCode = "AZFD0014";
public const string OutdatedBundlesVersionHelpLink = "https://aka.ms/functions-outdated-bundles";
}
}
17 changes: 17 additions & 0 deletions src/WebJobs.Script/ExtensionBundle/ExtensionBundleManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs.Script.Config;
using Microsoft.Azure.WebJobs.Script.Configuration;
using Microsoft.Azure.WebJobs.Script.Diagnostics;
using Microsoft.Azure.WebJobs.Script.Diagnostics.Extensions;
using Microsoft.Azure.WebJobs.Script.Models;
using Microsoft.Azure.WebJobs.Script.Properties;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using NuGet.Versioning;
Expand Down Expand Up @@ -375,5 +377,20 @@ public async Task<string> GetExtensionBundleBinPathAsync()
// if no bin directory is present something is wrong
return FileUtility.DirectoryExists(binPath) ? binPath : null;
}

public void CompareWithLatestMajorVersion()
{
string majorVersionStr = _extensionBundleVersion?.Split('.')?.FirstOrDefault() ?? string.Empty;
int majorVersion = int.TryParse(majorVersionStr, out int result) ? result : 0;

int latestMajorVersion = ScriptConstants.ExtensionBundleV4MajorVersion;
if (string.Compare(_options?.Id, ScriptConstants.DefaultExtensionBundleId, StringComparison.OrdinalIgnoreCase) == 0
&& majorVersion != 0
&& majorVersion < latestMajorVersion)
{
string message = string.Format(Resources.OutdatedExtensionBundlesVersionInfoFormat, _extensionBundleVersion, latestMajorVersion, latestMajorVersion + 1);
DiagnosticEventLoggerExtensions.LogDiagnosticEventWarning(_logger, DiagnosticEventConstants.OutdatedBundlesVersionErrorCode, message, DiagnosticEventConstants.OutdatedBundlesVersionHelpLink, null);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,7 @@ public interface IExtensionBundleManager
bool IsLegacyExtensionBundle();

Task<ExtensionBundleDetails> GetExtensionBundleDetails();

void CompareWithLatestMajorVersion();
}
}
6 changes: 6 additions & 0 deletions src/WebJobs.Script/Host/FunctionAppValidationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs.Script.Diagnostics.Extensions;
using Microsoft.Azure.WebJobs.Script.ExtensionBundle;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
Expand All @@ -22,15 +23,18 @@ internal sealed class FunctionAppValidationService : BackgroundService
private readonly IEnvironment _environment;
private readonly ILogger<FunctionAppValidationService> _logger;
private readonly IOptions<ScriptJobHostOptions> _scriptOptions;
private readonly IExtensionBundleManager _extensionBundleManager;

public FunctionAppValidationService(
ILogger<FunctionAppValidationService> logger,
IOptions<ScriptJobHostOptions> scriptOptions,
IExtensionBundleManager extensionBundleManager,
IEnvironment environment)
{
_scriptOptions = scriptOptions ?? throw new ArgumentNullException(nameof(scriptOptions));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_environment = environment ?? throw new ArgumentNullException(nameof(environment));
_extensionBundleManager = extensionBundleManager ?? throw new ArgumentNullException(nameof(extensionBundleManager));
}

protected override async Task ExecuteAsync(CancellationToken cancellationToken)
Expand All @@ -41,6 +45,8 @@ protected override async Task ExecuteAsync(CancellationToken cancellationToken)
Utility.ExecuteAfterColdStartDelay(_environment, Validate, cancellationToken);
}

// Validate the extension bundle and throw warning for outdated bundles
_extensionBundleManager.CompareWithLatestMajorVersion();
await Task.CompletedTask;
}

Expand Down
9 changes: 9 additions & 0 deletions src/WebJobs.Script/Properties/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/WebJobs.Script/Properties/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -179,4 +179,7 @@
<data name="LinuxConsumptionRemoveTimeZone" xml:space="preserve">
<value>The environment variables 'WEBSITE_TIME_ZONE' and 'TZ' are not supported on this platform. For more information, see https://go.microsoft.com/fwlink/?linkid=2250165.</value>
</data>
<data name="OutdatedExtensionBundlesVersionInfoFormat" xml:space="preserve">
<value>You are currently using an outdated version — {0} — of the extension bundle, which is deprecated as of 2026-05-30. To ensure optimal performance and access to the latest features, please update to a supported version in the range: [{1}.*, {2}.0.0).</value>
</data>
</root>
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
// 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.Immutable;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs.Script.Config;
using Microsoft.Azure.WebJobs.Script.Configuration;
using Microsoft.Azure.WebJobs.Script.Description;
using Microsoft.Azure.WebJobs.Script.ExtensionBundle;
using Microsoft.Azure.WebJobs.Script.Host;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
Expand All @@ -21,8 +23,10 @@ public class FunctionAppValidationServiceTests
{
private readonly ILogger<FunctionAppValidationService> _testLogger;
private readonly Mock<IOptions<ScriptJobHostOptions>> _scriptOptionsMock;
private readonly IExtensionBundleManager _extensionBundleManager;
private readonly ScriptJobHostOptions _scriptJobHostOptions;
private readonly TestLoggerProvider _testLoggerProvider;
private readonly ILoggerFactory _loggerFactory;

public FunctionAppValidationServiceTests()
{
Expand All @@ -36,9 +40,10 @@ public FunctionAppValidationServiceTests()
_scriptOptionsMock.Setup(o => o.Value).Returns(_scriptJobHostOptions);

_testLoggerProvider = new TestLoggerProvider();
var factory = new LoggerFactory();
factory.AddProvider(_testLoggerProvider);
_testLogger = factory.CreateLogger<FunctionAppValidationService>();
_loggerFactory = new LoggerFactory();
_loggerFactory.AddProvider(_testLoggerProvider);
_testLogger = _loggerFactory.CreateLogger<FunctionAppValidationService>();
_extensionBundleManager = new Mock<IExtensionBundleManager>().Object;
}

[Fact]
Expand All @@ -49,6 +54,7 @@ public async Task StartAsync_NotDotnetIsolatedApp_DoesNotLogError()
var service = new FunctionAppValidationService(
_testLogger,
_scriptOptionsMock.Object,
_extensionBundleManager,
new TestEnvironment());

// Act
Expand All @@ -72,6 +78,7 @@ public async Task StartAsync_PlaceholderMode_DoesNotLogError()
var service = new FunctionAppValidationService(
_testLogger,
_scriptOptionsMock.Object,
_extensionBundleManager,
environment);

// Act
Expand Down Expand Up @@ -105,6 +112,7 @@ public async Task StartAsync_NewAppWithNoPayload_DoesNotLogError()
var service = new FunctionAppValidationService(
_testLogger,
scriptOptionsMock.Object,
_extensionBundleManager,
environment);

// Act
Expand Down Expand Up @@ -136,6 +144,7 @@ public async Task StartAsync_MissingAzureFunctionsFolder_LogsWarning()
var service = new FunctionAppValidationService(
_testLogger,
_scriptOptionsMock.Object,
_extensionBundleManager,
environment);

// Act
Expand All @@ -147,5 +156,38 @@ await TestHelpers.Await(() =>
return completed > 0;
});
}

[Theory]
[InlineData("Microsoft.Azure.Functions.ExtensionBundle", "3.36.0", true)]
[InlineData("Microsoft.Azure.Functions.ExtensionBundle", "2.25.0", true)]
[InlineData("Microsoft.Azure.Functions.ExtensionBundle", "4.22.0", false)]
[InlineData("Microsoft.Azure.Functions.ExtensionBundle.Preview", "4.29.0", false)]
[InlineData("Microsoft.Azure.Functions.ExtensionBundle.Preview", "3.2.0", false)]
public void CompareWithLatestMajorVersion_LogsExpectedDiagnosticEvents(string bundleId, string bundleVersion, bool shouldLogEvent)
{
// Arrange
_testLoggerProvider.ClearAllLogMessages();

var options = new ExtensionBundleOptions { Id = bundleId };
var env = new TestEnvironment();
var config = new FunctionsHostingConfigOptions();
var manager = new ExtensionBundleManager(options, env, _loggerFactory, config);

// Set the private _extensionBundleVersion field using reflection
typeof(ExtensionBundleManager)
.GetField("_extensionBundleVersion", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
.SetValue(manager, bundleVersion);

// Act
manager.CompareWithLatestMajorVersion();

// Assert
var logMessages = _testLoggerProvider.GetAllLogMessages();
bool hasOutdatedBundleLog = logMessages.Any(m => m.FormattedMessage.Contains(bundleVersion) &&
m.FormattedMessage.Contains("outdated version") &&
m.FormattedMessage.Contains("of the extension bundle"));

Assert.Equal(shouldLogEvent, hasOutdatedBundleLog);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,10 @@ public TestExtensionBundleManager(string bundlePath = null, bool isExtensionBund
_isExtensionBundleConfigured = isExtensionBundleConfigured;
_isLegacyExtensionBundle = isLegacyExtensionBundle;
}
public void CompareWithLatestMajorVersion()
{
// No-op for test stub. This can be extended for test verifications if needed.
}

public Task<string> GetExtensionBundleBinPathAsync()
{
Expand Down
5 changes: 5 additions & 0 deletions test/WebJobs.Script.Tests/ExtensionManagerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,11 @@ public TestExtensionBundleManager(string bundlePath = null, bool isExtensionBund
_isLegacyExtensionBundle = isLegacyExtensionBundle;
}

public void CompareWithLatestMajorVersion()
{
// No-op for test stub. This can be extended for test verifications if needed.
}

public Task<string> GetExtensionBundleBinPathAsync()
{
return Task.FromResult(Path.Combine(_bundlePath, "bin"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -780,7 +780,7 @@ public async Task GetExtensionsStartupTypes_WorkerRuntimeNotSetForNodeApp_LoadsE

//Assert
var traces = testLoggerProvider.GetAllLogMessages();
var traceMessage = traces.FirstOrDefault(val => val.EventId.Name.Equals("ScriptStartNotLoadingExtensionBundle"));
var traceMessage = traces.FirstOrDefault(val => string.Equals(val.EventId.Name, "ScriptStartNotLoadingExtensionBundle"));
bool loadingExtensionBundle = traceMessage == null;

Assert.True(loadingExtensionBundle);
Expand Down