diff --git a/release_notes.md b/release_notes.md index 1f9ad364f..3a8823672 100644 --- a/release_notes.md +++ b/release_notes.md @@ -10,3 +10,5 @@ #### Changes - Added end-of-life warnings for all runtime versions during `func azure functionapp publish`. (#4760) +- Reduced console output noise by moving informational messages to verbose logging. (#4768) +- Fixed an issue where creating an MCP Tool trigger function would fail with "Unknown template 'McpToolTrigger'" error. (#4768) \ No newline at end of file diff --git a/src/Cli/func/Actions/HostActions/StartHostAction.cs b/src/Cli/func/Actions/HostActions/StartHostAction.cs index f60a8dc52..85eab011c 100644 --- a/src/Cli/func/Actions/HostActions/StartHostAction.cs +++ b/src/Cli/func/Actions/HostActions/StartHostAction.cs @@ -152,12 +152,6 @@ public override ICommandLineParserResult ParseArgs(string[] args) .WithDescription("A space separated list of functions to load.") .Callback(f => EnabledFunctions = f); - Parser - .Setup("verbose") - .WithDescription("When false, hides system logs other than warnings and errors.") - .SetDefault(false) - .Callback(v => VerboseLogging = v); - Parser .Setup("user-log-level") .WithDescription("Sets the minimum log level for displaying user logs. Valid values: Trace, Debug, Information, Warning, Error, Critical, None. This does not affect system logs.") @@ -185,16 +179,10 @@ 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); - var parserResult = base.ParseArgs(args); - bool verboseLoggingArgExists = parserResult.UnMatchedOptions.Any(o => o.LongName.Equals("verbose", StringComparison.OrdinalIgnoreCase)); - - // Input args do not contain --verbose flag - if (!VerboseLogging.Value && verboseLoggingArgExists) - { - VerboseLogging = null; - } + // Verbose logging now follows the global --verbose flag + VerboseLogging = GlobalCoreToolsSettings.IsVerbose; - return parserResult; + return base.ParseArgs(args); } private async Task BuildWebHost(ScriptApplicationHostOptions hostOptions, Uri listenAddress, Uri baseAddress, X509Certificate2 certificate) diff --git a/src/Cli/func/Common/SecretsManager.cs b/src/Cli/func/Common/SecretsManager.cs index 60db017f4..b49ef5663 100644 --- a/src/Cli/func/Common/SecretsManager.cs +++ b/src/Cli/func/Common/SecretsManager.cs @@ -5,6 +5,7 @@ using Azure.Functions.Cli.Interfaces; using Colors.Net; using Microsoft.Azure.WebJobs.Script; +using static Azure.Functions.Cli.Common.OutputTheme; using static Colors.Net.StringStaticMethods; namespace Azure.Functions.Cli.Common @@ -27,7 +28,11 @@ public static string AppSettingsFilePath }); var secretsFilePath = Path.Combine(rootPath, secretsFile); - ColoredConsole.WriteLine(DarkGray($"'{secretsFile}' found in root directory ({rootPath}).")); + if (GlobalCoreToolsSettings.IsVerbose) + { + ColoredConsole.WriteLine(VerboseColor($"'{secretsFile}' found in root directory ({rootPath}).")); + } + return secretsFilePath; } } diff --git a/src/Cli/func/Common/TemplatesManager.cs b/src/Cli/func/Common/TemplatesManager.cs index 8269673db..7ed8fc29f 100644 --- a/src/Cli/func/Common/TemplatesManager.cs +++ b/src/Cli/func/Common/TemplatesManager.cs @@ -8,6 +8,7 @@ using Azure.Functions.Cli.Interfaces; using Colors.Net; using Newtonsoft.Json; +using static Azure.Functions.Cli.Common.OutputTheme; namespace Azure.Functions.Cli.Common { @@ -177,7 +178,11 @@ private async Task DeployNewNodeProgrammingModel(string functionName, string fil foreach (var filePath in fileList.Keys) { RemoveFileIfExists(filePath); - ColoredConsole.WriteLine($"Creating a new file {filePath}"); + if (GlobalCoreToolsSettings.IsVerbose) + { + ColoredConsole.WriteLine(VerboseColor($"Creating a new file {filePath}")); + } + await FileSystemHelpers.WriteAllTextToFileAsync(filePath, fileList[filePath]); } } @@ -249,7 +254,11 @@ private async Task DeployTraditionalModel(string name, Template template) foreach (var file in template.Files.Where(kv => !kv.Key.EndsWith(".dat"))) { var filePath = Path.Combine(path, file.Key); - ColoredConsole.WriteLine($"Writing {filePath}"); + if (GlobalCoreToolsSettings.IsVerbose) + { + ColoredConsole.WriteLine(VerboseColor($"Writing {filePath}")); + } + await FileSystemHelpers.WriteAllTextToFileAsync(filePath, file.Value); } @@ -371,7 +380,11 @@ private async Task WriteFunctionBody(NewTemplate template, TemplateAction action var filePath = Path.Combine(Environment.CurrentDirectory, fileName); if (!FileSystemHelpers.FileExists(filePath)) { - ColoredConsole.WriteLine($"Creating a new file {filePath}"); + if (GlobalCoreToolsSettings.IsVerbose) + { + ColoredConsole.WriteLine(VerboseColor($"Creating a new file {filePath}")); + } + await FileSystemHelpers.WriteAllTextToFileAsync(filePath, sourceContent); } else @@ -382,7 +395,11 @@ private async Task WriteFunctionBody(NewTemplate template, TemplateAction action } var fileContent = await FileSystemHelpers.ReadAllTextFromFileAsync(filePath); - ColoredConsole.WriteLine($"Appending to {filePath}"); + if (GlobalCoreToolsSettings.IsVerbose) + { + ColoredConsole.WriteLine(VerboseColor($"Appending to {filePath}")); + } + fileContent = $"{fileContent}{Environment.NewLine}{Environment.NewLine}{sourceContent}"; // Update the file. diff --git a/src/Cli/func/ConsoleApp.cs b/src/Cli/func/ConsoleApp.cs index dc541e97c..e46d79795 100644 --- a/src/Cli/func/ConsoleApp.cs +++ b/src/Cli/func/ConsoleApp.cs @@ -22,9 +22,13 @@ internal class ConsoleApp private readonly IContainer _container; private readonly string[] _args; private readonly IEnumerable _actionAttributes; - private readonly string[] _helpArgs = new[] { "help", "h", "?" }; + private readonly string[] _helpArgs = ["help", "h", "?"]; private readonly TelemetryEvent _telemetryEvent; + // Global options that should be filtered out during action parsing + private static readonly HashSet _globalFlags = new(StringComparer.OrdinalIgnoreCase) { "--version", "--verbose" }; + private static readonly HashSet _globalOptionsWithValues = new(StringComparer.OrdinalIgnoreCase) { "--script-root", "--prefix" }; + internal ConsoleApp(string[] args, Assembly assembly, IContainer container) { _args = args; @@ -247,7 +251,7 @@ internal IAction Parse() argsToParse = isHelp ? _args.Where(a => !a.StartsWith("-")) - : _args; + : FilterOutGlobalOptions(_args); } // We'll need to grab context arg: string, subcontext arg: string, action arg: string @@ -263,6 +267,15 @@ internal IAction Parse() // If isHelp, skip one and parse the rest of the command as usual. var argsStack = new Stack(argsToParse.Reverse()); + // If no arguments remain after filtering, show general help + if (!argsStack.Any()) + { + _telemetryEvent.CommandName = "help"; + _telemetryEvent.IActionName = typeof(HelpAction).Name; + _telemetryEvent.Parameters = new List(); + return new HelpAction(_actionAttributes, CreateAction); + } + // Grab the first string, but don't pop it off the stack. // If it's indeed a valid context, will remove it later. // Otherwise, it could be just an action. Actions are allowed not to have contexts. @@ -405,44 +418,60 @@ internal IAction Parse() } } + /// + /// Filters out global options and their values from the argument list. + /// Global flags: --version, --verbose + /// Global options with values: --script-root, --prefix. + /// + private static IEnumerable FilterOutGlobalOptions(string[] args) + { + for (int i = 0; i < args.Length; i++) + { + var arg = args[i]; + + if (_globalFlags.Contains(arg)) + { + continue; + } + + if (_globalOptionsWithValues.Contains(arg)) + { + i++; // Skip the next argument (the value) + continue; + } + + yield return arg; + } + } + /// /// This method will update Environment.CurrentDirectory /// if there is a --script-root or a --prefix provided on the commandline. /// - /// args to check for --prefix or --script-root. private void UpdateCurrentDirectory(string[] args) { - // assume index of -1 means the string is not there - int index = -1; for (var i = 0; i < args.Length; i++) { - if (args[i].Equals("--script-root", StringComparison.OrdinalIgnoreCase) - || args[i].Equals("--prefix", StringComparison.OrdinalIgnoreCase)) + if (_globalOptionsWithValues.Contains(args[i]) && i + 1 < args.Length) { - // update the index to point to the following entry in args - // which should contain the path for a prefix - index = i + 1; _telemetryEvent.PrefixOrScriptRoot = true; - break; - } - } - // make sure index still in the array - if (index != -1 && index < args.Length) - { // Path.Combine takes care of checking if the path is full path or not. // For example, Path.Combine(@"C:\temp", @"dir\dir") => "C:\temp\dir\dir" // Path.Combine(@"C:\temp", @"C:\Windows") => "C:\Windows" // Path.Combine("/usr/bin", "dir/dir") => "/usr/bin/dir/dir" // Path.Combine("/usr/bin", "/opt/dir") => "/opt/dir" - var path = Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, args[index])); - if (FileSystemHelpers.DirectoryExists(path)) - { - Environment.CurrentDirectory = path; - } - else - { - throw new CliException($"\"{path}\" doesn't exist."); + var path = Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, args[i + 1])); + if (FileSystemHelpers.DirectoryExists(path)) + { + Environment.CurrentDirectory = path; + } + else + { + throw new CliException($"\"{path}\" doesn't exist."); + } + + return; } } } diff --git a/src/Cli/func/Helpers/DotnetHelpers.cs b/src/Cli/func/Helpers/DotnetHelpers.cs index 8506b9bba..d9a3a481a 100644 --- a/src/Cli/func/Helpers/DotnetHelpers.cs +++ b/src/Cli/func/Helpers/DotnetHelpers.cs @@ -129,7 +129,11 @@ await TemplateOperationAsync( public static async Task DeployDotnetFunction(string templateName, string functionName, string namespaceStr, string language, WorkerRuntime workerRuntime, AuthorizationLevel? httpAuthorizationLevel = null) { - ColoredConsole.WriteLine($"{Environment.NewLine}Creating dotnet function..."); + if (GlobalCoreToolsSettings.IsVerbose) + { + ColoredConsole.WriteLine(VerboseColor($"{Environment.NewLine}Creating dotnet function...")); + } + await TemplateOperationAsync( async () => { @@ -183,6 +187,7 @@ await TemplateOperationAsync( "iothubtrigger" => "iothub", "kafkatrigger" => "kafka", "kafkaoutput" => "kafkao", + "mcptooltrigger" => "mcptooltrigger", "queuetrigger" => "queue", "sendgrid" => "sendgrid", "servicebusqueuetrigger" => "squeue", diff --git a/src/Cli/func/Helpers/GlobalCoreToolsSettings.cs b/src/Cli/func/Helpers/GlobalCoreToolsSettings.cs index 64e1ecfbe..22377989d 100644 --- a/src/Cli/func/Helpers/GlobalCoreToolsSettings.cs +++ b/src/Cli/func/Helpers/GlobalCoreToolsSettings.cs @@ -12,9 +12,12 @@ public static class GlobalCoreToolsSettings { private static WorkerRuntime _currentWorkerRuntime; private static bool _isHelpRunning; + private static bool _isVerbose; public static bool IsHelpRunning => _isHelpRunning; + public static bool IsVerbose => _isVerbose; + public static ProgrammingModel? CurrentProgrammingModel { get; set; } public static WorkerRuntime CurrentWorkerRuntime @@ -48,6 +51,8 @@ public static WorkerRuntime CurrentWorkerRuntimeOrNone public static void Init(ISecretsManager secretsManager, string[] args) { + _isVerbose = args.Contains("--verbose"); + try { if (args.Contains("--csharp")) diff --git a/src/Cli/func/Helpers/PythonHelpers.cs b/src/Cli/func/Helpers/PythonHelpers.cs index ec5880a1e..b7ab196f9 100644 --- a/src/Cli/func/Helpers/PythonHelpers.cs +++ b/src/Cli/func/Helpers/PythonHelpers.cs @@ -178,7 +178,10 @@ public static void AssertPythonVersion(WorkerLanguageVersionInfo pythonVersion, return; } - ColoredConsole.WriteLine(AdditionalInfoColor($"Found Python version {pythonVersion.Version} ({pythonVersion.ExecutablePath}).")); + if (GlobalCoreToolsSettings.IsVerbose) + { + ColoredConsole.WriteLine(VerboseColor($"Found Python version {pythonVersion.Version} ({pythonVersion.ExecutablePath}).")); + } // Python 3.[7|8|9|10|11|12] (supported) if (IsVersionSupported(pythonVersion)) diff --git a/src/Cli/func/Helpers/WorkerRuntimeLanguageHelper.cs b/src/Cli/func/Helpers/WorkerRuntimeLanguageHelper.cs index 93bcd9953..fa7182d95 100644 --- a/src/Cli/func/Helpers/WorkerRuntimeLanguageHelper.cs +++ b/src/Cli/func/Helpers/WorkerRuntimeLanguageHelper.cs @@ -177,12 +177,16 @@ public static IEnumerable LanguagesForWorker(WorkerRuntime worker) public static WorkerRuntime GetCurrentWorkerRuntimeLanguage(ISecretsManager secretsManager, bool refreshSecrets = false) { var setting = Environment.GetEnvironmentVariable(Constants.FunctionsWorkerRuntime) - ?? secretsManager.GetSecrets(refreshSecrets).FirstOrDefault(s => s.Key.Equals(Constants.FunctionsWorkerRuntime, StringComparison.OrdinalIgnoreCase)).Value; + ?? secretsManager.GetSecrets(refreshSecrets)?.FirstOrDefault(s => s.Key.Equals(Constants.FunctionsWorkerRuntime, StringComparison.OrdinalIgnoreCase)).Value; try { WorkerRuntime workerRuntime = NormalizeWorkerRuntime(setting); - ColoredConsole.WriteLine($"Resolving worker runtime to '{GetRuntimeMoniker(workerRuntime)}'."); + if (GlobalCoreToolsSettings.IsVerbose) + { + ColoredConsole.WriteLine(VerboseColor($"Resolving worker runtime to '{GetRuntimeMoniker(workerRuntime)}'.")); + } + return workerRuntime; } catch diff --git a/test/Cli/Func.E2ETests/Commands/FuncStart/AuthTests.cs b/test/Cli/Func.E2ETests/Commands/FuncStart/AuthTests.cs index 83f776a16..260b7a5d0 100644 --- a/test/Cli/Func.E2ETests/Commands/FuncStart/AuthTests.cs +++ b/test/Cli/Func.E2ETests/Commands/FuncStart/AuthTests.cs @@ -40,7 +40,7 @@ public async Task Start_DotnetIsolated_EnableAuthFeature( }; // Build command arguments based on enableAuth parameter - var commandArgs = new List { "start", "--verbose", "--port", port.ToString() }; + var commandArgs = new List { "--port", port.ToString(), "--verbose" }; if (enableAuth) { commandArgs.Add("--enableAuth");