From c5b7d20e6eec539e39725b8dfa6096219715046f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 12:02:55 +0000 Subject: [PATCH 1/8] Initial plan From cba32defb0dac86844a1c745d9be26cf1c2496b2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 12:10:24 +0000 Subject: [PATCH 2/8] Replace --log-for with --output text|json for structured output - Rename LogFor enum (Human/Machine) to OutputFormat (Text/Json) in IProxyConfiguration - Rename --log-for CLI option to --output with text|json values - Update ProxyConfiguration, DevProxyCommand, DevProxyConfigOptions, ILoggingBuilderExtensions, ProxyEngine - Add JSON output support to OutdatedCommand - Add JSON output support to ConfigCommand (config get, config new) - Add output property to v2.2.0 rc.schema.json Co-authored-by: waldekmastykarz <11164679+waldekmastykarz@users.noreply.github.com> --- .../Proxy/IProxyConfiguration.cs | 12 ++--- DevProxy/Commands/ConfigCommand.cs | 38 ++++++++++++-- DevProxy/Commands/DevProxyCommand.cs | 22 ++++---- DevProxy/Commands/DevProxyConfigOptions.cs | 10 ++-- DevProxy/Commands/OutdatedCommand.cs | 51 ++++++++++++++++++- .../Extensions/ILoggingBuilderExtensions.cs | 10 ++-- DevProxy/Proxy/ProxyConfiguration.cs | 2 +- DevProxy/Proxy/ProxyEngine.cs | 4 +- schemas/v2.2.0/rc.schema.json | 8 +++ 9 files changed, 120 insertions(+), 37 deletions(-) diff --git a/DevProxy.Abstractions/Proxy/IProxyConfiguration.cs b/DevProxy.Abstractions/Proxy/IProxyConfiguration.cs index 59f1d6d0..a4b08687 100644 --- a/DevProxy.Abstractions/Proxy/IProxyConfiguration.cs +++ b/DevProxy.Abstractions/Proxy/IProxyConfiguration.cs @@ -18,12 +18,12 @@ public enum ReleaseType Beta } -public enum LogFor +public enum OutputFormat { - [EnumMember(Value = "human")] - Human, - [EnumMember(Value = "machine")] - Machine + [EnumMember(Value = "text")] + Text, + [EnumMember(Value = "json")] + Json } public interface IProxyConfiguration @@ -37,7 +37,7 @@ public interface IProxyConfiguration IEnumerable? FilterByHeaders { get; } bool InstallCert { get; set; } string? IPAddress { get; set; } - LogFor LogFor { get; set; } + OutputFormat Output { get; set; } LogLevel LogLevel { get; } ReleaseType NewVersionNotification { get; } bool NoFirstRun { get; set; } diff --git a/DevProxy/Commands/ConfigCommand.cs b/DevProxy/Commands/ConfigCommand.cs index 2e9499be..f32a9713 100644 --- a/DevProxy/Commands/ConfigCommand.cs +++ b/DevProxy/Commands/ConfigCommand.cs @@ -70,9 +70,10 @@ private void ConfigureCommand() configGetCommand.SetAction(async (parseResult) => { var configId = parseResult.GetValue(configIdArgument); + var outputFormat = parseResult.GetValueOrDefault(DevProxyCommand.OutputOptionName) ?? OutputFormat.Text; if (configId != null) { - await DownloadConfigAsync(configId); + await DownloadConfigAsync(configId, outputFormat); } }); @@ -87,7 +88,8 @@ private void ConfigureCommand() configNewCommand.SetAction(async (parseResult) => { var name = parseResult.GetValue(nameArgument) ?? "devproxyrc.json"; - await CreateConfigFileAsync(name); + var outputFormat = parseResult.GetValueOrDefault(DevProxyCommand.OutputOptionName) ?? OutputFormat.Text; + await CreateConfigFileAsync(name, outputFormat); }); var configOpenCommand = new Command("open", "Open devproxyrc.json"); @@ -108,7 +110,7 @@ private void ConfigureCommand() }.OrderByName()); } - private async Task DownloadConfigAsync(string configId) + private async Task DownloadConfigAsync(string configId, OutputFormat outputFormat) { try { @@ -148,6 +150,20 @@ private async Task DownloadConfigAsync(string configId) _logger.LogInformation("Config saved in {TargetFolderPath}\r\n", targetFolderPath); var configInfo = GetConfigInfo(targetFolderPath); + + if (outputFormat == OutputFormat.Json) + { + var json = JsonSerializer.Serialize(new + { + configId, + path = targetFolderPath, + configFiles = configInfo.ConfigFiles, + mockFiles = configInfo.MockFiles + }, ProxyUtils.JsonSerializerOptions); + Console.WriteLine(json); + return; + } + if (!configInfo.ConfigFiles.Any() && !configInfo.MockFiles.Any()) { return; @@ -320,7 +336,7 @@ private async Task DownloadFileAsync(string filePath, string targetFolderPath, s } } - private async Task CreateConfigFileAsync(string name) + private async Task CreateConfigFileAsync(string name, OutputFormat outputFormat) { try { @@ -345,7 +361,19 @@ private async Task CreateConfigFileAsync(string name) var snippetBody = GetSnippetBody(snippet.Body); var targetFileName = GetTargetFileName(name); await File.WriteAllTextAsync(targetFileName, snippetBody); - _logger.LogInformation("Config file created at {TargetFileName}", targetFileName); + + if (outputFormat == OutputFormat.Json) + { + var json = JsonSerializer.Serialize(new + { + path = targetFileName + }, ProxyUtils.JsonSerializerOptions); + Console.WriteLine(json); + } + else + { + _logger.LogInformation("Config file created at {TargetFileName}", targetFileName); + } } catch (Exception ex) { diff --git a/DevProxy/Commands/DevProxyCommand.cs b/DevProxy/Commands/DevProxyCommand.cs index 1740df9f..44c76b62 100644 --- a/DevProxy/Commands/DevProxyCommand.cs +++ b/DevProxy/Commands/DevProxyCommand.cs @@ -37,7 +37,7 @@ sealed class DevProxyCommand : RootCommand internal const string TimeoutOptionName = "--timeout"; internal const string DiscoverOptionName = "--discover"; internal const string EnvOptionName = "--env"; - internal const string LogForOptionName = "--log-for"; + internal const string OutputOptionName = "--output"; internal const string DetachedOptionName = "--detach"; internal const string InternalDaemonOptionName = "--_internal-daemon"; @@ -449,21 +449,21 @@ private void ConfigureCommand() } }); - var logForOption = new Option(LogForOptionName) + var outputOption = new Option(OutputOptionName) { - Description = $"Target audience for log output. Allowed values: {string.Join(", ", Enum.GetNames())}", - HelpName = "log-for", + Description = $"Output format. Allowed values: {string.Join(", ", Enum.GetNames())}", + HelpName = "format", Recursive = true }; - logForOption.Validators.Add(input => + outputOption.Validators.Add(input => { if (input.Tokens.Count == 0) { return; } - if (!Enum.TryParse(input.Tokens[0].Value, true, out _)) + if (!Enum.TryParse(input.Tokens[0].Value, true, out _)) { - input.AddError($"{input.Tokens[0].Value} is not a valid log-for value. Allowed values are: {string.Join(", ", Enum.GetNames())}"); + input.AddError($"{input.Tokens[0].Value} is not a valid output format. Allowed values are: {string.Join(", ", Enum.GetNames())}"); } }); @@ -489,9 +489,9 @@ private void ConfigureCommand() installCertOption, internalDaemonOption, ipAddressOption, - logForOption, logLevelOption, noFirstRunOption, + outputOption, portOption, recordOption, timeoutOption, @@ -593,10 +593,10 @@ private void ConfigureFromOptions(ParseResult parseResult) : new KeyValuePair(parts[0], parts[1]); }).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); } - var logFor = parseResult.GetValueOrDefault(LogForOptionName); - if (logFor is not null) + var output = parseResult.GetValueOrDefault(OutputOptionName); + if (output is not null) { - _proxyConfiguration.LogFor = logFor.Value; + _proxyConfiguration.Output = output.Value; } } diff --git a/DevProxy/Commands/DevProxyConfigOptions.cs b/DevProxy/Commands/DevProxyConfigOptions.cs index 5601c04a..da68c6d4 100644 --- a/DevProxy/Commands/DevProxyConfigOptions.cs +++ b/DevProxy/Commands/DevProxyConfigOptions.cs @@ -23,7 +23,7 @@ public string? ConfigFile public bool Discover => _parseResult?.GetValueOrDefault(DevProxyCommand.DiscoverOptionName) ?? false; public string? IPAddress => _parseResult?.GetValueOrDefault(DevProxyCommand.IpAddressOptionName); public bool IsStdioMode => _parseResult?.CommandResult.Command.Name == "stdio"; - public LogFor? LogFor => _parseResult?.GetValueOrDefault(DevProxyCommand.LogForOptionName); + public OutputFormat? Output => _parseResult?.GetValueOrDefault(DevProxyCommand.OutputOptionName); public LogLevel? LogLevel => _parseResult?.GetValueOrDefault(DevProxyCommand.LogLevelOptionName); public List? UrlsToWatch @@ -119,7 +119,7 @@ public DevProxyConfigOptions() } }; - var logForOption = new Option(DevProxyCommand.LogForOptionName) + var outputOption = new Option(DevProxyCommand.OutputOptionName) { CustomParser = result => { @@ -128,9 +128,9 @@ public DevProxyConfigOptions() return null; } - if (Enum.TryParse(result.Tokens[0].Value, true, out var logFor)) + if (Enum.TryParse(result.Tokens[0].Value, true, out var output)) { - return logFor; + return output; } return null; @@ -152,8 +152,8 @@ public DevProxyConfigOptions() configFileOption, portOption, urlsToWatchOption, - logForOption, logLevelOption, + outputOption, discoverOption }; this.AddOptions(options.OrderByName()); diff --git a/DevProxy/Commands/OutdatedCommand.cs b/DevProxy/Commands/OutdatedCommand.cs index 0cd435f9..652aac27 100644 --- a/DevProxy/Commands/OutdatedCommand.cs +++ b/DevProxy/Commands/OutdatedCommand.cs @@ -3,7 +3,10 @@ // See the LICENSE file in the project root for more information. using DevProxy.Abstractions.Proxy; +using DevProxy.Abstractions.Utils; using System.CommandLine; +using System.CommandLine.Parsing; +using System.Text.Json; namespace DevProxy.Commands; @@ -36,14 +39,21 @@ private void ConfigureCommand() SetAction(async (parseResult) => { var versionOnly = parseResult.GetValue(outdatedShortOption); - await CheckVersionAsync(versionOnly); + var outputFormat = parseResult.GetValueOrDefault(DevProxyCommand.OutputOptionName) ?? OutputFormat.Text; + await CheckVersionAsync(versionOnly, outputFormat); }); } - private async Task CheckVersionAsync(bool versionOnly) + private async Task CheckVersionAsync(bool versionOnly, OutputFormat outputFormat) { var releaseInfo = await _updateNotification.CheckForNewVersionAsync(_proxyConfiguration.NewVersionNotification); + if (outputFormat == OutputFormat.Json) + { + WriteJsonOutput(releaseInfo, versionOnly); + return; + } + if (releaseInfo is not null && releaseInfo.Version is not null) { var isBeta = releaseInfo.Version.Contains("-beta", StringComparison.OrdinalIgnoreCase); @@ -69,4 +79,41 @@ private async Task CheckVersionAsync(bool versionOnly) _logger.LogInformation("You are using the latest version of Dev Proxy."); } } + + private static void WriteJsonOutput(ReleaseInfo? releaseInfo, bool versionOnly) + { + if (releaseInfo is not null && releaseInfo.Version is not null) + { + var isBeta = releaseInfo.Version.Contains("-beta", StringComparison.OrdinalIgnoreCase); + var notesLink = isBeta ? "https://aka.ms/devproxy/notes" : "https://aka.ms/devproxy/beta/notes"; + + if (versionOnly) + { + var json = JsonSerializer.Serialize(new + { + version = releaseInfo.Version + }, ProxyUtils.JsonSerializerOptions); + Console.WriteLine(json); + } + else + { + var json = JsonSerializer.Serialize(new + { + version = releaseInfo.Version, + current = ProxyUtils.ProductVersion, + releaseNotes = notesLink, + upgradeUrl = "https://aka.ms/devproxy/upgrade" + }, ProxyUtils.JsonSerializerOptions); + Console.WriteLine(json); + } + } + else + { + var json = JsonSerializer.Serialize(new + { + current = ProxyUtils.ProductVersion + }, ProxyUtils.JsonSerializerOptions); + Console.WriteLine(json); + } + } } \ No newline at end of file diff --git a/DevProxy/Extensions/ILoggingBuilderExtensions.cs b/DevProxy/Extensions/ILoggingBuilderExtensions.cs index 011451c1..ede6cabd 100644 --- a/DevProxy/Extensions/ILoggingBuilderExtensions.cs +++ b/DevProxy/Extensions/ILoggingBuilderExtensions.cs @@ -27,9 +27,9 @@ public static ILoggingBuilder ConfigureDevProxyLogging( var configuredLogLevel = options.LogLevel ?? configuration.GetValue("logLevel", LogLevel.Information); - // Determine the log target audience (human or machine) - var configuredLogFor = options.LogFor ?? - configuration.GetValue("logFor", LogFor.Human); + // Determine the output format (text or json) + var configuredOutput = options.Output ?? + configuration.GetValue("output", OutputFormat.Text); // For stdio command, log to file instead of console to avoid interfering with proxied streams if (DevProxyCommand.IsStdioCommand) @@ -56,8 +56,8 @@ public static ILoggingBuilder ConfigureDevProxyLogging( var showSkipMessages = configuration.GetValue("showSkipMessages", true); var showTimestamps = configuration.GetValue("showTimestamps", true); - // Select the appropriate formatter based on logFor setting - var formatterName = configuredLogFor == LogFor.Machine + // Select the appropriate formatter based on output setting + var formatterName = configuredOutput == OutputFormat.Json ? MachineConsoleFormatter.FormatterName : ProxyConsoleFormatter.DefaultCategoryName; diff --git a/DevProxy/Proxy/ProxyConfiguration.cs b/DevProxy/Proxy/ProxyConfiguration.cs index 381fcc83..6f6bc095 100755 --- a/DevProxy/Proxy/ProxyConfiguration.cs +++ b/DevProxy/Proxy/ProxyConfiguration.cs @@ -34,7 +34,7 @@ public string ConfigFile public string? IPAddress { get; set; } = "127.0.0.1"; public bool InstallCert { get; set; } = true; [JsonConverter(typeof(JsonStringEnumConverter))] - public LogFor LogFor { get; set; } = LogFor.Human; + public OutputFormat Output { get; set; } = OutputFormat.Text; [JsonConverter(typeof(JsonStringEnumConverter))] public LogLevel LogLevel { get; set; } = LogLevel.Information; public bool NoFirstRun { get; set; } diff --git a/DevProxy/Proxy/ProxyEngine.cs b/DevProxy/Proxy/ProxyEngine.cs index 851e74cb..479c6e2b 100755 --- a/DevProxy/Proxy/ProxyEngine.cs +++ b/DevProxy/Proxy/ProxyEngine.cs @@ -174,7 +174,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) !DevProxyCommand.IsInternalDaemon && Environment.GetEnvironmentVariable("CI") is null; - if (_config.LogFor == LogFor.Machine) + if (_config.Output == OutputFormat.Json) { // Always print API instructions in machine mode // since LLMs/agents can use the API even in non-interactive mode @@ -266,7 +266,7 @@ private async Task ReadKeysAsync(CancellationToken cancellationToken) break; case ConsoleKey.C: Console.Clear(); - if (_config.LogFor == LogFor.Machine) + if (_config.Output == OutputFormat.Json) { PrintApiInstructions(_config); } diff --git a/schemas/v2.2.0/rc.schema.json b/schemas/v2.2.0/rc.schema.json index daa86c54..812be54f 100644 --- a/schemas/v2.2.0/rc.schema.json +++ b/schemas/v2.2.0/rc.schema.json @@ -85,6 +85,14 @@ ], "description": "The minimum log level for Dev Proxy output." }, + "output": { + "type": "string", + "enum": [ + "text", + "json" + ], + "description": "Output format. Use 'text' for readable console output (default), or 'json' for structured JSON Lines output suitable for LLMs and agents." + }, "newVersionNotification": { "type": "string", "enum": [ From d7908f3bfbf44bc3feef0fb25a5f933144097968 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 13:08:05 +0000 Subject: [PATCH 3/8] Suppress logger progress messages in JSON mode for config get Move progress messages to text-mode only and JSON output check before text-only messages, so --output json produces clean structured output without mixed logger messages. Co-authored-by: waldekmastykarz <11164679+waldekmastykarz@users.noreply.github.com> --- DevProxy/Commands/ConfigCommand.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/DevProxy/Commands/ConfigCommand.cs b/DevProxy/Commands/ConfigCommand.cs index f32a9713..f66914c1 100644 --- a/DevProxy/Commands/ConfigCommand.cs +++ b/DevProxy/Commands/ConfigCommand.cs @@ -135,7 +135,10 @@ private async Task DownloadConfigAsync(string configId, OutputFormat outputForma _logger.LogDebug("Creating target folder {TargetFolderPath}...", targetFolderPath); _ = Directory.CreateDirectory(targetFolderPath); - _logger.LogInformation("Downloading config {ConfigId}...", configId); + if (outputFormat == OutputFormat.Text) + { + _logger.LogInformation("Downloading config {ConfigId}...", configId); + } var sampleFiles = await GetFilesToDownloadAsync(configId); if (sampleFiles.Length == 0) @@ -148,7 +151,6 @@ private async Task DownloadConfigAsync(string configId, OutputFormat outputForma await DownloadFileAsync(sampleFile, targetFolderPath, configId); } - _logger.LogInformation("Config saved in {TargetFolderPath}\r\n", targetFolderPath); var configInfo = GetConfigInfo(targetFolderPath); if (outputFormat == OutputFormat.Json) @@ -164,6 +166,8 @@ private async Task DownloadConfigAsync(string configId, OutputFormat outputForma return; } + _logger.LogInformation("Config saved in {TargetFolderPath}\r\n", targetFolderPath); + if (!configInfo.ConfigFiles.Any() && !configInfo.MockFiles.Any()) { return; From ce6dcc2c3dc282abc3a84002933feb2f0bae1c5a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 13:28:48 +0000 Subject: [PATCH 4/8] Route JSON subcommand output through logger with StructuredOutput EventId Co-authored-by: waldekmastykarz <11164679+waldekmastykarz@users.noreply.github.com> --- DevProxy/Commands/ConfigCommand.cs | 4 ++-- DevProxy/Commands/OutdatedCommand.cs | 8 ++++---- DevProxy/Logging/ILoggerExtensions.cs | 9 +++++++++ DevProxy/Logging/LogEvents.cs | 15 +++++++++++++++ DevProxy/Logging/MachineConsoleFormatter.cs | 8 ++++++++ 5 files changed, 38 insertions(+), 6 deletions(-) create mode 100644 DevProxy/Logging/LogEvents.cs diff --git a/DevProxy/Commands/ConfigCommand.cs b/DevProxy/Commands/ConfigCommand.cs index f66914c1..35d80654 100644 --- a/DevProxy/Commands/ConfigCommand.cs +++ b/DevProxy/Commands/ConfigCommand.cs @@ -162,7 +162,7 @@ private async Task DownloadConfigAsync(string configId, OutputFormat outputForma configFiles = configInfo.ConfigFiles, mockFiles = configInfo.MockFiles }, ProxyUtils.JsonSerializerOptions); - Console.WriteLine(json); + _logger.LogStructuredOutput(json); return; } @@ -372,7 +372,7 @@ private async Task CreateConfigFileAsync(string name, OutputFormat outputFormat) { path = targetFileName }, ProxyUtils.JsonSerializerOptions); - Console.WriteLine(json); + _logger.LogStructuredOutput(json); } else { diff --git a/DevProxy/Commands/OutdatedCommand.cs b/DevProxy/Commands/OutdatedCommand.cs index 652aac27..fb11609d 100644 --- a/DevProxy/Commands/OutdatedCommand.cs +++ b/DevProxy/Commands/OutdatedCommand.cs @@ -80,7 +80,7 @@ private async Task CheckVersionAsync(bool versionOnly, OutputFormat outputFormat } } - private static void WriteJsonOutput(ReleaseInfo? releaseInfo, bool versionOnly) + private void WriteJsonOutput(ReleaseInfo? releaseInfo, bool versionOnly) { if (releaseInfo is not null && releaseInfo.Version is not null) { @@ -93,7 +93,7 @@ private static void WriteJsonOutput(ReleaseInfo? releaseInfo, bool versionOnly) { version = releaseInfo.Version }, ProxyUtils.JsonSerializerOptions); - Console.WriteLine(json); + _logger.LogStructuredOutput(json); } else { @@ -104,7 +104,7 @@ private static void WriteJsonOutput(ReleaseInfo? releaseInfo, bool versionOnly) releaseNotes = notesLink, upgradeUrl = "https://aka.ms/devproxy/upgrade" }, ProxyUtils.JsonSerializerOptions); - Console.WriteLine(json); + _logger.LogStructuredOutput(json); } } else @@ -113,7 +113,7 @@ private static void WriteJsonOutput(ReleaseInfo? releaseInfo, bool versionOnly) { current = ProxyUtils.ProductVersion }, ProxyUtils.JsonSerializerOptions); - Console.WriteLine(json); + _logger.LogStructuredOutput(json); } } } \ No newline at end of file diff --git a/DevProxy/Logging/ILoggerExtensions.cs b/DevProxy/Logging/ILoggerExtensions.cs index 53f1870c..f01bb077 100644 --- a/DevProxy/Logging/ILoggerExtensions.cs +++ b/DevProxy/Logging/ILoggerExtensions.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using DevProxy.Logging; + #pragma warning disable IDE0130 namespace Microsoft.Extensions.Logging; #pragma warning restore IDE0130 @@ -15,4 +17,11 @@ static class ILoggerExtensions { nameof(url), url }, { nameof(requestId), requestId } }); + + /// + /// Logs structured output (e.g., JSON) that should be written as-is + /// without any log envelope formatting. + /// + public static void LogStructuredOutput(this ILogger logger, string message) => + logger.Log(LogLevel.Information, LogEvents.StructuredOutput, "{Message}", message); } \ No newline at end of file diff --git a/DevProxy/Logging/LogEvents.cs b/DevProxy/Logging/LogEvents.cs new file mode 100644 index 00000000..a4c4ee1c --- /dev/null +++ b/DevProxy/Logging/LogEvents.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace DevProxy.Logging; + +static class LogEvents +{ + /// + /// EventId for structured output (e.g., JSON from subcommands). + /// When MachineConsoleFormatter sees this EventId, it writes the raw + /// message without wrapping it in a JSONL envelope. + /// + public static readonly EventId StructuredOutput = new(1, "StructuredOutput"); +} diff --git a/DevProxy/Logging/MachineConsoleFormatter.cs b/DevProxy/Logging/MachineConsoleFormatter.cs index ea36cc9a..9820dfbd 100644 --- a/DevProxy/Logging/MachineConsoleFormatter.cs +++ b/DevProxy/Logging/MachineConsoleFormatter.cs @@ -124,6 +124,14 @@ private static void WriteRegularLogMessage(in LogEntry logEntry, return; } + // Structured output (e.g., subcommand JSON) is written as-is + // without the JSONL envelope + if (logEntry.EventId == LogEvents.StructuredOutput) + { + textWriter.WriteLine(message); + return; + } + var requestId = GetRequestIdScope(scopeProvider); var category = logEntry.Category; From 85f57c8cb95e0be7ce8f2b24f6c18d3d230a2ac4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 13:33:20 +0000 Subject: [PATCH 5/8] Wrap structured output in consistent JSONL envelope with type "result" and data field Co-authored-by: waldekmastykarz <11164679+waldekmastykarz@users.noreply.github.com> --- DevProxy/Logging/LogEvents.cs | 5 +- DevProxy/Logging/MachineConsoleFormatter.cs | 54 +++++++++++++++++++-- 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/DevProxy/Logging/LogEvents.cs b/DevProxy/Logging/LogEvents.cs index a4c4ee1c..60f8eee6 100644 --- a/DevProxy/Logging/LogEvents.cs +++ b/DevProxy/Logging/LogEvents.cs @@ -8,8 +8,9 @@ static class LogEvents { /// /// EventId for structured output (e.g., JSON from subcommands). - /// When MachineConsoleFormatter sees this EventId, it writes the raw - /// message without wrapping it in a JSONL envelope. + /// When MachineConsoleFormatter sees this EventId, it wraps the message + /// in a consistent envelope with type "result" and a data field + /// containing the parsed JSON object. /// public static readonly EventId StructuredOutput = new(1, "StructuredOutput"); } diff --git a/DevProxy/Logging/MachineConsoleFormatter.cs b/DevProxy/Logging/MachineConsoleFormatter.cs index 9820dfbd..3dc934ea 100644 --- a/DevProxy/Logging/MachineConsoleFormatter.cs +++ b/DevProxy/Logging/MachineConsoleFormatter.cs @@ -124,11 +124,12 @@ private static void WriteRegularLogMessage(in LogEntry logEntry, return; } - // Structured output (e.g., subcommand JSON) is written as-is - // without the JSONL envelope + // Structured output (e.g., subcommand JSON) uses the same envelope + // format as log entries but with type "result" and a data field + // containing the parsed JSON object if (logEntry.EventId == LogEvents.StructuredOutput) { - textWriter.WriteLine(message); + WriteStructuredOutput(message, logEntry.Category, textWriter); return; } @@ -156,6 +157,45 @@ private static void WriteRegularLogMessage(in LogEntry logEntry, textWriter.WriteLine(json); } + private static void WriteStructuredOutput(string message, string category, TextWriter textWriter) + { + // Extract short category name + if (category is not null) + { + category = category[(category.LastIndexOf('.') + 1)..]; + } + + try + { + var data = JsonSerializer.Deserialize(message); + var logObject = new MachineResultEntry + { + Type = "result", + Data = data, + Category = category, + Timestamp = DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture) + }; + + var json = JsonSerializer.Serialize(logObject, _jsonOptions); + textWriter.WriteLine(json); + } + catch (JsonException) + { + // Fallback: if the message isn't valid JSON, write it as a + // regular log entry + var logObject = new MachineLogEntry + { + Type = "result", + Message = message, + Category = category, + Timestamp = DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture) + }; + + var json = JsonSerializer.Serialize(logObject, _jsonOptions); + textWriter.WriteLine(json); + } + } + private static string GetMessageTypeString(MessageType messageType) => _messageTypeStrings.TryGetValue(messageType, out var str) ? str : "unknown"; @@ -198,4 +238,12 @@ private sealed class MachineLogEntry public string? Timestamp { get; set; } public string? Exception { get; set; } } + + private sealed class MachineResultEntry + { + public string? Type { get; set; } + public JsonElement? Data { get; set; } + public string? Category { get; set; } + public string? Timestamp { get; set; } + } } From d849d8cd7e568c8affd7066898e0272b74f51ad2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 14:01:47 +0000 Subject: [PATCH 6/8] Rename MachineConsoleFormatter to JsonConsoleFormatter to reflect json output mode Co-authored-by: waldekmastykarz <11164679+waldekmastykarz@users.noreply.github.com> --- .../Extensions/ILoggingBuilderExtensions.cs | 6 ++--- ...leFormatter.cs => JsonConsoleFormatter.cs} | 22 +++++++++---------- DevProxy/Logging/LogEvents.cs | 2 +- 3 files changed, 15 insertions(+), 15 deletions(-) rename DevProxy/Logging/{MachineConsoleFormatter.cs => JsonConsoleFormatter.cs} (93%) diff --git a/DevProxy/Extensions/ILoggingBuilderExtensions.cs b/DevProxy/Extensions/ILoggingBuilderExtensions.cs index ede6cabd..89f7b296 100644 --- a/DevProxy/Extensions/ILoggingBuilderExtensions.cs +++ b/DevProxy/Extensions/ILoggingBuilderExtensions.cs @@ -58,7 +58,7 @@ public static ILoggingBuilder ConfigureDevProxyLogging( // Select the appropriate formatter based on output setting var formatterName = configuredOutput == OutputFormat.Json - ? MachineConsoleFormatter.FormatterName + ? JsonConsoleFormatter.FormatterName : ProxyConsoleFormatter.DefaultCategoryName; // For root command (proxy itself), use rich logging @@ -86,7 +86,7 @@ public static ILoggingBuilder ConfigureDevProxyLogging( formatterOptions.ShowTimestamps = showTimestamps; } ) - .AddConsoleFormatter(formatterOptions => + .AddConsoleFormatter(formatterOptions => { formatterOptions.IncludeScopes = true; formatterOptions.ShowSkipMessages = showSkipMessages; @@ -120,7 +120,7 @@ public static ILoggingBuilder ConfigureDevProxyLogging( formatterOptions.ShowTimestamps = showTimestamps; } ) - .AddConsoleFormatter(formatterOptions => + .AddConsoleFormatter(formatterOptions => { formatterOptions.IncludeScopes = true; formatterOptions.ShowSkipMessages = showSkipMessages; diff --git a/DevProxy/Logging/MachineConsoleFormatter.cs b/DevProxy/Logging/JsonConsoleFormatter.cs similarity index 93% rename from DevProxy/Logging/MachineConsoleFormatter.cs rename to DevProxy/Logging/JsonConsoleFormatter.cs index 3dc934ea..8ef39d75 100644 --- a/DevProxy/Logging/MachineConsoleFormatter.cs +++ b/DevProxy/Logging/JsonConsoleFormatter.cs @@ -13,9 +13,9 @@ namespace DevProxy.Logging; -sealed class MachineConsoleFormatter : ConsoleFormatter +sealed class JsonConsoleFormatter : ConsoleFormatter { - public const string FormatterName = "devproxy-machine"; + public const string FormatterName = "devproxy-json"; private static readonly JsonSerializerOptions _jsonOptions = new() { @@ -55,7 +55,7 @@ sealed class MachineConsoleFormatter : ConsoleFormatter private readonly ProxyConsoleFormatterOptions _options; private readonly HashSet _filteredMessageTypes; - public MachineConsoleFormatter(IOptions options) : base(FormatterName) + public JsonConsoleFormatter(IOptions options) : base(FormatterName) { Console.OutputEncoding = Encoding.UTF8; _options = options.Value; @@ -101,7 +101,7 @@ private void WriteRequestLog(RequestLog requestLog, string category, IExternalSc pluginName = pluginName[(pluginName.LastIndexOf('.') + 1)..]; } - var logObject = new MachineRequestLogEntry + var logObject = new JsonRequestLogEntry { Type = GetMessageTypeString(messageType), Message = requestLog.Message, @@ -142,7 +142,7 @@ private static void WriteRegularLogMessage(in LogEntry logEntry, category = category[(category.LastIndexOf('.') + 1)..]; } - var logObject = new MachineLogEntry + var logObject = new JsonLogEntry { Type = "log", Level = GetLogLevelString(logEntry.LogLevel), @@ -168,7 +168,7 @@ private static void WriteStructuredOutput(string message, string category, TextW try { var data = JsonSerializer.Deserialize(message); - var logObject = new MachineResultEntry + var logObject = new JsonResultEntry { Type = "result", Data = data, @@ -183,7 +183,7 @@ private static void WriteStructuredOutput(string message, string category, TextW { // Fallback: if the message isn't valid JSON, write it as a // regular log entry - var logObject = new MachineLogEntry + var logObject = new JsonLogEntry { Type = "result", Message = message, @@ -216,8 +216,8 @@ private static string GetLogLevelString(LogLevel logLevel) => return requestId; } - // JSON serialization models for machine output - private sealed class MachineRequestLogEntry + // JSON serialization models for JSON output + private sealed class JsonRequestLogEntry { public string? Type { get; set; } public string? Message { get; set; } @@ -228,7 +228,7 @@ private sealed class MachineRequestLogEntry public string? Timestamp { get; set; } } - private sealed class MachineLogEntry + private sealed class JsonLogEntry { public string? Type { get; set; } public string? Level { get; set; } @@ -239,7 +239,7 @@ private sealed class MachineLogEntry public string? Exception { get; set; } } - private sealed class MachineResultEntry + private sealed class JsonResultEntry { public string? Type { get; set; } public JsonElement? Data { get; set; } diff --git a/DevProxy/Logging/LogEvents.cs b/DevProxy/Logging/LogEvents.cs index 60f8eee6..dc6abc73 100644 --- a/DevProxy/Logging/LogEvents.cs +++ b/DevProxy/Logging/LogEvents.cs @@ -8,7 +8,7 @@ static class LogEvents { /// /// EventId for structured output (e.g., JSON from subcommands). - /// When MachineConsoleFormatter sees this EventId, it wraps the message + /// When JsonConsoleFormatter sees this EventId, it wraps the message /// in a consistent envelope with type "result" and a data field /// containing the parsed JSON object. /// From 4c0951e3c17fc6779c51c10cade9d06f7d17600a Mon Sep 17 00:00:00 2001 From: Waldek Mastykarz Date: Wed, 25 Feb 2026 15:33:58 +0100 Subject: [PATCH 7/8] Update DevProxy/Logging/ILoggerExtensions.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- DevProxy/Logging/ILoggerExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DevProxy/Logging/ILoggerExtensions.cs b/DevProxy/Logging/ILoggerExtensions.cs index f01bb077..2ba77907 100644 --- a/DevProxy/Logging/ILoggerExtensions.cs +++ b/DevProxy/Logging/ILoggerExtensions.cs @@ -23,5 +23,5 @@ static class ILoggerExtensions /// without any log envelope formatting. /// public static void LogStructuredOutput(this ILogger logger, string message) => - logger.Log(LogLevel.Information, LogEvents.StructuredOutput, "{Message}", message); + logger.Log(LogLevel.Information, LogEvents.StructuredOutput, message); } \ No newline at end of file From 61a72400ea99f6219b4f1c6a3a0734a72a2c3e5e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 14:39:18 +0000 Subject: [PATCH 8/8] Suppress error logs in JSON mode and fix CA2254 format string issue Co-authored-by: waldekmastykarz <11164679+waldekmastykarz@users.noreply.github.com> --- DevProxy/Commands/ConfigCommand.cs | 30 +++++++++++++++++++++------ DevProxy/Logging/ILoggerExtensions.cs | 2 +- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/DevProxy/Commands/ConfigCommand.cs b/DevProxy/Commands/ConfigCommand.cs index 35d80654..dfd9a085 100644 --- a/DevProxy/Commands/ConfigCommand.cs +++ b/DevProxy/Commands/ConfigCommand.cs @@ -117,7 +117,10 @@ private async Task DownloadConfigAsync(string configId, OutputFormat outputForma var appFolder = ProxyUtils.AppFolder; if (string.IsNullOrEmpty(appFolder) || !Directory.Exists(appFolder)) { - _logger.LogError("App folder {AppFolder} not found", appFolder); + if (outputFormat == OutputFormat.Text) + { + _logger.LogError("App folder {AppFolder} not found", appFolder); + } return; } @@ -143,7 +146,10 @@ private async Task DownloadConfigAsync(string configId, OutputFormat outputForma var sampleFiles = await GetFilesToDownloadAsync(configId); if (sampleFiles.Length == 0) { - _logger.LogError("Config {ConfigId} not found in the samples repo", configId); + if (outputFormat == OutputFormat.Text) + { + _logger.LogError("Config {ConfigId} not found in the samples repo", configId); + } return; } foreach (var sampleFile in sampleFiles) @@ -198,7 +204,10 @@ private async Task DownloadConfigAsync(string configId, OutputFormat outputForma } catch (Exception ex) { - _logger.LogError(ex, "Error downloading config"); + if (outputFormat == OutputFormat.Text) + { + _logger.LogError(ex, "Error downloading config"); + } } } @@ -352,13 +361,19 @@ private async Task CreateConfigFileAsync(string name, OutputFormat outputFormat) if (!snippets.TryGetValue(configFileSnippetName, out var snippet)) { - _logger.LogError("Snippet {SnippetName} not found", configFileSnippetName); + if (outputFormat == OutputFormat.Text) + { + _logger.LogError("Snippet {SnippetName} not found", configFileSnippetName); + } return; } if (snippet.Body is null || snippet.Body.Length == 0) { - _logger.LogError("Snippet {SnippetName} is empty", configFileSnippetName); + if (outputFormat == OutputFormat.Text) + { + _logger.LogError("Snippet {SnippetName} is empty", configFileSnippetName); + } return; } @@ -381,7 +396,10 @@ private async Task CreateConfigFileAsync(string name, OutputFormat outputFormat) } catch (Exception ex) { - _logger.LogError(ex, "Error downloading config"); + if (outputFormat == OutputFormat.Text) + { + _logger.LogError(ex, "Error downloading config"); + } } } diff --git a/DevProxy/Logging/ILoggerExtensions.cs b/DevProxy/Logging/ILoggerExtensions.cs index 2ba77907..7656c8c8 100644 --- a/DevProxy/Logging/ILoggerExtensions.cs +++ b/DevProxy/Logging/ILoggerExtensions.cs @@ -23,5 +23,5 @@ static class ILoggerExtensions /// without any log envelope formatting. /// public static void LogStructuredOutput(this ILogger logger, string message) => - logger.Log(LogLevel.Information, LogEvents.StructuredOutput, message); + logger.Log(LogLevel.Information, LogEvents.StructuredOutput, message, null, static (s, _) => s ?? string.Empty); } \ No newline at end of file