From 92cd66d52ffc78bdad3ab9d6afbd7c5bccbc0f91 Mon Sep 17 00:00:00 2001 From: v-vreyya Date: Tue, 18 Mar 2025 21:57:46 +0530 Subject: [PATCH 01/10] changes --- .../Actions/HostActions/StartHostAction.cs | 8 +- .../Diagnostics/LoggingFilterHelper.cs | 21 +++++- .../E2E/StartTests.cs | 74 +++++++++++++++++++ 3 files changed, 100 insertions(+), 3 deletions(-) diff --git a/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs b/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs index 95d60cb1e..bfa22da0a 100644 --- a/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs +++ b/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs @@ -77,6 +77,7 @@ internal class StartHostAction : BaseAction public string JsonOutputFile { get; set; } public string? HostRuntime { get; set; } + public string UserLogLevel { get; set; } public StartHostAction(ISecretsManager secretsManager, IProcessManager processManager) { @@ -178,6 +179,11 @@ public override ICommandLineParserResult ParseArgs(string[] args) .WithDescription($"If provided, determines which version of the host to start. Allowed values are '{DotnetConstants.InProc6HostRuntime}', '{DotnetConstants.InProc8HostRuntime}', and 'default' (which runs the out-of-process host).") .Callback(startHostFromRuntime => HostRuntime = startHostFromRuntime); + Parser + .Setup("userLogLevel") + .WithDescription($"If provided, determines the loglevel of user logs. Allowed values are '{LogLevel.Trace}', '{LogLevel.Debug}', '{LogLevel.Information}', '{LogLevel.Warning}', '{LogLevel.Error}', '{LogLevel.Critical}' and '{LogLevel.None}'.") + .Callback(userLogLevel => UserLogLevel = userLogLevel); + var parserResult = base.ParseArgs(args); bool verboseLoggingArgExists = parserResult.UnMatchedOptions.Any(o => o.LongName.Equals("verbose", StringComparison.OrdinalIgnoreCase)); // Input args do not contain --verbose flag @@ -190,7 +196,7 @@ public override ICommandLineParserResult ParseArgs(string[] args) private async Task BuildWebHost(ScriptApplicationHostOptions hostOptions, Uri listenAddress, Uri baseAddress, X509Certificate2 certificate) { - LoggingFilterHelper loggingFilterHelper = new LoggingFilterHelper(_hostJsonConfig, VerboseLogging); + LoggingFilterHelper loggingFilterHelper = new LoggingFilterHelper(_hostJsonConfig, VerboseLogging, UserLogLevel); if (GlobalCoreToolsSettings.CurrentWorkerRuntime == WorkerRuntime.dotnet || GlobalCoreToolsSettings.CurrentWorkerRuntime == WorkerRuntime.dotnetIsolated) { diff --git a/src/Azure.Functions.Cli/Diagnostics/LoggingFilterHelper.cs b/src/Azure.Functions.Cli/Diagnostics/LoggingFilterHelper.cs index 057e6375c..280321db5 100644 --- a/src/Azure.Functions.Cli/Diagnostics/LoggingFilterHelper.cs +++ b/src/Azure.Functions.Cli/Diagnostics/LoggingFilterHelper.cs @@ -18,8 +18,9 @@ public class LoggingFilterHelper public const string Ci_Continuous_Integration = "CONTINUOUS_INTEGRATION"; // Travis CI, Cirrus CI public const string Ci_Build_Number = "BUILD_NUMBER"; // Travis CI, Cirrus CI public const string Ci_Run_Id = "RUN_ID"; // TaskCluster, dsari + public static readonly string[] ValidUserLogLevels = ["Trace", "Debug", "Information", "Warning", "Error","Critical", "None"]; - public LoggingFilterHelper(IConfigurationRoot hostJsonConfig, bool? verboseLogging) + public LoggingFilterHelper(IConfigurationRoot hostJsonConfig, bool? verboseLogging, string userLogLevel = null) { VerboseLogging = verboseLogging.HasValue && verboseLogging.Value; @@ -34,7 +35,16 @@ public LoggingFilterHelper(IConfigurationRoot hostJsonConfig, bool? verboseLoggi if (Utilities.LogLevelExists(hostJsonConfig, Utilities.LogLevelDefaultSection, out LogLevel logLevel)) { SystemLogDefaultLogLevel = logLevel; - UserLogDefaultLogLevel = logLevel; + } + + // Check for user log level + if (!string.IsNullOrEmpty(userLogLevel)) + { + ValidateUserLogLevel(userLogLevel); + if (Enum.TryParse(userLogLevel, true, out LogLevel UserLogLevel)) + { + UserLogDefaultLogLevel = UserLogLevel; + } } } @@ -68,5 +78,12 @@ internal bool IsCiEnvironment(bool verboseLoggingArgExists) } return false; } + private void ValidateUserLogLevel(string UserLogLevel) + { + if (LoggingFilterHelper.ValidUserLogLevels.Contains(UserLogLevel, StringComparer.OrdinalIgnoreCase) == false) + { + throw new CliException($"The userLogLevel value provided, '{UserLogLevel}', is invalid. Valid values are '{string.Join("', '", LoggingFilterHelper.ValidUserLogLevels)}'."); + } + } } } diff --git a/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs b/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs index 81bb91483..052378097 100644 --- a/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs +++ b/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs @@ -1857,6 +1857,80 @@ await CliTester.Run(new RunConfiguration[] } } + [Theory] + [InlineData("dotnet-isolated", "debug")] + [InlineData("dotnet", "debug")] + public async Task Start_Host_SpecifieduserLogLevel_SuccessfulFunctionExecution(string WorkerRuntime, string UserLogLevel) + { + await CliTester.Run(new RunConfiguration[] + { + new RunConfiguration + { + Commands = new[] + { + $"init . --worker-runtime {WorkerRuntime}", + $"new --template Httptrigger --name HttpTrigger", + }, + CommandTimeout = TimeSpan.FromSeconds(300), + }, + new RunConfiguration + { + Commands = new[] + { + $"start --port {_funcHostPort} --userLogLevel {UserLogLevel}" + }, + ExpectExit = false, + Test = async (workingDir, p, _) => + { + using (var client = new HttpClient() { BaseAddress = new Uri($"http://localhost:{_funcHostPort}") }) + { + (await WaitUntilReady(client)).Should().BeTrue(because: _serverNotReady); + var response = await client.GetAsync("/api/HttpTriggerFunc?name=Test"); + response.StatusCode.Should().Be(HttpStatusCode.OK); + await Task.Delay(TimeSpan.FromSeconds(2)); + p.Kill(); + } + }, + CommandTimeout = TimeSpan.FromSeconds(100), + }, + }, _output); + } + + [Theory] + [InlineData("TestLogLevel")] + public async Task Start_Host_Validate_SpecifieduserLogLevel( string UserLogLevel) + { + await CliTester.Run(new RunConfiguration[] + { + new RunConfiguration + { + Commands = new[] + { + "init . --worker-runtime dotnet-isolated", + "new --template Httptrigger --name HttpTrigger", + } + }, + new RunConfiguration + { + Commands = new[] + { + $"start --port {_funcHostPort} --userLogLevel {UserLogLevel}" + }, + ExpectExit = true, + ExitInError = true, + ErrorContains = [$"The userLogLevel value provided, '{UserLogLevel}', is invalid. Valid values are '{string.Join("', '", LoggingFilterHelper.ValidUserLogLevels)}'."], + Test = async (workingDir, p, _) => + { + using (var client = new HttpClient() { BaseAddress = new Uri($"http://localhost:{_funcHostPort}") }) + { + await Task.Delay(TimeSpan.FromSeconds(2)); + } + }, + CommandTimeout = TimeSpan.FromSeconds(100), + }, + }, _output); + } + private async Task WaitUntilReady(HttpClient client) { for (var limit = 0; limit < 10; limit++) From 7768741131bf632a35cc335d5cc57b6d60156a81 Mon Sep 17 00:00:00 2001 From: v-vreyya Date: Tue, 18 Mar 2025 22:04:50 +0530 Subject: [PATCH 02/10] indentation changes --- src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs b/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs index bfa22da0a..fa264785b 100644 --- a/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs +++ b/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs @@ -77,6 +77,7 @@ internal class StartHostAction : BaseAction public string JsonOutputFile { get; set; } public string? HostRuntime { get; set; } + public string UserLogLevel { get; set; } public StartHostAction(ISecretsManager secretsManager, IProcessManager processManager) From db325221139ffb25024f2e838e9c9b58b4fce411 Mon Sep 17 00:00:00 2001 From: v-vreyya Date: Thu, 20 Mar 2025 18:59:25 +0530 Subject: [PATCH 03/10] 2nd version. --- .../Actions/HostActions/StartHostAction.cs | 19 +++-- src/Azure.Functions.Cli/Common/Constants.cs | 1 + .../Diagnostics/LoggingFilterHelper.cs | 9 +-- .../E2E/StartTests.cs | 75 +------------------ .../LoggingFilterHelperTests.cs | 1 - 5 files changed, 14 insertions(+), 91 deletions(-) diff --git a/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs b/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs index fa264785b..7a49d35d9 100644 --- a/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs +++ b/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs @@ -78,8 +78,6 @@ internal class StartHostAction : BaseAction public string? HostRuntime { get; set; } - public string UserLogLevel { get; set; } - public StartHostAction(ISecretsManager secretsManager, IProcessManager processManager) { _secretsManager = secretsManager; @@ -180,11 +178,6 @@ public override ICommandLineParserResult ParseArgs(string[] args) .WithDescription($"If provided, determines which version of the host to start. Allowed values are '{DotnetConstants.InProc6HostRuntime}', '{DotnetConstants.InProc8HostRuntime}', and 'default' (which runs the out-of-process host).") .Callback(startHostFromRuntime => HostRuntime = startHostFromRuntime); - Parser - .Setup("userLogLevel") - .WithDescription($"If provided, determines the loglevel of user logs. Allowed values are '{LogLevel.Trace}', '{LogLevel.Debug}', '{LogLevel.Information}', '{LogLevel.Warning}', '{LogLevel.Error}', '{LogLevel.Critical}' and '{LogLevel.None}'.") - .Callback(userLogLevel => UserLogLevel = userLogLevel); - var parserResult = base.ParseArgs(args); bool verboseLoggingArgExists = parserResult.UnMatchedOptions.Any(o => o.LongName.Equals("verbose", StringComparison.OrdinalIgnoreCase)); // Input args do not contain --verbose flag @@ -197,7 +190,7 @@ public override ICommandLineParserResult ParseArgs(string[] args) private async Task BuildWebHost(ScriptApplicationHostOptions hostOptions, Uri listenAddress, Uri baseAddress, X509Certificate2 certificate) { - LoggingFilterHelper loggingFilterHelper = new LoggingFilterHelper(_hostJsonConfig, VerboseLogging, UserLogLevel); + LoggingFilterHelper loggingFilterHelper = new LoggingFilterHelper(_hostJsonConfig, VerboseLogging, GetUserLogLevel()); if (GlobalCoreToolsSettings.CurrentWorkerRuntime == WorkerRuntime.dotnet || GlobalCoreToolsSettings.CurrentWorkerRuntime == WorkerRuntime.dotnetIsolated) { @@ -606,6 +599,8 @@ private void StartHostAsChildProcess(bool shouldStartNet8ChildProcess) try { + childProcessInfo.Environment["AzureFunctionsJobHost"] = "true"; + childProcessInfo.Environment["FUNCTIONS_LOG_LEVEL"] = "debug"; var childProcess = Process.Start(childProcessInfo); if (VerboseLogging == true) { @@ -838,5 +833,13 @@ private void EnsureWorkerRuntimeIsSet() // Update local.settings.json WorkerRuntimeLanguageHelper.SetWorkerRuntime(_secretsManager, GlobalCoreToolsSettings.CurrentWorkerRuntime.ToString()); } + + private string GetUserLogLevel() + { + var UserLogLevel = Environment.GetEnvironmentVariable(Constants.FunctionsLoggingLogLevel) + ?? _secretsManager.GetSecrets().FirstOrDefault(s => s.Key.Equals(Constants.FunctionsLoggingLogLevel, StringComparison.OrdinalIgnoreCase)).Value; + + return UserLogLevel; + } } } diff --git a/src/Azure.Functions.Cli/Common/Constants.cs b/src/Azure.Functions.Cli/Common/Constants.cs index 83f80bee3..228c81555 100644 --- a/src/Azure.Functions.Cli/Common/Constants.cs +++ b/src/Azure.Functions.Cli/Common/Constants.cs @@ -96,6 +96,7 @@ internal static class Constants public const string AzureDevSessionsRemoteHostName = "AzureDevSessionsRemoteHostName"; public const string AzureDevSessionsPortSuffixPlaceholder = ""; public const string GitHubReleaseApiUrl = "https://api.github.com/repos/Azure/azure-functions-core-tools/releases/latest"; + public const string FunctionsLoggingLogLevel = "AzureFunctionsJobHost__Logging__LogLevel__Function"; // Sample format https://n12abc3t-.asse.devtunnels.ms/ diff --git a/src/Azure.Functions.Cli/Diagnostics/LoggingFilterHelper.cs b/src/Azure.Functions.Cli/Diagnostics/LoggingFilterHelper.cs index 280321db5..e0cf44abd 100644 --- a/src/Azure.Functions.Cli/Diagnostics/LoggingFilterHelper.cs +++ b/src/Azure.Functions.Cli/Diagnostics/LoggingFilterHelper.cs @@ -40,7 +40,6 @@ public LoggingFilterHelper(IConfigurationRoot hostJsonConfig, bool? verboseLoggi // Check for user log level if (!string.IsNullOrEmpty(userLogLevel)) { - ValidateUserLogLevel(userLogLevel); if (Enum.TryParse(userLogLevel, true, out LogLevel UserLogLevel)) { UserLogDefaultLogLevel = UserLogLevel; @@ -78,12 +77,6 @@ internal bool IsCiEnvironment(bool verboseLoggingArgExists) } return false; } - private void ValidateUserLogLevel(string UserLogLevel) - { - if (LoggingFilterHelper.ValidUserLogLevels.Contains(UserLogLevel, StringComparer.OrdinalIgnoreCase) == false) - { - throw new CliException($"The userLogLevel value provided, '{UserLogLevel}', is invalid. Valid values are '{string.Join("', '", LoggingFilterHelper.ValidUserLogLevels)}'."); - } - } + } } diff --git a/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs b/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs index 052378097..e53957a42 100644 --- a/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs +++ b/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs @@ -10,6 +10,7 @@ using Azure.Functions.Cli.Common; using Azure.Functions.Cli.Tests.E2E.Helpers; using FluentAssertions; +using Microsoft.Azure.Storage; using Xunit; using Xunit.Abstractions; @@ -1857,80 +1858,6 @@ await CliTester.Run(new RunConfiguration[] } } - [Theory] - [InlineData("dotnet-isolated", "debug")] - [InlineData("dotnet", "debug")] - public async Task Start_Host_SpecifieduserLogLevel_SuccessfulFunctionExecution(string WorkerRuntime, string UserLogLevel) - { - await CliTester.Run(new RunConfiguration[] - { - new RunConfiguration - { - Commands = new[] - { - $"init . --worker-runtime {WorkerRuntime}", - $"new --template Httptrigger --name HttpTrigger", - }, - CommandTimeout = TimeSpan.FromSeconds(300), - }, - new RunConfiguration - { - Commands = new[] - { - $"start --port {_funcHostPort} --userLogLevel {UserLogLevel}" - }, - ExpectExit = false, - Test = async (workingDir, p, _) => - { - using (var client = new HttpClient() { BaseAddress = new Uri($"http://localhost:{_funcHostPort}") }) - { - (await WaitUntilReady(client)).Should().BeTrue(because: _serverNotReady); - var response = await client.GetAsync("/api/HttpTriggerFunc?name=Test"); - response.StatusCode.Should().Be(HttpStatusCode.OK); - await Task.Delay(TimeSpan.FromSeconds(2)); - p.Kill(); - } - }, - CommandTimeout = TimeSpan.FromSeconds(100), - }, - }, _output); - } - - [Theory] - [InlineData("TestLogLevel")] - public async Task Start_Host_Validate_SpecifieduserLogLevel( string UserLogLevel) - { - await CliTester.Run(new RunConfiguration[] - { - new RunConfiguration - { - Commands = new[] - { - "init . --worker-runtime dotnet-isolated", - "new --template Httptrigger --name HttpTrigger", - } - }, - new RunConfiguration - { - Commands = new[] - { - $"start --port {_funcHostPort} --userLogLevel {UserLogLevel}" - }, - ExpectExit = true, - ExitInError = true, - ErrorContains = [$"The userLogLevel value provided, '{UserLogLevel}', is invalid. Valid values are '{string.Join("', '", LoggingFilterHelper.ValidUserLogLevels)}'."], - Test = async (workingDir, p, _) => - { - using (var client = new HttpClient() { BaseAddress = new Uri($"http://localhost:{_funcHostPort}") }) - { - await Task.Delay(TimeSpan.FromSeconds(2)); - } - }, - CommandTimeout = TimeSpan.FromSeconds(100), - }, - }, _output); - } - private async Task WaitUntilReady(HttpClient client) { for (var limit = 0; limit < 10; limit++) diff --git a/test/Azure.Functions.Cli.Tests/LoggingFilterHelperTests.cs b/test/Azure.Functions.Cli.Tests/LoggingFilterHelperTests.cs index 4c63dc9df..0fa70f155 100644 --- a/test/Azure.Functions.Cli.Tests/LoggingFilterHelperTests.cs +++ b/test/Azure.Functions.Cli.Tests/LoggingFilterHelperTests.cs @@ -30,7 +30,6 @@ public void LoggingFilterHelper_Tests(string categoryKey, bool? verboseLogging, } if ( !string.IsNullOrEmpty(categoryKey) && categoryKey.Equals("Default", StringComparison.OrdinalIgnoreCase)) { - Assert.Equal(expectedDefaultLogLevel, loggingFilterHelper.UserLogDefaultLogLevel); Assert.Equal(expectedDefaultLogLevel, loggingFilterHelper.SystemLogDefaultLogLevel); } else From 6b68e31b3edf694192808fe2c78b60a138e4aead Mon Sep 17 00:00:00 2001 From: v-vreyya Date: Thu, 20 Mar 2025 19:01:53 +0530 Subject: [PATCH 04/10] corrections --- src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs b/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs index 7a49d35d9..037a214ee 100644 --- a/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs +++ b/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs @@ -599,8 +599,6 @@ private void StartHostAsChildProcess(bool shouldStartNet8ChildProcess) try { - childProcessInfo.Environment["AzureFunctionsJobHost"] = "true"; - childProcessInfo.Environment["FUNCTIONS_LOG_LEVEL"] = "debug"; var childProcess = Process.Start(childProcessInfo); if (VerboseLogging == true) { From 705c55e838567e79dd672345a0f5c5239e5fc765 Mon Sep 17 00:00:00 2001 From: v-vreyya Date: Thu, 20 Mar 2025 21:27:53 +0530 Subject: [PATCH 05/10] test case for dotnet --- .../E2E/StartTests.cs | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs b/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs index e53957a42..83ffd5809 100644 --- a/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs +++ b/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs @@ -1858,6 +1858,74 @@ await CliTester.Run(new RunConfiguration[] } } + [Fact] + public async Task Start_DotnetApp_WithDebugLogs_DisplaysDebugLogsInConsole() + { + var DebugLogMessage = "This is a debug log message"; + + await CliTester.Run(new RunConfiguration[] + { + new RunConfiguration + { + Commands = new[] + { + "init . --worker-runtime dotnet", + "new --template Httptrigger --name HttpTrigger" + }, + Test = async (workingDir, _, _) => + { + // Add debug logs to FunctionApp.cs + var functionAppPath = Path.Combine(workingDir, "HttpTrigger.cs"); + if (File.Exists(functionAppPath)) + { + var content = await File.ReadAllTextAsync(functionAppPath); + content = content.Replace( + "log.LogInformation(\"C# HTTP trigger function processed a request.\");", + $"log.LogInformation(\"C# HTTP trigger function processed a request.\");\nlog.LogDebug(\"{DebugLogMessage}\");" + ); + await File.WriteAllTextAsync(functionAppPath, content); + } + + // Update local.settings.json + var localSettingsPath = Path.Combine(workingDir, "local.settings.json"); + if (File.Exists(localSettingsPath)) + { + var settingsContent = await File.ReadAllTextAsync(localSettingsPath); + settingsContent = settingsContent.Replace( + "\"Values\": {", + "\"Values\": {\n \"AzureFunctionsJobHost__Logging__LogLevel__Function\": \"Debug\"," + ); + await File.WriteAllTextAsync(localSettingsPath, settingsContent); + } + }, + CommandTimeout = TimeSpan.FromSeconds(300) + }, + new RunConfiguration + { + Commands = new[] + { + $"start --port {_funcHostPort}" + }, + ExpectExit = false, + OutputContains = new[] + { + DebugLogMessage + }, + Test = async (_, p, stdout) => + { + using (var client = new HttpClient() { BaseAddress = new Uri($"http://localhost:{_funcHostPort}/") }) + { + (await WaitUntilReady(client)).Should().BeTrue(because: _serverNotReady); + var response = await client.GetAsync("/api/HttpTrigger?name=Test"); + response.StatusCode.Should().Be(HttpStatusCode.OK); + p.Kill(); + } + }, + CommandTimeout = TimeSpan.FromSeconds(300) + } + }, _output); + } + private async Task WaitUntilReady(HttpClient client) { for (var limit = 0; limit < 10; limit++) From 63d64511c3598a8a4a5f8271b774a2236132e434 Mon Sep 17 00:00:00 2001 From: v-vreyya Date: Mon, 24 Mar 2025 16:45:29 +0530 Subject: [PATCH 06/10] testcase --- test/Azure.Functions.Cli.Tests/E2E/StartTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs b/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs index 83ffd5809..9c22dab1e 100644 --- a/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs +++ b/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs @@ -1859,6 +1859,7 @@ await CliTester.Run(new RunConfiguration[] } [Fact] + [Trait(TestTraits.Group, TestTraits.RequiresNestedInProcArtifacts)] public async Task Start_DotnetApp_WithDebugLogs_DisplaysDebugLogsInConsole() { var DebugLogMessage = "This is a debug log message"; From 39093ef62fa1964f91bb444d767e8e0429b7abe6 Mon Sep 17 00:00:00 2001 From: v-vreyya Date: Mon, 24 Mar 2025 16:47:21 +0530 Subject: [PATCH 07/10] correction --- src/Azure.Functions.Cli/Diagnostics/LoggingFilterHelper.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Azure.Functions.Cli/Diagnostics/LoggingFilterHelper.cs b/src/Azure.Functions.Cli/Diagnostics/LoggingFilterHelper.cs index e0cf44abd..c730b843d 100644 --- a/src/Azure.Functions.Cli/Diagnostics/LoggingFilterHelper.cs +++ b/src/Azure.Functions.Cli/Diagnostics/LoggingFilterHelper.cs @@ -18,7 +18,6 @@ public class LoggingFilterHelper public const string Ci_Continuous_Integration = "CONTINUOUS_INTEGRATION"; // Travis CI, Cirrus CI public const string Ci_Build_Number = "BUILD_NUMBER"; // Travis CI, Cirrus CI public const string Ci_Run_Id = "RUN_ID"; // TaskCluster, dsari - public static readonly string[] ValidUserLogLevels = ["Trace", "Debug", "Information", "Warning", "Error","Critical", "None"]; public LoggingFilterHelper(IConfigurationRoot hostJsonConfig, bool? verboseLogging, string userLogLevel = null) { From 83bafb854d48eee81adf7fbc3d4fc4b425312831 Mon Sep 17 00:00:00 2001 From: v-vreyya Date: Tue, 1 Apr 2025 19:14:44 +0530 Subject: [PATCH 08/10] --flag changes --- .../Actions/HostActions/StartHostAction.cs | 35 +++++- .../Diagnostics/LoggingFilterHelper.cs | 3 +- .../E2E/StartTests.cs | 111 ++++++++++++++++++ 3 files changed, 145 insertions(+), 4 deletions(-) diff --git a/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs b/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs index 037a214ee..ef65602f2 100644 --- a/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs +++ b/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs @@ -78,6 +78,8 @@ internal class StartHostAction : BaseAction public string? HostRuntime { get; set; } + public string UserLogLevel { get; set; } + public StartHostAction(ISecretsManager secretsManager, IProcessManager processManager) { _secretsManager = secretsManager; @@ -178,6 +180,11 @@ public override ICommandLineParserResult ParseArgs(string[] args) .WithDescription($"If provided, determines which version of the host to start. Allowed values are '{DotnetConstants.InProc6HostRuntime}', '{DotnetConstants.InProc8HostRuntime}', and 'default' (which runs the out-of-process host).") .Callback(startHostFromRuntime => HostRuntime = startHostFromRuntime); + Parser + .Setup("userLogLevel") + .WithDescription($"If provided, determines the log level of user logs. Allowed values are '{LogLevel.Trace}', '{LogLevel.Debug}', '{LogLevel.Information}', '{LogLevel.Warning}', '{LogLevel.Error}', '{LogLevel.Critical}' and '{LogLevel.None}'. The default is Information.") + .Callback(userLogLevel => UserLogLevel = userLogLevel); + var parserResult = base.ParseArgs(args); bool verboseLoggingArgExists = parserResult.UnMatchedOptions.Any(o => o.LongName.Equals("verbose", StringComparison.OrdinalIgnoreCase)); // Input args do not contain --verbose flag @@ -190,7 +197,7 @@ public override ICommandLineParserResult ParseArgs(string[] args) private async Task BuildWebHost(ScriptApplicationHostOptions hostOptions, Uri listenAddress, Uri baseAddress, X509Certificate2 certificate) { - LoggingFilterHelper loggingFilterHelper = new LoggingFilterHelper(_hostJsonConfig, VerboseLogging, GetUserLogLevel()); + LoggingFilterHelper loggingFilterHelper = new LoggingFilterHelper(_hostJsonConfig, VerboseLogging, UserLogLevel); if (GlobalCoreToolsSettings.CurrentWorkerRuntime == WorkerRuntime.dotnet || GlobalCoreToolsSettings.CurrentWorkerRuntime == WorkerRuntime.dotnetIsolated) { @@ -400,7 +407,20 @@ public override async Task RunAsync() { await PreRunConditions(); var isVerbose = VerboseLogging.HasValue && VerboseLogging.Value; - + + //set flag --userLogLevel as Enviroment variable + if (!string.IsNullOrEmpty(UserLogLevel)) + { + if (IsValidUserLogLevel(UserLogLevel)) + { + Environment.SetEnvironmentVariable("AzureFunctionsJobHost__Logging__LogLevel__Function", UserLogLevel); + } + } + else + { + UserLogLevel = GetUserLogLevelFromLocalSettings(); + } + // Return if running is delegated to another version of Core Tools if (await TryHandleInProcDotNetLaunchAsync()) { @@ -832,7 +852,16 @@ private void EnsureWorkerRuntimeIsSet() WorkerRuntimeLanguageHelper.SetWorkerRuntime(_secretsManager, GlobalCoreToolsSettings.CurrentWorkerRuntime.ToString()); } - private string GetUserLogLevel() + private bool IsValidUserLogLevel(string UserLogLevel) + { + if (LoggingFilterHelper.ValidUserLogLevels.Contains(UserLogLevel, StringComparer.OrdinalIgnoreCase) == false) + { + throw new CliException($"The userLogLevel value provided, '{UserLogLevel}', is invalid. Valid values are '{string.Join("', '", LoggingFilterHelper.ValidUserLogLevels)}'. The default is Information."); + } + return true; + } + + private string GetUserLogLevelFromLocalSettings() { var UserLogLevel = Environment.GetEnvironmentVariable(Constants.FunctionsLoggingLogLevel) ?? _secretsManager.GetSecrets().FirstOrDefault(s => s.Key.Equals(Constants.FunctionsLoggingLogLevel, StringComparison.OrdinalIgnoreCase)).Value; diff --git a/src/Azure.Functions.Cli/Diagnostics/LoggingFilterHelper.cs b/src/Azure.Functions.Cli/Diagnostics/LoggingFilterHelper.cs index c730b843d..9afa9b3f5 100644 --- a/src/Azure.Functions.Cli/Diagnostics/LoggingFilterHelper.cs +++ b/src/Azure.Functions.Cli/Diagnostics/LoggingFilterHelper.cs @@ -18,6 +18,7 @@ public class LoggingFilterHelper public const string Ci_Continuous_Integration = "CONTINUOUS_INTEGRATION"; // Travis CI, Cirrus CI public const string Ci_Build_Number = "BUILD_NUMBER"; // Travis CI, Cirrus CI public const string Ci_Run_Id = "RUN_ID"; // TaskCluster, dsari + public static readonly string[] ValidUserLogLevels = ["Trace", "Debug", "Information", "Warning", "Error", "Critical", "None"]; public LoggingFilterHelper(IConfigurationRoot hostJsonConfig, bool? verboseLogging, string userLogLevel = null) { @@ -38,7 +39,7 @@ public LoggingFilterHelper(IConfigurationRoot hostJsonConfig, bool? verboseLoggi // Check for user log level if (!string.IsNullOrEmpty(userLogLevel)) - { + { if (Enum.TryParse(userLogLevel, true, out LogLevel UserLogLevel)) { UserLogDefaultLogLevel = UserLogLevel; diff --git a/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs b/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs index 9c22dab1e..d0d8e533e 100644 --- a/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs +++ b/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs @@ -1858,6 +1858,63 @@ await CliTester.Run(new RunConfiguration[] } } + [Fact] + [Trait(TestTraits.Group, TestTraits.RequiresNestedInProcArtifacts)] + public async Task Start_DotnetApp_WithUserLogLevelFlag_ShowDebugLogsInConsole() + { + var DebugLogMessage = "This is a debug log message"; + + await CliTester.Run(new RunConfiguration[] + { + new RunConfiguration + { + Commands = new[] + { + "init . --worker-runtime dotnet", + "new --template Httptrigger --name HttpTrigger" + }, + Test = async (workingDir, _, _) => + { + // Add debug logs to FunctionApp.cs + var functionAppPath = Path.Combine(workingDir, "HttpTrigger.cs"); + if (File.Exists(functionAppPath)) + { + var content = await File.ReadAllTextAsync(functionAppPath); + content = content.Replace( + "log.LogInformation(\"C# HTTP trigger function processed a request.\");", + $"log.LogInformation(\"C# HTTP trigger function processed a request.\");\nlog.LogDebug(\"{DebugLogMessage}\");" + ); + await File.WriteAllTextAsync(functionAppPath, content); + } + }, + CommandTimeout = TimeSpan.FromSeconds(300) + }, + new RunConfiguration + { + Commands = new[] + { + $"start --port {_funcHostPort} --userLogLevel Debug" + }, + ExpectExit = false, + OutputContains = new[] + { + DebugLogMessage + }, + Test = async (_, p, stdout) => + { + using (var client = new HttpClient() { BaseAddress = new Uri($"http://localhost:{_funcHostPort}/") }) + { + (await WaitUntilReady(client)).Should().BeTrue(because: _serverNotReady); + var response = await client.GetAsync("/api/HttpTrigger?name=Test"); + response.StatusCode.Should().Be(HttpStatusCode.OK); + p.Kill(); + } + }, + CommandTimeout = TimeSpan.FromSeconds(300) + } + }, _output); + } + [Fact] [Trait(TestTraits.Group, TestTraits.RequiresNestedInProcArtifacts)] public async Task Start_DotnetApp_WithDebugLogs_DisplaysDebugLogsInConsole() @@ -1926,6 +1983,60 @@ await CliTester.Run(new RunConfiguration[] } }, _output); } + [Fact] + [Trait(TestTraits.Group, TestTraits.RequiresNestedInProcArtifacts)] + public async Task Start_NodeApp_DisplaysDebugLogsInConsole() + { + await CliTester.Run(new RunConfiguration[] + { + new RunConfiguration + { + Commands = new[] + { + "init . --worker-runtime node", + "new --template Httptrigger --name HttpTrigger" + }, + Test = async (workingDir, _, _) => + { + // Add debug logs to FunctionApp.cs + var functionAppPath = Path.Combine(workingDir,"src", "functions", "HttpTrigger.js"); + if (File.Exists(functionAppPath)) + { + var content = await File.ReadAllTextAsync(functionAppPath); + content = content.Replace( + $"context.log(`Http function processed request for url \"${{request.url}}\"`);", + $"context.log(`Http function processed request for url \"${{request.url}}\"`);\ncontext.debug(\"This is a debug log message\");" + ); + await File.WriteAllTextAsync(functionAppPath, content); + } + }, + CommandTimeout = TimeSpan.FromSeconds(300) + }, + new RunConfiguration + { + Commands = new[] + { + $"start --port {_funcHostPort} --userLogLevel Debug" + }, + ExpectExit = false, + OutputContains = new[] + { + "This is a debug log message" + }, + Test = async (_, p, stdout) => + { + using (var client = new HttpClient() { BaseAddress = new Uri($"http://localhost:{_funcHostPort}/") }) + { + (await WaitUntilReady(client)).Should().BeTrue(because: _serverNotReady); + var response = await client.GetAsync("/api/HttpTrigger?name=Test"); + response.StatusCode.Should().Be(HttpStatusCode.OK); + p.Kill(); + } + }, + CommandTimeout = TimeSpan.FromSeconds(300) + } + }, _output); + } private async Task WaitUntilReady(HttpClient client) { From 7a564ea27b48cd59b6f078ee45c7f9e24e9529e5 Mon Sep 17 00:00:00 2001 From: v-vreyya Date: Wed, 2 Apr 2025 14:36:08 +0530 Subject: [PATCH 09/10] test cases --- .../Actions/HostActions/StartHostAction.cs | 29 +++-- .../E2E/StartTests.cs | 110 ++++++++++++++++++ 2 files changed, 127 insertions(+), 12 deletions(-) diff --git a/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs b/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs index ef65602f2..9ad5d8bf8 100644 --- a/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs +++ b/src/Azure.Functions.Cli/Actions/HostActions/StartHostAction.cs @@ -408,18 +408,8 @@ public override async Task RunAsync() await PreRunConditions(); var isVerbose = VerboseLogging.HasValue && VerboseLogging.Value; - //set flag --userLogLevel as Enviroment variable - if (!string.IsNullOrEmpty(UserLogLevel)) - { - if (IsValidUserLogLevel(UserLogLevel)) - { - Environment.SetEnvironmentVariable("AzureFunctionsJobHost__Logging__LogLevel__Function", UserLogLevel); - } - } - else - { - UserLogLevel = GetUserLogLevelFromLocalSettings(); - } + //set --userLogLevel flag + ConfigureUserLogLevelFromFlagOrSettings(); // Return if running is delegated to another version of Core Tools if (await TryHandleInProcDotNetLaunchAsync()) @@ -851,6 +841,21 @@ private void EnsureWorkerRuntimeIsSet() // Update local.settings.json WorkerRuntimeLanguageHelper.SetWorkerRuntime(_secretsManager, GlobalCoreToolsSettings.CurrentWorkerRuntime.ToString()); } + + private void ConfigureUserLogLevelFromFlagOrSettings() + { + if (!string.IsNullOrEmpty(UserLogLevel)) + { + if (IsValidUserLogLevel(UserLogLevel)) + { + Environment.SetEnvironmentVariable("AzureFunctionsJobHost__Logging__LogLevel__Function", UserLogLevel); + } + } + else + { + UserLogLevel = GetUserLogLevelFromLocalSettings(); + } + } private bool IsValidUserLogLevel(string UserLogLevel) { diff --git a/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs b/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs index d0d8e533e..f10f8d3ef 100644 --- a/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs +++ b/test/Azure.Functions.Cli.Tests/E2E/StartTests.cs @@ -1983,6 +1983,7 @@ await CliTester.Run(new RunConfiguration[] } }, _output); } + [Fact] [Trait(TestTraits.Group, TestTraits.RequiresNestedInProcArtifacts)] public async Task Start_NodeApp_DisplaysDebugLogsInConsole() @@ -2038,6 +2039,115 @@ await CliTester.Run(new RunConfiguration[] }, _output); } + [Fact] + [Trait(TestTraits.Group, TestTraits.RequiresNestedInProcArtifacts)] + public async Task Start_DotnetApp_DisplaysDebugLogsInConsole() + { + await CliTester.Run(new RunConfiguration[] + { + new RunConfiguration + { + Commands = new[] + { + "init . --worker-runtime dotnet-isolated", + "new --template Httptrigger --name HttpTrigger" + }, + Test = async (workingDir, _, _) => + { + //set minimum level in Program.cs + var functionProgramFile = Path.Combine(workingDir, "Program.cs"); + if (File.Exists(functionProgramFile)) + { + var Programcontent = await File.ReadAllTextAsync(functionProgramFile); + Programcontent = Programcontent.Replace( + $"using Microsoft.Extensions.Hosting;", + $"using Microsoft.Extensions.Hosting;\r\nusing Microsoft.Extensions.Logging;" + ). + Replace( + $"builder.Build().Run();", + $"builder.Logging.SetMinimumLevel(LogLevel.Debug);\r\nbuilder.Build().Run();" + ); + await File.WriteAllTextAsync(functionProgramFile, Programcontent); + } + + // Add debug logs to FunctionApp.cs + var functionAppPath = Path.Combine(workingDir,"HttpTrigger.cs"); + if (File.Exists(functionAppPath)) + { + var content = await File.ReadAllTextAsync(functionAppPath); + content = content.Replace( + $"_logger.LogInformation(\"C# HTTP trigger function processed a request.\");", + $"_logger.LogInformation(\"C# HTTP trigger function processed a request.\");\r\n_logger.LogDebug(\"This is Debug log from Dotnet-Isolated function app.\");" + ); + await File.WriteAllTextAsync(functionAppPath, content); + } + }, + CommandTimeout = TimeSpan.FromSeconds(300) + }, + new RunConfiguration + { + Commands = new[] + { + $"start --port {_funcHostPort} --userLogLevel Debug" + }, + ExpectExit = false, + OutputContains = new[] + { + "This is Debug log from Dotnet-Isolated function app." + }, + Test = async (_, p, stdout) => + { + using (var client = new HttpClient() { BaseAddress = new Uri($"http://localhost:{_funcHostPort}/") }) + { + (await WaitUntilReady(client)).Should().BeTrue(because: _serverNotReady); + var response = await client.GetAsync("/api/HttpTrigger?name=Test"); + response.StatusCode.Should().Be(HttpStatusCode.OK); + p.Kill(); + } + }, + CommandTimeout = TimeSpan.FromSeconds(300) + } + }, _output); + } + + [Theory] + [InlineData("dotnet", "TestDotnet")] + [InlineData("dotnet-isolated", "TestDotnetIsolated")] + [InlineData("node", "TestNode")] + public async Task Start_FunctionApp_ExpectedToFail_WithInvalidUserLogLevel(string WorkerRuntime, string UserLogLevel) + { + await CliTester.Run(new RunConfiguration[] + { + new RunConfiguration + { + Commands = new[] + { + $"init . --worker-runtime {WorkerRuntime}", + $"new --template Httptrigger --name HttpTrigger", + }, + CommandTimeout = TimeSpan.FromSeconds(300) + }, + new RunConfiguration + { + Commands = new[] + { + $"start --port {_funcHostPort} --userLogLevel {UserLogLevel}" + }, + ExpectExit = true, + ExitInError = true, + ErrorContains = [$"The userLogLevel value provided, '{UserLogLevel}', is invalid. Valid values are '{string.Join("', '", LoggingFilterHelper.ValidUserLogLevels)}'. The default is Information."], + Test = async (workingDir, p, _) => + { + using (var client = new HttpClient() { BaseAddress = new Uri($"http://localhost:{_funcHostPort}") }) + { + await Task.Delay(TimeSpan.FromSeconds(2)); + } + }, + CommandTimeout = TimeSpan.FromSeconds(300) + } + }, _output); + } + private async Task WaitUntilReady(HttpClient client) { for (var limit = 0; limit < 10; limit++) From df44ce9797702d125d345f357410be66c282004b Mon Sep 17 00:00:00 2001 From: v-vreyya Date: Fri, 4 Apr 2025 19:21:48 +0530 Subject: [PATCH 10/10] function changes --- src/Azure.Functions.Cli/Common/Utilities.cs | 1 + .../Diagnostics/LoggingFilterHelper.cs | 20 +++++++++++-------- .../LoggingFilterHelperTests.cs | 1 + 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/Azure.Functions.Cli/Common/Utilities.cs b/src/Azure.Functions.Cli/Common/Utilities.cs index d9670e698..9de97b321 100644 --- a/src/Azure.Functions.Cli/Common/Utilities.cs +++ b/src/Azure.Functions.Cli/Common/Utilities.cs @@ -26,6 +26,7 @@ internal static class Utilities { public const string LogLevelSection = "loglevel"; public const string LogLevelDefaultSection = "Default"; + public const string LogLevelFunctionSection = "Function"; internal static void PrintLogo() { diff --git a/src/Azure.Functions.Cli/Diagnostics/LoggingFilterHelper.cs b/src/Azure.Functions.Cli/Diagnostics/LoggingFilterHelper.cs index 9afa9b3f5..dc3b21a28 100644 --- a/src/Azure.Functions.Cli/Diagnostics/LoggingFilterHelper.cs +++ b/src/Azure.Functions.Cli/Diagnostics/LoggingFilterHelper.cs @@ -32,18 +32,22 @@ public LoggingFilterHelper(IConfigurationRoot hostJsonConfig, bool? verboseLoggi { SystemLogDefaultLogLevel = LogLevel.Information; } - if (Utilities.LogLevelExists(hostJsonConfig, Utilities.LogLevelDefaultSection, out LogLevel logLevel)) + if (Utilities.LogLevelExists(hostJsonConfig, Utilities.LogLevelDefaultSection, out LogLevel defaultLogLevel)) { - SystemLogDefaultLogLevel = logLevel; + SystemLogDefaultLogLevel = defaultLogLevel; + UserLogDefaultLogLevel = defaultLogLevel; } - // Check for user log level - if (!string.IsNullOrEmpty(userLogLevel)) + // set user log value to Function + if ( Utilities.LogLevelExists(hostJsonConfig, Utilities.LogLevelFunctionSection, out LogLevel functionLogLevel)) { - if (Enum.TryParse(userLogLevel, true, out LogLevel UserLogLevel)) - { - UserLogDefaultLogLevel = UserLogLevel; - } + UserLogDefaultLogLevel = functionLogLevel; + } + + // set user log value to --userLoglevel Flag + if (!string.IsNullOrEmpty(userLogLevel) && Enum.TryParse(userLogLevel, true, out LogLevel UserLogLevel)) + { + UserLogDefaultLogLevel = UserLogLevel; } } diff --git a/test/Azure.Functions.Cli.Tests/LoggingFilterHelperTests.cs b/test/Azure.Functions.Cli.Tests/LoggingFilterHelperTests.cs index 0fa70f155..4c63dc9df 100644 --- a/test/Azure.Functions.Cli.Tests/LoggingFilterHelperTests.cs +++ b/test/Azure.Functions.Cli.Tests/LoggingFilterHelperTests.cs @@ -30,6 +30,7 @@ public void LoggingFilterHelper_Tests(string categoryKey, bool? verboseLogging, } if ( !string.IsNullOrEmpty(categoryKey) && categoryKey.Equals("Default", StringComparison.OrdinalIgnoreCase)) { + Assert.Equal(expectedDefaultLogLevel, loggingFilterHelper.UserLogDefaultLogLevel); Assert.Equal(expectedDefaultLogLevel, loggingFilterHelper.SystemLogDefaultLogLevel); } else