https://github.com/JDevlieghere created
https://github.com/llvm/llvm-project/pull/206189
Add Diagnostics::Collect, which gathers the state a triager needs into a
directory, best-effort (one failed section never sinks the rest): the always-on
log plus the debugger's file logs, statistics.json from DebuggerStats, and a
snapshot of the commands run first when triaging (target list, image list,
thread list, backtraces, image lookup, frame variable).
It returns a Diagnostics::Report with the LLDB version, host, and how LLDB was
invoked, plus an Attachments holding the bundle directory and the files written
into it. Each file is recorded as it is written, so a file that could not be
created is simply absent from the list. The report is expected to grow more
fields over time.
`diagnostics dump` now calls Collect and prints the report as JSON to the
terminal instead of only reporting where the directory was written. Here's what
this all looks like:
```
(lldb) diagnostics dump
{
"attachments": {
"directory":
"/var/folders/6b/3sb80ks56rz5vwlhsdvpsxmh0000gn/T/diagnostics-4b5e9f",
"files": [
"diagnostics.log",
"statistics.json",
"commands.txt"
]
},
"invocation": "./build/bin/count",
"os": "arm64-apple-macosx platform=host os=27.0 build=26A374",
"version": "lldb version 23.0.0git ([email protected]:llvm/llvm-project.git
revision 85fa95f05bc5f0c6b4fd845141e522cd4b9589d7)\n clang revision
85fa95f05bc5f0c6b4fd845141e522cd4b9589d7\n llvm revision
85fa95f05bc5f0c6b4fd845141e522cd4b9589d7"
}
```
This is in preparation for a future PR which adds the ability to take all this
information and pre-fill a bug report with it.
>From 23d1fb42ba382aba6cbc4ab84aaafcd18ff8bd4a Mon Sep 17 00:00:00 2001
From: Jonas Devlieghere <[email protected]>
Date: Fri, 26 Jun 2026 14:51:30 -0700
Subject: [PATCH] [lldb] Collect a diagnostics bundle on the Diagnostics class
Add Diagnostics::Collect, which gathers the state a triager needs into a
directory, best-effort (one failed section never sinks the rest): the
always-on log plus the debugger's file logs, statistics.json from
DebuggerStats, and a snapshot of the commands run first when triaging
(target list, image list, thread list, backtraces, image lookup, frame
variable).
It returns a Diagnostics::Report with the LLDB version, host, and how
LLDB was invoked, plus an Attachments holding the bundle directory and
the files written into it. Each file is recorded as it is written, so a
file that could not be created is simply absent from the list. The
report is expected to grow more fields over time.
"diagnostics dump" now calls Collect and prints the report as JSON to
the terminal instead of only reporting where the directory was written.
Here's what this all looks like:
```
(lldb) diagnostics dump
{
"attachments": {
"directory":
"/var/folders/6b/3sb80ks56rz5vwlhsdvpsxmh0000gn/T/diagnostics-4b5e9f",
"files": [
"diagnostics.log",
"statistics.json",
"commands.txt"
]
},
"invocation": "./build/bin/count",
"os": "arm64-apple-macosx platform=host os=27.0 build=26A374",
"version": "lldb version 23.0.0git ([email protected]:llvm/llvm-project.git
revision 85fa95f05bc5f0c6b4fd845141e522cd4b9589d7)\n clang revision
85fa95f05bc5f0c6b4fd845141e522cd4b9589d7\n llvm revision
85fa95f05bc5f0c6b4fd845141e522cd4b9589d7"
}
---
lldb/include/lldb/Core/Debugger.h | 5 +-
lldb/include/lldb/Core/Diagnostics.h | 67 +++++-
.../Commands/CommandObjectDiagnostics.cpp | 19 +-
lldb/source/Core/Debugger.cpp | 10 +-
lldb/source/Core/Diagnostics.cpp | 194 +++++++++++++++++-
lldb/test/Shell/Diagnostics/TestDump.test | 30 ++-
6 files changed, 304 insertions(+), 21 deletions(-)
diff --git a/lldb/include/lldb/Core/Debugger.h
b/lldb/include/lldb/Core/Debugger.h
index fff514fed156d..5ca678870efd2 100644
--- a/lldb/include/lldb/Core/Debugger.h
+++ b/lldb/include/lldb/Core/Debugger.h
@@ -267,8 +267,9 @@ class Debugger : public
std::enable_shared_from_this<Debugger>,
void SetLoggingCallback(lldb::LogOutputCallback log_callback, void *baton);
/// Copy this debugger's file-backed log files into the given directory, for
- /// inclusion in a diagnostics bundle. Best-effort; failures are skipped.
- void CopyLogFilesToDirectory(const FileSpec &dir);
+ /// inclusion in a diagnostics bundle. Returns the names of the files that
+ /// were copied. Best-effort: files that cannot be copied are skipped.
+ std::vector<std::string> CopyLogFilesToDirectory(const FileSpec &dir);
Status SetPropertyValue(const ExecutionContext *exe_ctx,
VarSetOperationType op, llvm::StringRef
property_path,
diff --git a/lldb/include/lldb/Core/Diagnostics.h
b/lldb/include/lldb/Core/Diagnostics.h
index 1952001084095..b1ee0d2fbeda0 100644
--- a/lldb/include/lldb/Core/Diagnostics.h
+++ b/lldb/include/lldb/Core/Diagnostics.h
@@ -14,9 +14,20 @@
#include "llvm/Support/Error.h"
#include <optional>
+#include <string>
+#include <vector>
+
+namespace llvm {
+namespace json {
+class Value;
+} // namespace json
+} // namespace llvm
namespace lldb_private {
+class Debugger;
+class ExecutionContext;
+
/// Diagnostics maintain an always-on, in-memory log of recent diagnostic
/// messages that can be written out to help investigate bugs and troubleshoot
/// issues.
@@ -25,9 +36,37 @@ class Diagnostics {
Diagnostics();
~Diagnostics();
+ /// The bundle directory and the files written into it, recorded as each one
+ /// is created so a file that could not be written is simply absent.
+ struct Attachments {
+ std::string directory;
+ std::vector<std::string> files;
+ };
+
+ /// The state a triager needs to make sense of a bug report. The full payload
+ /// is written into the bundle directory. These scalars are carried in the
+ /// terminal output and the report body rather than as redundant files. More
+ /// fields are expected to accrue over time.
+ struct Report {
+ std::string version;
+ std::string os;
+ std::string invocation;
+ Attachments attachments;
+ };
+
/// Write the in-memory diagnostic log into the given directory.
llvm::Error Create(const FileSpec &dir);
+ /// Collect a full diagnostics bundle into \p dir and return its report.
+ ///
+ /// Writes the always-on log, the debugger's file-backed logs, statistics,
+ /// and a snapshot of the commands a triager runs first. Collection is
+ /// best-effort: a failure to produce one artifact never aborts the rest, so
+ /// a partial bundle is always better than none.
+ llvm::Expected<Report> Collect(Debugger &debugger,
+ const ExecutionContext &exe_ctx,
+ const FileSpec &dir);
+
/// Write the diagnostic log into a directory and print a message to the
given
/// output stream.
/// @{
@@ -35,7 +74,8 @@ class Diagnostics {
bool Dump(llvm::raw_ostream &stream, const FileSpec &dir);
/// @}
- void Report(llvm::StringRef message);
+ /// Record a diagnostic message into the always-on, in-memory log.
+ void Record(llvm::StringRef message);
static Diagnostics &Instance();
@@ -51,9 +91,34 @@ class Diagnostics {
llvm::Error DumpDiangosticsLog(const FileSpec &dir) const;
+ /// Collect the individual parts of the bundle into \p dir, appending the
name
+ /// of each file to \p files as it is written.
+ /// @{
+ void CollectLogs(Debugger &debugger, const FileSpec &dir,
+ std::vector<std::string> &files);
+ static void CollectStatistics(Debugger &debugger,
+ const ExecutionContext &exe_ctx,
+ const FileSpec &dir,
+ std::vector<std::string> &files);
+ static void CollectCommands(Debugger &debugger,
+ const ExecutionContext &exe_ctx,
+ const FileSpec &dir,
+ std::vector<std::string> &files);
+ /// @}
+
+ /// Scalars carried in the report rather than written as files.
+ /// @{
+ static std::string GetHostDescription(const ExecutionContext &exe_ctx);
+ static std::string GetInvocation();
+ /// @}
+
RotatingLogHandler m_log_handler;
};
+/// Render a diagnostics report as JSON, for `diagnostics dump`'s terminal
+/// output.
+llvm::json::Value toJSON(const Diagnostics::Report &report);
+
} // namespace lldb_private
#endif
diff --git a/lldb/source/Commands/CommandObjectDiagnostics.cpp
b/lldb/source/Commands/CommandObjectDiagnostics.cpp
index a44d4bc90eac1..5a65b95e08f05 100644
--- a/lldb/source/Commands/CommandObjectDiagnostics.cpp
+++ b/lldb/source/Commands/CommandObjectDiagnostics.cpp
@@ -7,7 +7,6 @@
//===----------------------------------------------------------------------===//
#include "CommandObjectDiagnostics.h"
-#include "lldb/Core/Debugger.h"
#include "lldb/Core/Diagnostics.h"
#include "lldb/Host/OptionParser.h"
#include "lldb/Interpreter/CommandOptionArgumentTable.h"
@@ -17,6 +16,8 @@
#include "lldb/Interpreter/OptionValueUInt64.h"
#include "lldb/Interpreter/Options.h"
+#include "llvm/Support/JSON.h"
+
using namespace lldb;
using namespace lldb_private;
@@ -86,19 +87,19 @@ class CommandObjectDiagnosticsDump : public
CommandObjectParsed {
return;
}
- llvm::Error error = Diagnostics::Instance().Create(*directory);
- if (error) {
+ // Collect the diagnostics bundle into the directory.
+ llvm::Expected<Diagnostics::Report> report =
+ Diagnostics::Instance().Collect(GetDebugger(), m_exe_ctx, *directory);
+ if (!report) {
result.AppendErrorWithFormat("failed to write diagnostics to %s",
directory->GetPath().c_str());
- result.AppendError(llvm::toString(std::move(error)));
+ result.AppendError(llvm::toString(report.takeError()));
return;
}
- // Copy this debugger's file-backed logs into the directory. This used to
be
- // done by a Diagnostics callback registered by the Debugger.
- GetDebugger().CopyLogFilesToDirectory(*directory);
-
- result.GetOutputStream() << "diagnostics written to " << *directory <<
'\n';
+ // Print the report as JSON so the user can review what a bug report would
+ // carry. The bundle directory and its files are listed under
"attachments".
+ result.GetOutputStream().Format("{0:2}\n", toJSON(*report));
result.SetStatus(eReturnStatusSuccessFinishResult);
}
diff --git a/lldb/source/Core/Debugger.cpp b/lldb/source/Core/Debugger.cpp
index b2e6fe66dc29e..05ef4a69c08f2 100644
--- a/lldb/source/Core/Debugger.cpp
+++ b/lldb/source/Core/Debugger.cpp
@@ -1673,14 +1673,18 @@ void
Debugger::SetLoggingCallback(lldb::LogOutputCallback log_callback,
std::make_shared<CallbackLogHandler>(log_callback, baton);
}
-void Debugger::CopyLogFilesToDirectory(const FileSpec &dir) {
+std::vector<std::string>
+Debugger::CopyLogFilesToDirectory(const FileSpec &dir) {
+ std::vector<std::string> copied;
for (auto &entry : m_stream_handlers) {
llvm::StringRef log_path = entry.first();
llvm::StringRef file_name = llvm::sys::path::filename(log_path);
FileSpec destination = dir.CopyByAppendingPathComponent(file_name);
// Best-effort: skip logs that can't be copied rather than aborting.
- llvm::sys::fs::copy_file(log_path, destination.GetPath());
+ if (!llvm::sys::fs::copy_file(log_path, destination.GetPath()))
+ copied.push_back(file_name.str());
}
+ return copied;
}
void Debugger::SetDestroyCallback(
@@ -1797,7 +1801,7 @@ void Debugger::ReportDiagnosticImpl(Severity severity,
std::string message,
// The diagnostic subsystem is optional but we still want to broadcast
// events when it's disabled.
if (Diagnostics::Enabled())
- Diagnostics::Instance().Report(message);
+ Diagnostics::Instance().Record(message);
// We don't broadcast info events.
if (severity == lldb::eSeverityInfo)
diff --git a/lldb/source/Core/Diagnostics.cpp b/lldb/source/Core/Diagnostics.cpp
index f67d03364be77..cf4612753eabd 100644
--- a/lldb/source/Core/Diagnostics.cpp
+++ b/lldb/source/Core/Diagnostics.cpp
@@ -7,12 +7,29 @@
//===----------------------------------------------------------------------===//
#include "lldb/Core/Diagnostics.h"
+#include "lldb/Core/Debugger.h"
+#include "lldb/Host/Host.h"
+#include "lldb/Host/HostInfo.h"
+#include "lldb/Interpreter/CommandInterpreter.h"
+#include "lldb/Interpreter/CommandReturnObject.h"
+#include "lldb/Target/ExecutionContext.h"
+#include "lldb/Target/Platform.h"
+#include "lldb/Target/Statistics.h"
+#include "lldb/Target/Target.h"
+#include "lldb/Utility/Args.h"
#include "lldb/Utility/LLDBAssert.h"
+#include "lldb/Utility/ProcessInfo.h"
+#include "lldb/Version/Version.h"
#include "llvm/Support/Error.h"
#include "llvm/Support/FileSystem.h"
+#include "llvm/Support/FormatVariadic.h"
+#include "llvm/Support/JSON.h"
#include "llvm/Support/raw_ostream.h"
+
#include <optional>
+#include <string>
+#include <vector>
using namespace lldb_private;
using namespace lldb;
@@ -92,6 +109,181 @@ llvm::Error Diagnostics::DumpDiangosticsLog(const FileSpec
&dir) const {
return Error::success();
}
-void Diagnostics::Report(llvm::StringRef message) {
+void Diagnostics::Record(llvm::StringRef message) {
m_log_handler.Emit(message);
}
+
+// Write a single artifact into the bundle and, on success, record its name in
+// \p files. Best-effort: a write failure leaves the file out of the list, so a
+// missing artifact stays visible. The file is made owner-only because the
+// bundle can contain paths, argv, and command history.
+static void WriteArtifact(const FileSpec &dir, llvm::StringRef name,
+ llvm::StringRef content,
+ std::vector<std::string> &files) {
+ FileSpec file = dir.CopyByAppendingPathComponent(name);
+ std::error_code ec;
+ llvm::raw_fd_ostream os(file.GetPath(), ec, llvm::sys::fs::OF_Text);
+ if (ec)
+ return;
+ os << content;
+ os.flush();
+ llvm::sys::fs::setPermissions(file.GetPath(), llvm::sys::fs::owner_read |
+
llvm::sys::fs::owner_write);
+ files.push_back(name.str());
+}
+
+// Run a command through the interpreter and return its combined output and
+// error text, for inclusion as a snapshot in the bundle.
+static std::string CaptureCommand(Debugger &debugger, llvm::StringRef command)
{
+ CommandReturnObject result(/*colors=*/false);
+ debugger.GetCommandInterpreter().HandleCommand(command.str().c_str(),
+ eLazyBoolNo, result);
+ return (result.GetOutputString() + result.GetErrorString()).str();
+}
+
+namespace {
+/// The execution context a triage command needs before it is worth running.
+enum class Requires { Always, Target, Process, Frame };
+
+struct TriageCommand {
+ llvm::StringRef command;
+ Requires requirement;
+};
+} // namespace
+
+// The commands a triager runs first, captured into the bundle. Add a row to
+// extend the snapshot. The requirement keeps a command from emitting a
spurious
+// "no process" error when its context is absent.
+static constexpr TriageCommand g_triage_commands[] = {
+ {"target list", Requires::Always},
+ {"image list", Requires::Target},
+ {"thread list", Requires::Process},
+ {"thread backtrace all", Requires::Process},
+ {"image lookup -va $pc", Requires::Frame},
+ {"frame variable", Requires::Frame},
+};
+
+static bool Available(Requires requirement, const ExecutionContext &exe_ctx) {
+ switch (requirement) {
+ case Requires::Always:
+ return true;
+ case Requires::Target:
+ return exe_ctx.GetTargetPtr() != nullptr;
+ case Requires::Process:
+ return exe_ctx.GetProcessPtr() != nullptr;
+ case Requires::Frame:
+ return exe_ctx.GetFramePtr() != nullptr;
+ }
+ llvm_unreachable("unhandled Requires");
+}
+
+llvm::Expected<Diagnostics::Report>
+Diagnostics::Collect(Debugger &debugger, const ExecutionContext &exe_ctx,
+ const FileSpec &dir) {
+ // The bundle holds potentially sensitive data (paths, argv, command
history),
+ // so restrict the directory to the owner before writing anything into it.
+ llvm::sys::fs::setPermissions(dir.GetPath(), llvm::sys::fs::owner_read |
+ llvm::sys::fs::owner_write |
+ llvm::sys::fs::owner_exe);
+
+ Report report;
+ report.attachments.directory = dir.GetPath();
+ CollectLogs(debugger, dir, report.attachments.files);
+ CollectStatistics(debugger, exe_ctx, dir, report.attachments.files);
+ CollectCommands(debugger, exe_ctx, dir, report.attachments.files);
+
+ report.version = lldb_private::GetVersion();
+ report.os = GetHostDescription(exe_ctx);
+ report.invocation = GetInvocation();
+ return report;
+}
+
+void Diagnostics::CollectLogs(Debugger &debugger, const FileSpec &dir,
+ std::vector<std::string> &files) {
+ // The always-on diagnostic log.
+ if (Error error = Create(dir))
+ consumeError(std::move(error));
+ else
+ files.push_back("diagnostics.log");
+
+ // This debugger's file-backed logs.
+ for (std::string &name : debugger.CopyLogFilesToDirectory(dir))
+ files.push_back(std::move(name));
+}
+
+void Diagnostics::CollectStatistics(Debugger &debugger,
+ const ExecutionContext &exe_ctx,
+ const FileSpec &dir,
+ std::vector<std::string> &files) {
+ StatisticsOptions options;
+ json::Value stats = DebuggerStats::ReportStatistics(
+ debugger, exe_ctx.GetTargetPtr(), options);
+ std::string str;
+ raw_string_ostream os(str);
+ os << formatv("{0:2}", stats);
+ WriteArtifact(dir, "statistics.json", str, files);
+}
+
+void Diagnostics::CollectCommands(Debugger &debugger,
+ const ExecutionContext &exe_ctx,
+ const FileSpec &dir,
+ std::vector<std::string> &files) {
+ std::string snapshot;
+ for (const TriageCommand &tc : g_triage_commands) {
+ if (!Available(tc.requirement, exe_ctx))
+ continue;
+ snapshot += formatv("=== {0} ===\n", tc.command).str();
+ snapshot += CaptureCommand(debugger, tc.command);
+ snapshot += "\n\n";
+ }
+ WriteArtifact(dir, "commands.txt", snapshot, files);
+}
+
+std::string Diagnostics::GetHostDescription(const ExecutionContext &exe_ctx) {
+ std::string os = HostInfo::GetTargetTriple().str();
+ Target *target = exe_ctx.GetTargetPtr();
+ if (!target)
+ return os;
+ PlatformSP platform_sp = target->GetPlatform();
+ if (!platform_sp)
+ return os;
+
+ os += formatv(" platform={0}", platform_sp->GetName()).str();
+ VersionTuple version = platform_sp->GetOSVersion();
+ if (!version.empty())
+ os += " os=" + version.getAsString();
+ if (std::optional<std::string> build = platform_sp->GetOSBuildString())
+ os += " build=" + *build;
+ return os;
+}
+
+std::string Diagnostics::GetInvocation() {
+ // libLLDB does not store its own argv, so read the invocation from the host
+ // process.
+ ProcessInstanceInfo info;
+ if (!Host::GetProcessInfo(Host::GetCurrentProcessID(), info))
+ return {};
+
+ const Args &args = info.GetArguments();
+ std::string invocation;
+ for (size_t i = 0; i < args.GetArgumentCount(); ++i) {
+ if (i)
+ invocation += ' ';
+ invocation += args.GetArgumentAtIndex(i);
+ }
+ return invocation;
+}
+
+llvm::json::Value lldb_private::toJSON(const Diagnostics::Report &report) {
+ json::Object obj{
+ {"version", report.version},
+ {"os", report.os},
+ };
+ if (!report.invocation.empty())
+ obj["invocation"] = report.invocation;
+ obj["attachments"] = json::Object{
+ {"directory", report.attachments.directory},
+ {"files", json::Array(report.attachments.files)},
+ };
+ return obj;
+}
diff --git a/lldb/test/Shell/Diagnostics/TestDump.test
b/lldb/test/Shell/Diagnostics/TestDump.test
index 477a4c03a13b1..d13972de0cf29 100644
--- a/lldb/test/Shell/Diagnostics/TestDump.test
+++ b/lldb/test/Shell/Diagnostics/TestDump.test
@@ -1,13 +1,33 @@
-# Check that the diagnostics dump command uses the correct directory and
-# creates one if needed.
+# Check that 'diagnostics dump' collects a bundle and prints a JSON summary of
+# what a bug report would carry.
# Dump to an existing directory.
# RUN: rm -rf %t.existing
# RUN: mkdir -p %t.existing
-# RUN: %lldb -o 'diagnostics dump -d %t.existing'
+# RUN: %lldb --no-lldbinit -b -o 'diagnostics dump -d %t.existing' | FileCheck
%s
# RUN: test -d %t.existing
-# Dump to a non-existing directory.
+# Dump to a non-existing directory; it is created.
# RUN: rm -rf %t.nonexisting
-# RUN: %lldb -o 'diagnostics dump -d %t.nonexisting'
+# RUN: %lldb --no-lldbinit -b -o 'diagnostics dump -d %t.nonexisting'
# RUN: test -d %t.nonexisting
+
+# The terminal output is a JSON summary. The bundle directory and the files
+# written into it are nested under "attachments".
+# CHECK-DAG: "version":
+# CHECK-DAG: "os":
+# CHECK-DAG: "attachments":
+# CHECK-DAG: "directory":
+# CHECK-DAG: "files":
+# CHECK-DAG: "diagnostics.log"
+# CHECK-DAG: "statistics.json"
+# CHECK-DAG: "commands.txt"
+
+# The bundle holds the collected artifacts; the scalars above are not repeated
+# as redundant files.
+# RUN: test -f %t.existing/statistics.json
+# RUN: test -f %t.existing/commands.txt
+
+# The command snapshot is populated, not just present.
+# RUN: FileCheck %s --check-prefix=CMDS --input-file=%t.existing/commands.txt
+# CMDS: target list
_______________________________________________
lldb-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits