Skip to content

Warn if .azurefunctions folder does not exist #10967

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

Merged
merged 44 commits into from
May 15, 2025
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
335a2d7
Initial changes
surgupta-msft Mar 26, 2025
151b54c
Adding comment
surgupta-msft Mar 26, 2025
3df08a5
Improving log and adding test
surgupta-msft Apr 1, 2025
dbb9557
cleanup utility
surgupta-msft Apr 1, 2025
2ad8032
build fix
surgupta-msft Apr 1, 2025
1815d68
Method cleanup
surgupta-msft Apr 1, 2025
7b042ba
minor fix
surgupta-msft Apr 2, 2025
12db36b
updating release notes
surgupta-msft Apr 2, 2025
4693c8d
Merge with main
surgupta-msft Apr 2, 2025
80e2bd8
Addresisng PR feedback
surgupta-msft Apr 3, 2025
cd0441f
Fixing tests
surgupta-msft Apr 4, 2025
d4bb19c
Tests fixing
surgupta-msft Apr 4, 2025
3afb581
Tests
surgupta-msft Apr 4, 2025
fcc57b7
Tests fixing
surgupta-msft Apr 4, 2025
0375667
minor
surgupta-msft Apr 4, 2025
63d5e1e
Adding check log avoid logging in placeholder mode
surgupta-msft Apr 4, 2025
958da5a
updating if check
surgupta-msft Apr 10, 2025
3d334a2
Merge branch 'dev' into surgupta/innerbuild-warn
surgupta-msft Apr 10, 2025
a00805e
Test project build fix
surgupta-msft Apr 15, 2025
6c98fb7
Fixing eventId
surgupta-msft Apr 15, 2025
97d4db1
Merge with dev
surgupta-msft Apr 30, 2025
f8c9b97
Adding hosting service to validate function app
surgupta-msft Apr 30, 2025
fbe2b87
release notes fix
surgupta-msft Apr 30, 2025
ed21374
Adding tests
surgupta-msft Apr 30, 2025
f3674e3
Adding additional tests
surgupta-msft Apr 30, 2025
6cadba0
PR cleanup
surgupta-msft Apr 30, 2025
eca58e4
Addressing PR feedback
surgupta-msft May 3, 2025
88f0aed
Rebase with main
surgupta-msft May 4, 2025
780b151
Adding e2e test
surgupta-msft May 3, 2025
f64b801
Updating E2E test
surgupta-msft May 5, 2025
1374a3c
Merge with main
surgupta-msft May 6, 2025
d152e72
Adding placeholder check before creating timer
surgupta-msft May 6, 2025
0bfc12d
Conditionally register the validation service
surgupta-msft May 6, 2025
108d1a7
Improving logging message based on PR feedback
surgupta-msft May 7, 2025
a4facb2
Fixing tests
surgupta-msft May 7, 2025
824fce5
Added logic to check valid zip
surgupta-msft May 13, 2025
63f8cd1
Fixing tests
surgupta-msft May 13, 2025
fb16548
Rebasing with dev
surgupta-msft May 14, 2025
10d1ea2
Tests cleanup
surgupta-msft May 14, 2025
50d995f
Addressed feedback on a test app
surgupta-msft May 14, 2025
4834166
Clean up in tests
surgupta-msft May 14, 2025
2369091
Minor cleanup
surgupta-msft May 15, 2025
17986cd
Class visibility update
surgupta-msft May 15, 2025
ff21cb3
Including exception in the trace log
surgupta-msft May 15, 2025
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
31 changes: 16 additions & 15 deletions release_notes.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
### Release notes

<!-- Please add your release notes in the following format:
- My change description (#PR)
-->
- Improved memory metrics reporting using CGroup data for Linux consumption (#10968)
- Memory allocation optimizations in `RpcWorkerConfigFactory.AddProviders` (#10959)
- Fixing GrpcWorkerChannel concurrency bug (#10998)
- Avoid circular dependency when resolving LinuxContainerLegionMetricsPublisher. (#10991)
- Add 'unix' to the list of runtimes kept when importing PowerShell worker for Linux builds
- Update PowerShell 7.4 worker to 4.0.4206
- Update Python Worker Version to [4.37.0](https://github.com/Azure/azure-functions-python-worker/releases/tag/4.37.0)
- Add runtime and process metrics. (#11034)
- Add `win-arm64` and `linux-arm64` to the list of PowerShell runtimes; added filter for `osx` RIDs (includes `osx-x64` and `osx-arm64`) (#11013)
- Disable Diagnostic Events when Table Storage is not accessible (#10996)
### Release notes

<!-- Please add your release notes in the following format:
- My change description (#PR)
-->
- Improved memory metrics reporting using CGroup data for Linux consumption (#10968)
- Memory allocation optimizations in `RpcWorkerConfigFactory.AddProviders` (#10959)
- Fixing GrpcWorkerChannel concurrency bug (#10998)
- Avoid circular dependency when resolving LinuxContainerLegionMetricsPublisher. (#10991)
- Add 'unix' to the list of runtimes kept when importing PowerShell worker for Linux builds
- Update PowerShell 7.4 worker to 4.0.4206
- Update Python Worker Version to [4.37.0](https://github.com/Azure/azure-functions-python-worker/releases/tag/4.37.0)
- Add runtime and process metrics. (#11034)
- Add `win-arm64` and `linux-arm64` to the list of PowerShell runtimes; added filter for `osx` RIDs (includes `osx-x64` and `osx-arm64`) (#11013)
- Disable Diagnostic Events when Table Storage is not accessible (#10996)
- Warn if .azurefunctions folder does not exist (#10967)
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Microsoft.Azure.WebJobs.Script.Diagnostics;
using Microsoft.Azure.WebJobs.Script.Eventing;
using Microsoft.Azure.WebJobs.Script.FileProvisioning;
using Microsoft.Azure.WebJobs.Script.Host;
using Microsoft.Azure.WebJobs.Script.Scale;
using Microsoft.Azure.WebJobs.Script.WebHost.Diagnostics;
using Microsoft.Azure.WebJobs.Script.Workers;
Expand Down Expand Up @@ -48,6 +49,7 @@ private static ExpectedDependencyBuilder CreateExpectedDependencies()
.Expect<WorkerConsoleLogService>()
.Expect<FunctionInvocationDispatcherShutdownManager>()
.Expect<WorkerConcurrencyManager>()
.Expect<FunctionAppValidationService>()
.Optional<FuncAppFileProvisioningService>() // Used by powershell.
.Optional<JobHostService>() // Missing when host is offline.
.Optional<FunctionsSyncService>() // Conditionally registered.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ public async Task<IEnumerable<Type>> GetExtensionsStartupTypesAsync()
}
}

bool isDotnetIsolatedApp = IsDotnetIsolatedApp(functionMetadataCollection, SystemEnvironment.Instance);
bool isDotnetIsolatedApp = Utility.IsDotnetIsolatedApp(SystemEnvironment.Instance, functionMetadataCollection);
bool isDotnetApp = isPrecompiledFunctionApp || isDotnetIsolatedApp;
var isLogicApp = SystemEnvironment.Instance.IsLogicApp();

Expand Down Expand Up @@ -340,12 +340,6 @@ void CollectError(Type extensionType, Version minimumVersion, ExtensionStartupTy
}
}

private bool IsDotnetIsolatedApp(IEnumerable<FunctionMetadata> functions, IEnvironment environment)
{
string workerRuntime = Utility.GetWorkerRuntime(functions, environment);
return workerRuntime?.Equals(RpcWorkerConstants.DotNetIsolatedLanguageWorkerName, StringComparison.OrdinalIgnoreCase) ?? false;
}

private ExtensionRequirementsInfo GetExtensionRequirementsInfo()
{
ExtensionRequirementsInfo requirementsInfo = _extensionRequirementOptions.Value.Bundles != null || _extensionRequirementOptions.Value.Extensions != null
Expand Down
22 changes: 16 additions & 6 deletions src/WebJobs.Script/Diagnostics/Extensions/LoggerExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -184,14 +184,19 @@ internal static class LoggerExtension
"Referenced bundle {bundleId} of version {bundleVersion} does not meet the required minimum version of {minimumVersion}. Update your extension bundle reference in host.json to reference {minimumVersion2} or later.");

private static readonly Action<ILogger, string, Exception> _hostJsonZipDeploymentIssue =
LoggerMessage.Define<string>(LogLevel.Error,
new EventId(338, nameof(HostJsonZipDeploymentIssue)),
"No functions were found. A valid host.json file wasn't found in the package root. However, one was located at: {hostJsonFilesPath}. This state indicates that your deployment package was created incorrectly. For deployment package requirements, see https://aka.ms/deployment-zip-push.");
LoggerMessage.Define<string>(LogLevel.Error,
new EventId(338, nameof(HostJsonZipDeploymentIssue)),
"No functions were found. A valid host.json file wasn't found in the package root. However, one was located at: {hostJsonFilesPath}. This state indicates that your deployment package was created incorrectly. For deployment package requirements, see https://aka.ms/deployment-zip-push.");

private static readonly Action<ILogger, Exception> _noHostJsonFile =
LoggerMessage.Define(LogLevel.Information,
new EventId(339, nameof(NoHostJsonFile)),
"No functions were found. This can occur before you deploy code to your function app or when the host.json file is missing from the most recent deployment. Make sure that your deployment package includes the host.json file in the root of the package. For deployment package requirements, see https://aka.ms/functions-deployment-technologies.");
LoggerMessage.Define(LogLevel.Information,
new EventId(339, nameof(NoHostJsonFile)),
"No functions were found. This can occur before you deploy code to your function app or when the host.json file is missing from the most recent deployment. Make sure that your deployment package includes the host.json file in the root of the package. For deployment package requirements, see https://aka.ms/functions-deployment-technologies.");

private static readonly Action<ILogger, Exception> _noAzureFunctionsFolder =
LoggerMessage.Define(LogLevel.Warning,
new EventId(340, nameof(NoAzureFunctionsFolder)),
"Could not find the .azurefunctions folder in the deployed function app artifacts. Make sure that your deployment package includes the .azurefunctions folder. For deployment package requirements, see https://aka.ms/functions-deployment-technologies.");

private static readonly Action<ILogger, string, Exception> _publishingMetrics =
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(338, nameof(PublishingMetrics)), "{metrics}");
Expand Down Expand Up @@ -387,5 +392,10 @@ public static void NoHostJsonFile(this ILogger logger)
{
_noHostJsonFile(logger, null);
}

public static void NoAzureFunctionsFolder(this ILogger logger)
{
_noAzureFunctionsFolder(logger, null);
}
}
}
56 changes: 56 additions & 0 deletions src/WebJobs.Script/Host/FunctionAppValidationService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// 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.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs.Script.Diagnostics.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Microsoft.Azure.WebJobs.Script.Host
{
public sealed class FunctionAppValidationService : IHostedService
{
private readonly IEnvironment _environment;
private readonly ILogger<FunctionAppValidationService> _logger;
private readonly IOptions<ScriptJobHostOptions> _scriptOptions;
private readonly TimeSpan _initializationDelay = TimeSpan.FromSeconds(10);

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

public async Task StartAsync(CancellationToken cancellationToken)
{
// Adding a delay to ensure that this validation does not impact the cold start performance
_ = Task.Delay(_initializationDelay, cancellationToken).ContinueWith(t => Validate());

await Task.CompletedTask;
}

public Task StopAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}

internal void Validate()
{
if (!_environment.IsPlaceholderModeEnabled() &&
!_scriptOptions.Value.IsDefaultHostConfig &&
Utility.IsDotnetIsolatedApp(environment: _environment) &&
!Directory.Exists(Path.Combine(_scriptOptions.Value.RootScriptPath, ScriptConstants.AzureFunctionsSystemDirectoryName)))
{
_logger.NoAzureFunctionsFolder();
}
}
}
}
2 changes: 2 additions & 0 deletions src/WebJobs.Script/ScriptHostBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
using Microsoft.Azure.WebJobs.Script.Extensibility;
using Microsoft.Azure.WebJobs.Script.ExtensionBundle;
using Microsoft.Azure.WebJobs.Script.FileProvisioning;
using Microsoft.Azure.WebJobs.Script.Host;
using Microsoft.Azure.WebJobs.Script.Http;
using Microsoft.Azure.WebJobs.Script.ManagedDependencies;
using Microsoft.Azure.WebJobs.Script.Scale;
Expand Down Expand Up @@ -175,6 +176,7 @@ public static IHostBuilder AddScriptHostCore(this IHostBuilder builder, ScriptAp

builder.ConfigureServices((context, services) =>
{
services.AddSingleton<IHostedService, FunctionAppValidationService>();
services.AddSingleton<ExternalConfigurationStartupValidator>();
services.AddSingleton<IHostedService>(s =>
{
Expand Down
6 changes: 6 additions & 0 deletions src/WebJobs.Script/Utility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,12 @@ internal static bool IsSingleLanguage(IEnumerable<FunctionMetadata> functions, s
return ContainsFunctionWithWorkerRuntime(filteredFunctions, workerRuntime);
}

internal static bool IsDotnetIsolatedApp(IEnvironment environment, IEnumerable<FunctionMetadata> functions = null)
{
string workerRuntime = GetWorkerRuntime(functions, environment);
return workerRuntime?.Equals(RpcWorkerConstants.DotNetIsolatedLanguageWorkerName, StringComparison.OrdinalIgnoreCase) ?? false;
}

internal static string GetWorkerRuntime(IEnumerable<FunctionMetadata> functions, IEnvironment environment = null)
{
if (environment != null)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// 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.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs.Script.Description;
using Microsoft.Azure.WebJobs.Script.Host;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.WebJobs.Script.Tests;
using Moq;
using Xunit;

namespace Microsoft.Azure.WebJobs.Script.Tests
{
public class FunctionAppValidationServiceTests
{
private readonly ILogger<FunctionAppValidationService> _testLogger;
private readonly Mock<IOptions<ScriptJobHostOptions>> _scriptOptionsMock;
private readonly ScriptJobHostOptions _scriptJobHostOptions;
private readonly TestLoggerProvider _testLoggerProvider;
private readonly TimeSpan _initializationDelay = TimeSpan.FromSeconds(11);

public FunctionAppValidationServiceTests()
{
_scriptOptionsMock = new Mock<IOptions<ScriptJobHostOptions>>();

_scriptJobHostOptions = new ScriptJobHostOptions
{
RootScriptPath = "test-root-path",
IsDefaultHostConfig = false
};

_scriptOptionsMock.Setup(o => o.Value).Returns(_scriptJobHostOptions);

_testLoggerProvider = new TestLoggerProvider();
LoggerFactory factory = new LoggerFactory();
factory.AddProvider(_testLoggerProvider);
_testLogger = factory.CreateLogger<FunctionAppValidationService>();
}

[Fact]
public async Task StartAsync_NotDotnetIsolatedApp_DoesNotLogError()
{
_testLoggerProvider.ClearAllLogMessages();

var service = new FunctionAppValidationService(
_testLogger,
_scriptOptionsMock.Object,
new TestEnvironment());

// Act
await service.StartAsync(CancellationToken.None);

//Assert
var traces = _testLoggerProvider.GetAllLogMessages();
var traceMessage = traces.FirstOrDefault(val => val.EventId.Name.Equals("NoAzureFunctionsFolder"));

Assert.Null(traceMessage);
}

[Fact]
public async Task StartAsync_PlaceholderMode_DoesNotLogError()
{
_testLoggerProvider.ClearAllLogMessages();

var environment = new TestEnvironment();
environment.SetEnvironmentVariable(EnvironmentSettingNames.AzureWebsitePlaceholderMode, "1");

var service = new FunctionAppValidationService(
_testLogger,
_scriptOptionsMock.Object,
environment);

// Act
await service.StartAsync(CancellationToken.None);

//Assert
var traces = _testLoggerProvider.GetAllLogMessages();
var traceMessage = traces.FirstOrDefault(val => val.EventId.Name.Equals("NoAzureFunctionsFolder"));

Assert.Null(traceMessage);
}

[Fact]
public async Task StartAsync_NewAppWithNoPayload_DoesNotLogError()
{
_testLoggerProvider.ClearAllLogMessages();

var environment = new TestEnvironment();
environment.SetEnvironmentVariable(EnvironmentSettingNames.FunctionWorkerRuntime, "dotnet-isolated");

var scriptOptionsMock = new Mock<IOptions<ScriptJobHostOptions>>();

var scriptJobHostOptions = new ScriptJobHostOptions
{
RootScriptPath = "test-root-path",
IsDefaultHostConfig = true
};

scriptOptionsMock.Setup(o => o.Value).Returns(scriptJobHostOptions);

var service = new FunctionAppValidationService(
_testLogger,
scriptOptionsMock.Object,
environment);

// Act
await service.StartAsync(CancellationToken.None);

//Assert
var traces = _testLoggerProvider.GetAllLogMessages();
var traceMessage = traces.FirstOrDefault(val => val.EventId.Name.Equals("NoAzureFunctionsFolder"));

Assert.Null(traceMessage);
}

[Fact]
public async Task StartAsync_NoAzureFunctionsFolder_LogsWarning()
{
_testLoggerProvider.ClearAllLogMessages();

// Arrange
var functionMetadataList = ImmutableArray.Create(new FunctionMetadata());

var environment = new TestEnvironment();
environment.SetEnvironmentVariable(EnvironmentSettingNames.FunctionWorkerRuntime, "dotnet-isolated");

var service = new FunctionAppValidationService(
_testLogger,
_scriptOptionsMock.Object,
environment);

// Act
await service.StartAsync(CancellationToken.None);

await Task.Delay(_initializationDelay);

//Assert
var traces = _testLoggerProvider.GetAllLogMessages();
var traceMessage = traces.FirstOrDefault(val => val.EventId.Name.Equals("NoAzureFunctionsFolder"));

Assert.NotNull(traceMessage);
}
}
}
Loading