Skip to content
Open
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
6 changes: 3 additions & 3 deletions src/Azure.DataApiBuilder.Mcp/Core/McpStdioServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,7 @@ private void HandleListTools(JsonElement? id)
/// <remarks>
/// Log level precedence (highest to lowest):
/// 1. MCP <c>logging/setLevel</c> (Agent) - always wins, overrides CLI and Config.
/// 2. CLI <c>--LogLevel</c> flag.
/// 2. CLI <c>--log-level</c> flag.
/// 3. Config <c>runtime.telemetry.log-level</c>.
/// 4. Default: <c>None</c> for MCP stdio mode (silent by default to keep stdout clean for JSON-RPC),
/// <c>Error</c> in Production, <c>Debug</c> in Development.
Expand All @@ -329,7 +329,7 @@ private void HandleListTools(JsonElement? id)
/// hot-reloads do not overwrite the agent's choice.
/// 3. Restore <see cref="Console.Error"/> to the real stderr stream when logging is enabled,
/// in case startup redirected it to <see cref="TextWriter.Null"/> (default for
/// <c>--mcp-stdio</c> or <c>--LogLevel none</c>).
/// <c>--mcp-stdio</c> or <c>--log-level none</c>).
/// </remarks>
private void HandleSetLogLevel(JsonElement? id, JsonElement root)
{
Expand Down Expand Up @@ -388,7 +388,7 @@ private void HandleSetLogLevel(JsonElement? id, JsonElement root)
bool updated = logLevelController.UpdateFromMcp(level);

// Restore stderr if the agent successfully turned logging on. When `--mcp-stdio` (or
// `--LogLevel none`) was the startup default, stderr was redirected to TextWriter.Null;
// `--log-level none`) was the startup default, stderr was redirected to TextWriter.Null;
// re-enable it now so subsequent logs flow.
if (updated && isLoggingEnabled)
{
Expand Down
6 changes: 3 additions & 3 deletions src/Cli.Tests/CustomLoggerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace Cli.Tests;
/// <summary>
/// Tests for <see cref="CustomLoggerProvider"/> covering both the standard CLI
/// path (writes to stdout/stderr with abbreviated labels) and the MCP stdio path
/// (suppressed by default, opt-in via either CLI <c>--LogLevel</c> or the
/// (suppressed by default, opt-in via either CLI <c>--log-level</c> or the
/// runtime config's <c>log-level</c>, always routed to stderr to keep the
/// JSON-RPC channel on stdout uncorrupted).
/// </summary>
Expand Down Expand Up @@ -85,7 +85,7 @@ public void LogOutput_UsesAbbreviatedLogLevelLabels(LogLevel logLevel, string ex
}

/// <summary>
/// MCP stdio mode with no overrides (neither CLI <c>--LogLevel</c> nor
/// MCP stdio mode with no overrides (neither CLI <c>--log-level</c> nor
/// config <c>log-level</c>): all output must be suppressed so the JSON-RPC
/// channel stays clean.
/// </summary>
Expand All @@ -106,7 +106,7 @@ public void Mcp_NoOverrides_SuppressesAllOutput()
}

/// <summary>
/// MCP stdio mode with a CLI-supplied <c>--LogLevel</c>: logs must always
/// MCP stdio mode with a CLI-supplied <c>--log-level</c>: logs must always
/// go to stderr (never stdout) and the level threshold from
/// <see cref="Cli.Utils.CliLogLevel"/> must be honored.
/// </summary>
Expand Down
39 changes: 21 additions & 18 deletions src/Cli.Tests/EndToEndTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -814,23 +814,25 @@ public Task TestUpdatingStoredProcedureWithRestMethods()
}

/// <summary>
/// Test to validate that the engine starts successfully when --verbose and --LogLevel
/// Test to validate that the engine starts successfully when --verbose and --log-level
/// options are used with the start command
/// This test does not validate whether the engine logs messages at the specified log level
/// </summary>
/// <param name="logLevelOption">Log level options</param>
[DataTestMethod]
[DataRow("", DisplayName = "No logging from command line.")]
[DataRow("--verbose", DisplayName = "Verbose logging from command line.")]
[DataRow("--LogLevel 0", DisplayName = "LogLevel 0 from command line.")]
[DataRow("--LogLevel 1", DisplayName = "LogLevel 1 from command line.")]
[DataRow("--LogLevel 2", DisplayName = "LogLevel 2 from command line.")]
[DataRow("--LogLevel Trace", DisplayName = "LogLevel Trace from command line.")]
[DataRow("--LogLevel Debug", DisplayName = "LogLevel Debug from command line.")]
[DataRow("--LogLevel Information", DisplayName = "LogLevel Information from command line.")]
[DataRow("--LogLevel tRace", DisplayName = "Case sensitivity: LogLevel Trace from command line.")]
[DataRow("--LogLevel DebUG", DisplayName = "Case sensitivity: LogLevel Debug from command line.")]
[DataRow("--LogLevel information", DisplayName = "Case sensitivity: LogLevel Information from command line.")]
[DataRow("--log-level 0", DisplayName = "LogLevel 0 from command line.")]
[DataRow("--log-level 1", DisplayName = "LogLevel 1 from command line.")]
[DataRow("--log-level 2", DisplayName = "LogLevel 2 from command line.")]
[DataRow("--log-level Trace", DisplayName = "LogLevel Trace from command line.")]
[DataRow("--log-level Debug", DisplayName = "LogLevel Debug from command line.")]
[DataRow("--log-level Information", DisplayName = "LogLevel Information from command line.")]
[DataRow("--log-level tRace", DisplayName = "Case sensitivity: LogLevel Trace from command line.")]
[DataRow("--log-level DebUG", DisplayName = "Case sensitivity: LogLevel Debug from command line.")]
[DataRow("--log-level information", DisplayName = "Case sensitivity: LogLevel Information from command line.")]
[DataRow("--LogLevel 0", DisplayName = "Case sensitivity: LogLevel legacy Information from command line.")]
[DataRow("--LogLevel information", DisplayName = "Case sensitivity: LogLevel legacy Information from command line.")]
Comment on lines +833 to +835
public void TestEngineStartUpWithVerboseAndLogLevelOptions(string logLevelOption)
{
_fileSystem!.File.WriteAllText(TEST_RUNTIME_CONFIG_FILE, INITIAL_CONFIG);
Expand All @@ -850,7 +852,7 @@ public void TestEngineStartUpWithVerboseAndLogLevelOptions(string logLevelOption
}

/// <summary>
/// Test to validate that the engine starts successfully when --LogLevel is set to Warning
/// Test to validate that the engine starts successfully when --log-level is set to Warning
/// or above. At these levels, CLI phase messages (logged at Information) are suppressed,
/// so no stdout output with message 'info' is expected during the CLI phase.
/// </summary>
Expand All @@ -871,7 +873,7 @@ public async Task TestEngineStartUpWithHighLogLevelOptions(string logLevelOption
StringWriter consoleOutput = new();
Console.SetOut(consoleOutput);

string[] args = { "start", "--config", TEST_RUNTIME_CONFIG_FILE, "--LogLevel", logLevelOption };
string[] args = { "start", "--config", TEST_RUNTIME_CONFIG_FILE, "--log-level", logLevelOption };
_fileSystem!.File.WriteAllText(TEST_RUNTIME_CONFIG_FILE, INITIAL_CONFIG);

// Run Program.Execute on a background task because StartEngine blocks until the host shuts down.
Expand All @@ -886,7 +888,7 @@ public async Task TestEngineStartUpWithHighLogLevelOptions(string logLevelOption
}

/// <summary>
/// Test to validate that the engine starts successfully when --LogLevel is set to None.
/// Test to validate that the engine starts successfully when --log-level is set to None.
/// At these levels, CLI phase messages (logged at Information) are suppressed,
/// so no stdout output is expected during the CLI phase.
/// </summary>
Expand All @@ -901,7 +903,7 @@ public async Task TestEngineStartUpWithLogLevelNone(string logLevelOption)
StringWriter consoleOutput = new();
Console.SetOut(consoleOutput);

string[] args = { "start", "--config", TEST_RUNTIME_CONFIG_FILE, "--LogLevel", logLevelOption };
string[] args = { "start", "--config", TEST_RUNTIME_CONFIG_FILE, "--log-level", logLevelOption };
_fileSystem!.File.WriteAllText(TEST_RUNTIME_CONFIG_FILE, INITIAL_CONFIG);

// Run Program.Execute on a background task because StartEngine blocks until the host shuts down.
Expand All @@ -915,12 +917,12 @@ public async Task TestEngineStartUpWithLogLevelNone(string logLevelOption)
}

/// Validates that `dab start` correctly sets <see cref="Startup.IsCliOverriding"/>
/// based on whether the --LogLevel CLI flag is provided.
/// based on whether the --log-level CLI flag is provided.
///
/// When the --LogLevel flag is provided, IsCliOverriding should be true.
/// When the --LogLevel flag is omitted (log level comes from the config file), IsCliOverriding should be false.
/// When the --log-level flag is provided, IsCliOverriding should be true.
/// When the --log-level flag is omitted (log level comes from the config file), IsCliOverriding should be false.
/// </summary>
/// <param name="cliLogLevel">The --LogLevel CLI flag value, or null to omit the flag.</param>
/// <param name="cliLogLevel">The --log-level CLI flag value, or null to omit the flag.</param>
/// <param name="expectedIsOverridden">Expected value of Startup.IsCliOverriding.</param>
[DataTestMethod]
[DataRow(null, false, DisplayName = "IsCliOverriding is false")]
Expand Down Expand Up @@ -975,6 +977,7 @@ public async Task TestStartCommandResolvesLogLevelFromConfigOrFlag(
StartOptions options = new(
verbose: false,
logLevel: cliLogLevel,
logLevelLegacy: null,
isHttpsRedirectionDisabled: false,
mcpStdio: false,
mcpRole: null,
Expand Down
8 changes: 6 additions & 2 deletions src/Cli/Commands/StartOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@ public class StartOptions : Options

public LogBuffer CliBuffer { get; }

public StartOptions(bool verbose, LogLevel? logLevel, bool isHttpsRedirectionDisabled, bool mcpStdio, string? mcpRole, string config)
public StartOptions(bool verbose, LogLevel? logLevel, LogLevel? logLevelLegacy, bool isHttpsRedirectionDisabled, bool mcpStdio, string? mcpRole, string config)
: base(config)
{
// When verbose is true we set LogLevel to information.
LogLevel = verbose is true ? Microsoft.Extensions.Logging.LogLevel.Information : logLevel;
LogLevelLegacy = logLevelLegacy;
IsHttpsRedirectionDisabled = isHttpsRedirectionDisabled;
McpStdio = mcpStdio;
McpRole = mcpRole;
Expand All @@ -37,9 +38,12 @@ public StartOptions(bool verbose, LogLevel? logLevel, bool isHttpsRedirectionDis
[Option("verbose", SetName = "verbose", Required = false, HelpText = "Specifies logging level as informational.")]
public bool Verbose { get; }

[Option("LogLevel", SetName = "LogLevel", Required = false, HelpText = LOGLEVEL_HELPTEXT)]
[Option("log-level", SetName = "loglevel", Required = false, HelpText = LOGLEVEL_HELPTEXT)]
public LogLevel? LogLevel { get; }

[Option("LogLevel", SetName = "LogLevel", Required = false, HelpText = LOGLEVEL_HELPTEXT, Hidden = true)]
public LogLevel? LogLevelLegacy { get; }
Comment on lines +41 to +45

[Option("no-https-redirect", Required = false, HelpText = "Disables automatic https redirects.")]
public bool IsHttpsRedirectionDisabled { get; }

Expand Down
27 changes: 19 additions & 8 deletions src/Cli/ConfigGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3066,10 +3066,10 @@ public static bool TryStartEngineWithOptions(StartOptions options, FileSystemRun
List<string> args = new()
{ "--ConfigFileName", runtimeConfigFile };

/// Add arguments for LogLevel. Only pass --LogLevel when user explicitly specified it,
/// Add arguments for LogLevel. Only pass --log-level when user explicitly specified it,
/// so that MCP logging/setLevel can still adjust the level when no CLI override is present.
///
/// When --LogLevel is NOT specified:
/// When --log-level is NOT specified:
/// - MCP stdio mode: Service defaults to None for clean stdout output
/// - Non-MCP mode: Service defaults to Debug (Development) or Error (Production) based on config
LogLevel minimumLogLevel;
Expand All @@ -3079,19 +3079,30 @@ public static bool TryStartEngineWithOptions(StartOptions options, FileSystemRun
Utils.IsConfigOverriding = false;
Utils.ConfigLogLevel = LogLevel.Information;

LogLevel? logLevel = null;
if (options.LogLevel is not null)
{
if (options.LogLevel is < LogLevel.Trace or > LogLevel.None)
logLevel = options.LogLevel;
}
else if (options.LogLevelLegacy is not null)
{
options.CliBuffer.BufferLog(LogLevel.Warning, $"--LogLevel is deprecated, please use --log-level instead.");
logLevel = options.LogLevelLegacy;
}

if (logLevel is not null)
{
if (logLevel is < LogLevel.Trace or > LogLevel.None)
{
options.CliBuffer.BufferLog(LogLevel.Error,
$"LogLevel's valid range is 0 to 6, your value: {options.LogLevel}, see: https://learn.microsoft.com/dotnet/api/microsoft.extensions.logging.loglevel");
$"LogLevel's valid range is 0 to 6, your value: {logLevel}, see: https://learn.microsoft.com/dotnet/api/microsoft.extensions.logging.loglevel");
return false;
}

minimumLogLevel = (LogLevel)options.LogLevel;
// Only add --LogLevel when user explicitly specified it via CLI.
minimumLogLevel = (LogLevel)logLevel;
// Only add --log-level when user explicitly specified it via CLI.
// This allows MCP logging/setLevel to work when no CLI override is present.
args.Add("--LogLevel");
args.Add("--log-level");
args.Add(minimumLogLevel.ToString());
}
else
Expand All @@ -3100,7 +3111,7 @@ public static bool TryStartEngineWithOptions(StartOptions options, FileSystemRun

// Track whether config explicitly set a log level. In MCP stdio mode this
// allows CLI logs to be emitted to stderr (instead of being suppressed)
// when the user expressed intent via the config file rather than --LogLevel.
// when the user expressed intent via the config file rather than --log-level.
if (deserializedRuntimeConfig.HasExplicitLogLevel())
{
Utils.IsConfigOverriding = true;
Expand Down
8 changes: 4 additions & 4 deletions src/Cli/CustomLoggerProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ public class CustomConsoleLogger : ILogger
private readonly LogLevel _minimumLogLevel;

// Minimum LogLevel for CLI output.
// For MCP mode: prefer CLI's --LogLevel, fall back to config's log-level, otherwise suppress all.
// For MCP mode: prefer CLI's --log-level, fall back to config's log-level, otherwise suppress all.
// For non-MCP mode: always use the level passed to the constructor.
// Note: --LogLevel is meant for the ENGINE's log level, not CLI's output.
// Note: --log-level is meant for the ENGINE's log level, not CLI's output.
public CustomConsoleLogger(LogLevel minimumLogLevel = LogLevel.Information)
{
_minimumLogLevel = Cli.Utils.IsMcpStdioMode
Expand Down Expand Up @@ -93,13 +93,13 @@ public CustomConsoleLogger(LogLevel minimumLogLevel = LogLevel.Information)
/// <summary>
/// Creates Log message by setting console message color based on LogLevel.
/// In MCP stdio mode:
/// - If user explicitly set --LogLevel (CLI) or log-level (config): write to stderr (colored output)
/// - If user explicitly set --log-level (CLI) or log-level (config): write to stderr (colored output)
/// - Otherwise: suppress entirely to keep stdout clean for JSON-RPC protocol.
/// </summary>
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
// In MCP stdio mode, only output logs if user explicitly requested a log level
// via either the CLI --LogLevel flag or the runtime config file's log-level.
// via either the CLI --log-level flag or the runtime config file's log-level.
// In that case, write to stderr to keep stdout clean for JSON-RPC.
if (Cli.Utils.IsMcpStdioMode)
{
Expand Down
1 change: 1 addition & 0 deletions src/Cli/Exporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ private static async Task ExportGraphQL(
StartOptions startOptions = new(
verbose: false,
logLevel: LogLevel.None,
logLevelLegacy: null,
isHttpsRedirectionDisabled: false,
config: options.Config!,
mcpStdio: false,
Expand Down
2 changes: 1 addition & 1 deletion src/Cli/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ private static void ParseEarlyFlags(string[] args)
{
Utils.IsMcpStdioMode = true;
}
else if (string.Equals(arg, "--LogLevel", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
else if (string.Equals(arg, "--log-level", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
{
Utils.IsCliOverriding = true;
if (Enum.TryParse<LogLevel>(args[i + 1], ignoreCase: true, out LogLevel cliLogLevel))
Expand Down
6 changes: 3 additions & 3 deletions src/Cli/Utils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,21 +29,21 @@ public class Utils
public static bool IsMcpStdioMode { get; set; }

/// <summary>
/// When true, the CLI is the source overriding the log level (i.e., <c>--LogLevel</c> was supplied).
/// When true, the CLI is the source overriding the log level (i.e., <c>--log-level</c> was supplied).
/// This allows logs to be written to stderr instead of being completely suppressed.
/// </summary>
public static bool IsCliOverriding { get; set; }

/// <summary>
/// The log level specified via CLI --LogLevel flag.
/// The log level specified via CLI --log-level flag.
/// Only valid when IsCliOverriding is true.
/// </summary>
public static LogLevel CliLogLevel { get; set; } = LogLevel.Information;

/// <summary>
/// When true, the runtime config is the source overriding the log level
/// (i.e., <c>runtime.telemetry.log-level</c> was explicitly set).
/// This allows CLI logs to be written to stderr in MCP mode even when no --LogLevel flag was provided.
/// This allows CLI logs to be written to stderr in MCP mode even when no --log-level flag was provided.
/// </summary>
public static bool IsConfigOverriding { get; set; }

Expand Down
4 changes: 2 additions & 2 deletions src/Core/Telemetry/ILogLevelController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public interface ILogLevelController
{
/// <summary>
/// Gets a value indicating whether the CLI is the source overriding the log level
/// (i.e., <c>--LogLevel</c> was supplied). When true, runtime-config (hot-reload)
/// (i.e., <c>--log-level</c> was supplied). When true, runtime-config (hot-reload)
/// updates are ignored.
/// </summary>
bool IsCliOverriding { get; }
Expand All @@ -35,7 +35,7 @@ public interface ILogLevelController
/// The MCP level string is mapped to the appropriate LogLevel.
/// Log-level precedence (highest to lowest):
/// 1. Agent (MCP <c>logging/setLevel</c>) — always wins.
/// 2. CLI <c>--LogLevel</c> flag.
/// 2. CLI <c>--log-level</c> flag.
/// 3. Config <c>runtime.telemetry.log-level</c>.
/// 4. Defaults.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ public void UpdateFromRuntimeConfig_RespectsAgentOverride()

/// <summary>
/// Hot-reloading the runtime config must not overwrite a CLI-set level. The CLI
/// <c>--LogLevel</c> flag is the operator's deliberate startup choice, so a
/// <c>--log-level</c> flag is the operator's deliberate startup choice, so a
/// subsequent <see cref="DynamicLogLevelProvider.UpdateFromRuntimeConfig"/> with a
/// different config-pinned level must be ignored.
/// </summary>
Expand Down
Loading
Loading