diff --git a/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs b/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs index 08bce2e961..427bc29127 100644 --- a/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs +++ b/src/Tools/dotnet-trace/CommandLine/Commands/CollectLinuxCommand.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.CommandLine; +using System.CommandLine.Parsing; using System.Diagnostics; using System.IO; using System.Linq; @@ -35,7 +36,9 @@ internal sealed record CollectLinuxArgs( TimeSpan Duration, string Name, int ProcessId, - bool Probe); + bool Probe, + string DebugLogOutput = null, + string DebugFilter = null); public CollectLinuxCommandHandler(IConsole console = null) { @@ -54,7 +57,7 @@ internal static bool IsSupported() string ostype = File.ReadAllText("/etc/os-release"); isSupportedLinuxPlatform = !ostype.Contains("ID=alpine"); } - catch (Exception ex) when (ex is FileNotFoundException or DirectoryNotFoundException or IOException) {} + catch (Exception ex) when (ex is FileNotFoundException or DirectoryNotFoundException or IOException) { } } return isSupportedLinuxPlatform; @@ -143,7 +146,8 @@ internal int CollectLinux(CollectLinuxArgs args) { File.Delete(scriptPath); } - } catch { } + } + catch { } } } @@ -164,16 +168,27 @@ public static Command CollectLinuxCommand() CommonOptions.DurationOption, CommonOptions.NameOption, CommonOptions.ProcessIdOption, + DebugLogOption, + DebugFilterOption }; collectLinuxCommand.TreatUnmatchedTokensAsErrors = true; // collect-linux currently does not support child process tracing. collectLinuxCommand.Description = "Collects diagnostic traces using perf_events, a Linux OS technology. collect-linux requires admin privileges to capture kernel- and user-mode events, and by default, captures events from all processes. This Linux-only command includes the same .NET events as dotnet-trace collect, and it uses the kernel’s user_events mechanism to emit .NET events as perf events, enabling unification of user-space .NET events with kernel-space system events. Use --probe (optionally with -p|--process-id or -n|--name) to only check which processes can be traced by collect-linux without collecting a trace."; - collectLinuxCommand.SetAction((parseResult, ct) => { + collectLinuxCommand.SetAction((parseResult, ct) => + { string providersValue = parseResult.GetValue(CommonOptions.ProvidersOption) ?? string.Empty; string perfEventsValue = parseResult.GetValue(PerfEventsOption) ?? string.Empty; string profilesValue = parseResult.GetValue(CommonOptions.ProfileOption) ?? string.Empty; CollectLinuxCommandHandler handler = new(); + // Handle --debug-log: null if not specified, empty string if specified without value, or the provided value + string debugLogOutput = null; + OptionResult debugLogResult = parseResult.GetResult(DebugLogOption); + if (debugLogResult != null) + { + debugLogOutput = parseResult.GetValue(DebugLogOption) ?? string.Empty; + } + int rc = handler.CollectLinux(new CollectLinuxArgs( Ct: ct, Providers: providersValue.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries), @@ -185,7 +200,10 @@ public static Command CollectLinuxCommand() Duration: parseResult.GetValue(CommonOptions.DurationOption), Name: parseResult.GetValue(CommonOptions.NameOption) ?? string.Empty, ProcessId: parseResult.GetValue(CommonOptions.ProcessIdOption), - Probe: parseResult.GetValue(ProbeOption))); + Probe: parseResult.GetValue(ProbeOption), + DebugLogOutput: debugLogOutput, + DebugFilter: parseResult.GetValue(DebugFilterOption) + )); return Task.FromResult(rc); }); @@ -433,7 +451,37 @@ private byte[] BuildRecordTraceArgs(CollectLinuxArgs args, out string scriptPath recordTraceArgs.Add("--script-file"); recordTraceArgs.Add(scriptPath); + if (args.DebugLogOutput is not null) + { + string logFilter = args.DebugFilter ?? "error,one_collect::helpers::dotnet::os::linux=debug"; + + if (string.Equals(args.DebugLogOutput, "console", StringComparison.OrdinalIgnoreCase)) + { + recordTraceArgs.Add("--log-mode"); + recordTraceArgs.Add("console"); + } + else + { + string logFilePath = string.IsNullOrEmpty(args.DebugLogOutput) + ? resolvedOutput.FullName + ".debuglog" + : args.DebugLogOutput; + recordTraceArgs.Add("--log-mode"); + recordTraceArgs.Add("file"); + recordTraceArgs.Add("--log-file"); + recordTraceArgs.Add(logFilePath); + } + + recordTraceArgs.Add("--log-filter"); + recordTraceArgs.Add(logFilter); + } + string options = string.Join(' ', recordTraceArgs); + + if (args.DebugLogOutput is not null) + { + Console.WriteLine($"Generated recordtrace options: {options}"); + } + return Encoding.UTF8.GetBytes(options); } @@ -511,12 +559,28 @@ private int OutputHandler(uint type, IntPtr data, UIntPtr dataLen) Description = @"Comma-separated list of perf events (e.g. syscalls:sys_enter_execve,sched:sched_switch)." }; + private static readonly Option ProbeOption = new("--probe") { Description = "Probe .NET processes for support of the EventPipe UserEvents IPC command used by collect-linux, without collecting a trace. Results list supported processes first. Use '-o stdout' to print CSV (pid,processName,supportsCollectLinux) to the console, or '-o ' to write the CSV. Probe a single process with -n|--name or -p|--process-id.", }; + private static readonly Option DebugLogOption = + new("--debug-log") + { + Description = @"Enable diagnostic logging for collect-linux itself. Specify 'console' to log to the console, or a file path to log to a file. If no value is provided, logs are written to '.debuglog'.", + Arity = ArgumentArity.ZeroOrOne + }; + + private static readonly Option DebugFilterOption = + new("--debug-filter") + { + Description = @"Override the default log filter for debug logging. Only used when --debug-log is specified. Default filter: 'error,one_collect::helpers::dotnet::os::linux=debug'.", + Hidden = true + }; + + private enum ProbeOutputMode { Console, @@ -544,9 +608,9 @@ private static partial int RunRecordTrace( UIntPtr commandLen, recordTraceCallback callback); -#region testing seams + #region testing seams internal Func RecordTraceInvoker { get; set; } = RunRecordTrace; internal IConsole Console { get; set; } -#endregion + #endregion } } diff --git a/src/tests/dotnet-trace/CollectLinuxCommandFunctionalTests.cs b/src/tests/dotnet-trace/CollectLinuxCommandFunctionalTests.cs index f4cea70ba7..f81738f0c7 100644 --- a/src/tests/dotnet-trace/CollectLinuxCommandFunctionalTests.cs +++ b/src/tests/dotnet-trace/CollectLinuxCommandFunctionalTests.cs @@ -41,7 +41,9 @@ private static CollectLinuxCommandHandler.CollectLinuxArgs TestArgs( TimeSpan duration = default, string name = "", int processId = 0, - bool probe = false) + bool probe = false, + string debugLogOutput = null, + string debugFilter = null) { return new CollectLinuxCommandHandler.CollectLinuxArgs(ct, providers ?? Array.Empty(), @@ -53,7 +55,9 @@ private static CollectLinuxCommandHandler.CollectLinuxArgs TestArgs( duration, name, processId, - probe); + probe, + debugLogOutput, + debugFilter); } [ConditionalTheory(nameof(IsCollectLinuxSupported))] @@ -222,6 +226,113 @@ public void CollectLinuxCommand_NotSupported_OnNonLinux() }); } + [ConditionalFact(nameof(IsCollectLinuxSupported))] + public void CollectLinuxCommand_DebugLog_Console() + { + MockConsole console = new(200, 30); + string recordTraceArgs = null; + CollectLinuxCommandHandler handler = new(console) + { + RecordTraceInvoker = (cmd, len, cb) => + { + recordTraceArgs = System.Text.Encoding.UTF8.GetString(cmd); + cb(3, IntPtr.Zero, UIntPtr.Zero); + return 0; + } + }; + + int exitCode = handler.CollectLinux(TestArgs(debugLogOutput: "console")); + Assert.Equal((int)ReturnCode.Ok, exitCode); + Assert.Contains("--log-mode console", recordTraceArgs); + Assert.Contains("--log-filter error,one_collect::helpers::dotnet::os::linux=debug", recordTraceArgs); + } + + [ConditionalFact(nameof(IsCollectLinuxSupported))] + public void CollectLinuxCommand_DebugLog_FileDefault() + { + MockConsole console = new(200, 30); + string recordTraceArgs = null; + CollectLinuxCommandHandler handler = new(console) + { + RecordTraceInvoker = (cmd, len, cb) => + { + recordTraceArgs = System.Text.Encoding.UTF8.GetString(cmd); + cb(3, IntPtr.Zero, UIntPtr.Zero); + return 0; + } + }; + + // Empty string means --debug-log was specified without a value, should default to file + int exitCode = handler.CollectLinux(TestArgs(debugLogOutput: "")); + Assert.Equal((int)ReturnCode.Ok, exitCode); + Assert.Contains("--log-mode file", recordTraceArgs); + Assert.Contains("--log-file", recordTraceArgs); + Assert.Contains(".debuglog", recordTraceArgs); + Assert.Contains("--log-filter error,one_collect::helpers::dotnet::os::linux=debug", recordTraceArgs); + } + + [ConditionalFact(nameof(IsCollectLinuxSupported))] + public void CollectLinuxCommand_DebugLog_CustomFile() + { + MockConsole console = new(200, 30); + string recordTraceArgs = null; + CollectLinuxCommandHandler handler = new(console) + { + RecordTraceInvoker = (cmd, len, cb) => + { + recordTraceArgs = System.Text.Encoding.UTF8.GetString(cmd); + cb(3, IntPtr.Zero, UIntPtr.Zero); + return 0; + } + }; + + int exitCode = handler.CollectLinux(TestArgs(debugLogOutput: "/tmp/my-debug.log")); + Assert.Equal((int)ReturnCode.Ok, exitCode); + Assert.Contains("--log-mode file", recordTraceArgs); + Assert.Contains("--log-file /tmp/my-debug.log", recordTraceArgs); + } + + [ConditionalFact(nameof(IsCollectLinuxSupported))] + public void CollectLinuxCommand_DebugLog_CustomFilter() + { + MockConsole console = new(200, 30); + string recordTraceArgs = null; + CollectLinuxCommandHandler handler = new(console) + { + RecordTraceInvoker = (cmd, len, cb) => + { + recordTraceArgs = System.Text.Encoding.UTF8.GetString(cmd); + cb(3, IntPtr.Zero, UIntPtr.Zero); + return 0; + } + }; + + int exitCode = handler.CollectLinux(TestArgs(debugLogOutput: "console", debugFilter: "trace")); + Assert.Equal((int)ReturnCode.Ok, exitCode); + Assert.Contains("--log-filter trace", recordTraceArgs); + Assert.DoesNotContain("error,one_collect", recordTraceArgs); + } + + [ConditionalFact(nameof(IsCollectLinuxSupported))] + public void CollectLinuxCommand_DebugFilter_IgnoredWithoutDebugLog() + { + MockConsole console = new(200, 30); + string recordTraceArgs = null; + var handler = new CollectLinuxCommandHandler(console); + handler.RecordTraceInvoker = (cmd, len, cb) => + { + recordTraceArgs = System.Text.Encoding.UTF8.GetString(cmd); + cb(3, IntPtr.Zero, UIntPtr.Zero); + return 0; + }; + + // debugFilter is provided but debugLogOutput is null - filter should be ignored + int exitCode = handler.CollectLinux(TestArgs(debugFilter: "trace")); + Assert.Equal((int)ReturnCode.Ok, exitCode); + Assert.DoesNotContain("--log-filter", recordTraceArgs); + Assert.DoesNotContain("--log-mode", recordTraceArgs); + } + private static int Run(object args, MockConsole console) { var handler = new CollectLinuxCommandHandler(console);