Skip to content

[lldb] Add an artifact provider mechanism to Diagnostics#210417

Merged
JDevlieghere merged 1 commit into
mainfrom
users/JDevlieghere/lldb-diagnostics-artifact-provider
Jul 17, 2026
Merged

[lldb] Add an artifact provider mechanism to Diagnostics#210417
JDevlieghere merged 1 commit into
mainfrom
users/JDevlieghere/lldb-diagnostics-artifact-provider

Conversation

@JDevlieghere

Copy link
Copy Markdown
Member

Add a hook that lets a subsystem contribute a file to the diagnostics
bundle, without breaking layering and making Core depend on it. A plugin registers an
ArtifactProvider callback and a file name via AddArtifactProvider.
Providers run when a bundle is collected, and each one that yields
content is written into the bundle and recorded in the report's
attachments.

This restores, in a simpler form, the collection point that the removed
Diagnostics callback mechanism offered. The motivating consumer is the
downstream Swift health check, which registers a provider to write its
"swift-healthcheck.log" into the bundle. Unlike the old callback, a
provider does not open or write its own file: it just returns the
contents as a string and Diagnostics handles the rest.

I have a follow-up PR that takes advantage of this new mechanism
upstream to collect the GDB remote packet log.


Stack created with GitHub Stacks CLIGive Feedback 💬

@JDevlieghere
JDevlieghere marked this pull request as ready for review July 17, 2026 19:28
Add a hook that lets a subsystem contribute a file to the diagnostics
bundle, without breaking layering and making Core depend on it. A plugin registers an
ArtifactProvider callback and a file name via AddArtifactProvider.
Providers run when a bundle is collected, and each one that yields
content is written into the bundle and recorded in the report's
attachments.

This restores, in a simpler form, the collection point that the removed
Diagnostics callback mechanism offered. The motivating consumer is the
downstream Swift health check, which registers a provider to write its
"swift-healthcheck.log" into the bundle. Unlike the old callback, a
provider does not open or write its own file: it just returns the
contents as a string and Diagnostics handles the rest.

I have a follow-up PR that takes advantage of this new mechanism
upstream to collect the GDB remote packet log.
@JDevlieghere
JDevlieghere force-pushed the users/JDevlieghere/lldb-diagnostics-artifact-provider branch from be1c338 to b41eda2 Compare July 17, 2026 21:03
@llvmorg-github-actions

Copy link
Copy Markdown

@llvm/pr-subscribers-lldb

Author: Jonas Devlieghere (JDevlieghere)

Changes

Add a hook that lets a subsystem contribute a file to the diagnostics
bundle, without breaking layering and making Core depend on it. A plugin registers an
ArtifactProvider callback and a file name via AddArtifactProvider.
Providers run when a bundle is collected, and each one that yields
content is written into the bundle and recorded in the report's
attachments.

This restores, in a simpler form, the collection point that the removed
Diagnostics callback mechanism offered. The motivating consumer is the
downstream Swift health check, which registers a provider to write its
"swift-healthcheck.log" into the bundle. Unlike the old callback, a
provider does not open or write its own file: it just returns the
contents as a string and Diagnostics handles the rest.

I have a follow-up PR that takes advantage of this new mechanism
upstream to collect the GDB remote packet log.


<sub>Stack created with <a href="https://github.com/github/gh-stack"&gt;GitHub Stacks CLI</a> • <a href="https://gh.io/stacks-feedback"&gt;Give Feedback 💬</a></sub>


Full diff: https://github.com/llvm/llvm-project/pull/210417.diff

4 Files Affected:

  • (modified) lldb/include/lldb/Core/Diagnostics.h (+30)
  • (modified) lldb/source/Core/Diagnostics.cpp (+30)
  • (modified) lldb/unittests/Core/CMakeLists.txt (+1)
  • (added) lldb/unittests/Core/DiagnosticsTest.cpp (+140)
diff --git a/lldb/include/lldb/Core/Diagnostics.h b/lldb/include/lldb/Core/Diagnostics.h
index 554314e32e8ce..bcacf3b9c301d 100644
--- a/lldb/include/lldb/Core/Diagnostics.h
+++ b/lldb/include/lldb/Core/Diagnostics.h
@@ -14,6 +14,8 @@
 #include "lldb/Utility/Log.h"
 #include "llvm/Support/Error.h"
 
+#include <functional>
+#include <mutex>
 #include <optional>
 #include <string>
 #include <vector>
@@ -90,6 +92,19 @@ class Diagnostics {
   /// Record a diagnostic message into the always-on, in-memory log.
   void Record(llvm::StringRef message);
 
+  /// Supplies an artifact's contents on demand. Subsystems register a provider
+  /// so Core need not depend on them. Each runs when a bundle is collected.
+  using ArtifactProvider = std::function<std::string()>;
+  using ArtifactProviderID = uint64_t;
+
+  /// Register \p provider to contribute file \p name. Returns an id for
+  /// RemoveArtifactProvider. Thread-safe.
+  ArtifactProviderID AddArtifactProvider(std::string name,
+                                         ArtifactProvider provider);
+
+  /// Unregister a provider. Thread-safe.
+  void RemoveArtifactProvider(ArtifactProviderID id);
+
   static Diagnostics &Instance();
 
   static DiagnosticsProperties &GetGlobalProperties();
@@ -122,6 +137,8 @@ class Diagnostics {
   static void CollectBinaries(const ExecutionContext &exe_ctx,
                               const FileSpec &dir,
                               std::vector<std::string> &files);
+  void CollectArtifactProviders(const FileSpec &dir,
+                                std::vector<std::string> &files);
   /// @}
 
   /// Scalars carried in the report rather than written as files.
@@ -131,6 +148,19 @@ class Diagnostics {
   /// @}
 
   RotatingLogHandler m_log_handler;
+
+  struct ArtifactProviderEntry {
+    ArtifactProviderID id;
+    std::string name;
+    ArtifactProvider provider;
+  };
+
+  /// Registered artifact providers, guarded by the mutex.
+  /// @{
+  ArtifactProviderID m_next_artifact_provider_id = 0;
+  std::vector<ArtifactProviderEntry> m_artifact_providers;
+  std::mutex m_artifact_providers_mutex;
+  /// @}
 };
 
 /// Render a diagnostics report as JSON, for `diagnostics dump`'s terminal
diff --git a/lldb/source/Core/Diagnostics.cpp b/lldb/source/Core/Diagnostics.cpp
index 311d8fab2b1bc..08adcaebdf038 100644
--- a/lldb/source/Core/Diagnostics.cpp
+++ b/lldb/source/Core/Diagnostics.cpp
@@ -25,12 +25,14 @@
 #include "lldb/Utility/ProcessInfo.h"
 #include "lldb/Version/Version.h"
 
+#include "llvm/ADT/STLExtras.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 <mutex>
 #include <optional>
 #include <string>
 #include <vector>
@@ -149,6 +151,20 @@ void Diagnostics::Record(llvm::StringRef message) {
   m_log_handler.Emit(message);
 }
 
+Diagnostics::ArtifactProviderID
+Diagnostics::AddArtifactProvider(std::string name, ArtifactProvider provider) {
+  std::lock_guard<std::mutex> guard(m_artifact_providers_mutex);
+  ArtifactProviderID id = m_next_artifact_provider_id++;
+  m_artifact_providers.push_back({id, std::move(name), std::move(provider)});
+  return id;
+}
+
+void Diagnostics::RemoveArtifactProvider(ArtifactProviderID id) {
+  std::lock_guard<std::mutex> guard(m_artifact_providers_mutex);
+  llvm::erase_if(m_artifact_providers,
+                 [id](const ArtifactProviderEntry &e) { return e.id == id; });
+}
+
 // 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
@@ -253,6 +269,7 @@ Diagnostics::Collect(Debugger &debugger, const ExecutionContext &exe_ctx,
   CollectCommands(debugger, exe_ctx, dir, report.attachments.files);
   if (GetGlobalProperties().GetCollectBinaries())
     CollectBinaries(exe_ctx, dir, report.attachments.files);
+  CollectArtifactProviders(dir, report.attachments.files);
 
   report.version = lldb_private::GetVersion();
   report.os = GetHostDescription(exe_ctx);
@@ -317,6 +334,19 @@ void Diagnostics::CollectBinaries(const ExecutionContext &exe_ctx,
     CopyBinary(process->GetCoreFile(), dir, files);
 }
 
+void Diagnostics::CollectArtifactProviders(const FileSpec &dir,
+                                           std::vector<std::string> &files) {
+  // Snapshot under the lock, then run providers without it: a provider can be
+  // slow and must not block registration or removal.
+  std::vector<ArtifactProviderEntry> providers;
+  {
+    std::lock_guard<std::mutex> guard(m_artifact_providers_mutex);
+    providers = m_artifact_providers;
+  }
+  for (const ArtifactProviderEntry &entry : providers)
+    WriteArtifact(dir, entry.name, entry.provider(), files);
+}
+
 std::string Diagnostics::GetHostDescription(const ExecutionContext &exe_ctx) {
   std::string os = HostInfo::GetTargetTriple().str();
   Target *target = exe_ctx.GetTargetPtr();
diff --git a/lldb/unittests/Core/CMakeLists.txt b/lldb/unittests/Core/CMakeLists.txt
index d69432d332f44..959793d7d4de0 100644
--- a/lldb/unittests/Core/CMakeLists.txt
+++ b/lldb/unittests/Core/CMakeLists.txt
@@ -3,6 +3,7 @@ add_lldb_unittest(LLDBCoreTests
   DebuggerTest.cpp
   CommunicationTest.cpp
   DiagnosticEventTest.cpp
+  DiagnosticsTest.cpp
   DumpDataExtractorTest.cpp
   DumpRegisterInfoTest.cpp
   FormatEntityTest.cpp
diff --git a/lldb/unittests/Core/DiagnosticsTest.cpp b/lldb/unittests/Core/DiagnosticsTest.cpp
new file mode 100644
index 0000000000000..6bf0e7720b8fe
--- /dev/null
+++ b/lldb/unittests/Core/DiagnosticsTest.cpp
@@ -0,0 +1,140 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "lldb/Core/Diagnostics.h"
+#include "Plugins/Platform/MacOSX/PlatformMacOSX.h"
+#include "Plugins/Platform/MacOSX/PlatformRemoteMacOSX.h"
+#include "TestingSupport/TestUtilities.h"
+#include "lldb/Core/Debugger.h"
+#include "lldb/Host/FileSystem.h"
+#include "lldb/Host/HostInfo.h"
+#include "lldb/Target/ExecutionContext.h"
+#include "llvm/ADT/STLExtras.h"
+#include "llvm/Support/FileSystem.h"
+#include "llvm/Support/MemoryBuffer.h"
+#include "llvm/Testing/Support/Error.h"
+#include "gtest/gtest.h"
+
+#include <mutex>
+
+using namespace lldb;
+using namespace lldb_private;
+
+namespace {
+class DiagnosticsTest : public ::testing::Test {
+public:
+  void SetUp() override {
+    FileSystem::Initialize();
+    HostInfo::Initialize();
+    PlatformMacOSX::Initialize();
+    std::call_once(TestUtilities::g_debugger_initialize_flag,
+                   []() { Debugger::Initialize(nullptr); });
+    ArchSpec arch("x86_64-apple-macosx-");
+    Platform::SetHostPlatform(
+        PlatformRemoteMacOSX::CreateInstance(true, &arch));
+  }
+  void TearDown() override {
+    PlatformMacOSX::Terminate();
+    HostInfo::Terminate();
+    FileSystem::Terminate();
+  }
+};
+
+// Read a file from the bundle directory, or "" if it cannot be read.
+std::string ReadBundleFile(const FileSpec &dir, llvm::StringRef name) {
+  FileSpec path = dir.CopyByAppendingPathComponent(name);
+  llvm::ErrorOr<std::unique_ptr<llvm::MemoryBuffer>> buffer =
+      llvm::MemoryBuffer::getFile(path.GetPath());
+  if (!buffer)
+    return "";
+  return (*buffer)->getBuffer().str();
+}
+} // namespace
+
+TEST_F(DiagnosticsTest, ArtifactProviderIDsAreUnique) {
+  Diagnostics diagnostics;
+  Diagnostics::ArtifactProviderID id0 =
+      diagnostics.AddArtifactProvider("a.txt", [] { return "a"; });
+  Diagnostics::ArtifactProviderID id1 =
+      diagnostics.AddArtifactProvider("b.txt", [] { return "b"; });
+  EXPECT_NE(id0, id1);
+}
+
+TEST_F(DiagnosticsTest, ArtifactProviderContributesToBundle) {
+  DebuggerSP debugger_sp = Debugger::CreateInstance();
+  ASSERT_TRUE(debugger_sp);
+
+  Diagnostics diagnostics;
+  int call_count = 0;
+  Diagnostics::ArtifactProviderID id = diagnostics.AddArtifactProvider(
+      "my-artifact.txt", [&call_count]() -> std::string {
+        ++call_count;
+        return "hello diagnostics";
+      });
+
+  ExecutionContext exe_ctx;
+
+  // A registered provider is invoked and its content lands in the bundle and
+  // in the report's attachments.
+  {
+    llvm::Expected<FileSpec> dir = Diagnostics::CreateUniqueDirectory();
+    ASSERT_THAT_EXPECTED(dir, llvm::Succeeded());
+    llvm::Expected<Diagnostics::Report> report =
+        diagnostics.Collect(*debugger_sp, exe_ctx, *dir);
+    ASSERT_THAT_EXPECTED(report, llvm::Succeeded());
+
+    EXPECT_EQ(call_count, 1);
+    EXPECT_TRUE(
+        llvm::is_contained(report->attachments.files, "my-artifact.txt"));
+    EXPECT_EQ(ReadBundleFile(*dir, "my-artifact.txt"), "hello diagnostics");
+
+    llvm::sys::fs::remove_directories(dir->GetPath());
+  }
+
+  // After removal the provider is neither invoked nor present in the bundle.
+  diagnostics.RemoveArtifactProvider(id);
+  call_count = 0;
+  {
+    llvm::Expected<FileSpec> dir = Diagnostics::CreateUniqueDirectory();
+    ASSERT_THAT_EXPECTED(dir, llvm::Succeeded());
+    llvm::Expected<Diagnostics::Report> report =
+        diagnostics.Collect(*debugger_sp, exe_ctx, *dir);
+    ASSERT_THAT_EXPECTED(report, llvm::Succeeded());
+
+    EXPECT_EQ(call_count, 0);
+    EXPECT_FALSE(
+        llvm::is_contained(report->attachments.files, "my-artifact.txt"));
+
+    llvm::sys::fs::remove_directories(dir->GetPath());
+  }
+
+  Debugger::Destroy(debugger_sp);
+}
+
+TEST_F(DiagnosticsTest, RemoveArtifactProviderIgnoresUnknownID) {
+  Diagnostics diagnostics;
+  Diagnostics::ArtifactProviderID id =
+      diagnostics.AddArtifactProvider("kept.txt", [] { return "kept"; });
+
+  // Removing an id that was never handed out must not disturb other providers.
+  diagnostics.RemoveArtifactProvider(id + 1);
+
+  DebuggerSP debugger_sp = Debugger::CreateInstance();
+  ASSERT_TRUE(debugger_sp);
+  ExecutionContext exe_ctx;
+  llvm::Expected<FileSpec> dir = Diagnostics::CreateUniqueDirectory();
+  ASSERT_THAT_EXPECTED(dir, llvm::Succeeded());
+  llvm::Expected<Diagnostics::Report> report =
+      diagnostics.Collect(*debugger_sp, exe_ctx, *dir);
+  ASSERT_THAT_EXPECTED(report, llvm::Succeeded());
+
+  EXPECT_TRUE(llvm::is_contained(report->attachments.files, "kept.txt"));
+
+  llvm::sys::fs::remove_directories(dir->GetPath());
+  Debugger::Destroy(debugger_sp);
+}

@JDevlieghere
JDevlieghere merged commit 2120490 into main Jul 17, 2026
11 of 12 checks passed
@JDevlieghere
JDevlieghere deleted the users/JDevlieghere/lldb-diagnostics-artifact-provider branch July 17, 2026 21:20
JDevlieghere added a commit to swiftlang/llvm-project that referenced this pull request Jul 20, 2026
* [lldb] Add a non-Darwin Host::OpenURL and a Host::URLEncode helper (llvm#206129)

Host::OpenURL was only defined for Darwin (in Host.mm). Add a portable
implementation in the common Host.cpp: on Unix it launches xdg-open; on
Windows it returns "unsupported" for now. xdg-open is run without a
shell (run_in_shell=false) so query-string metacharacters in the URL are
never interpreted by the shell.

Also add Host::URLEncode, an RFC 3986 percent-encoder for assembling
tracker URLs. These are the building blocks for an upcoming "diagnostics
report" command that opens a pre-filled bug URL, and the encoder is
shared with a downstream tap-to-radar reporter.

(cherry picked from commit dbd4528)

* [lldb] Remove the Diagnostics callback mechanism (llvm#206132)

The Diagnostics framework had a callback registry (AddCallback /
RemoveCallback) so subsystems could contribute files to a diagnostics
directory, intended to also run during crash handling. That crash-time
path never materialized, and the sole registered callback was the
Debugger copying its file-backed logs. If you had no logging enabled,
the directory would be empty, confusing the users.

Remove the registry and the callback loop in Diagnostics::Create (which
now just writes the in-memory log), and expose the log copying as
Debugger::CopyLogFilesToDirectory, which "diagnostics dump" calls
directly. The dump command now copies the invoking debugger's logs
rather than every debugger's, which is the more useful behavior I want
to double down on.

(cherry picked from commit 9f34f1c)

* [lldb] Move Diagnostics from Utility to Core (NFC) (llvm#206152)

Nothing in the Utility or Host layers uses Diagnostics. Its only callers
are Debugger (the always-on log feeder), SBDebugger, and the
SystemInitializerCommon lifecycle. Those all live in Core or above. The
header depends only on Utility primitives (FileSpec, Log, Error), and
lldbInitialization already links lldbCore, so the move adds no new link
dependency anywhere.

Relocating it to Core lets Diagnostics reach Debugger, Target,
CommandInterpreter, and Host, which simplifies an upcoming change that
collect a richer diagnostics bundle (statistics, command snapshots,
invocation, etc) and allows us to implement that directly in the
Diagnostics class.

(cherry picked from commit 2ceab13)

* [lldb] Collect a diagnostics bundle on the Diagnostics class (llvm#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 (git@github.com:llvm/llvm-project.git revision 85fa95f)\n  clang revision 85fa95f\n  llvm revision 85fa95f"
}
```

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.

(cherry picked from commit a873660)

* [lldb] Add a BugReporter plugin type and "diagnostics report" (llvm#206578)

Introduce a BugReporter plugin kind that files an assembled
Diagnostics::Report through a pluggable destination, plus a "diagnostics
report" command (aliased "bugreport") that collects the bundle and files
it through the first registered reporter.

CreateBugReporterInstance() returns the first registered reporter, so a
reporter registered earlier wins and a downstream tree can take over by
registering ahead of the built-ins. BugReporterNone is the
always-registered, last-in-order fallback. Its File() returns an error
pointing at LLDB_BUG_REPORT_URL, so the command surfaces "no tracker
configured" through the normal error path instead of special-casing it.

"diagnostics report" writes the bundle, prints a review warning, and
files it unless --no-open is given. The upcoming GitHub reporter, gated
by a CMake option, is the first real destination.

(cherry picked from commit 8cf09c5)

* [lldb] Add a GitHub bug reporter (llvm#206607)

Add a BugReporter plugin that files a diagnostics bundle as an
llvm/llvm-project GitHub issue. File() renders a short Markdown body
from the Diagnostics::Report (version, host, invocation, and a pointer
to the bundle directory to attach), truncates it under a GET-safe URL
length on a UTF-8 character boundary, and opens a pre-filled issues/new
page with Host::OpenURL.

It is gated by LLDB_ENABLE_GITHUB_BUG_REPORTER (default on) and
registers ahead of the no-op fallback, so it is the default destination
for "diagnostics report" while a downstream tree can still register its
own reporter ahead of it.

(cherry picked from commit 1556da0)

* [lldb] Add an artifact provider mechanism to Diagnostics (llvm#210417)

Add a hook that lets a subsystem contribute a file to the diagnostics
bundle, without breaking layering and making Core depend on it. A plugin
registers an ArtifactProvider callback and a file name via AddArtifactProvider.
Providers run when a bundle is collected, and each one that yields
content is written into the bundle and recorded in the report's
attachments.

This restores, in a simpler form, the collection point that the removed
Diagnostics callback mechanism offered. The motivating consumer is the
downstream Swift health check, which registers a provider to write its
"swift-healthcheck.log" into the bundle. Unlike the old callback, a
provider does not open or write its own file: it just returns the
contents as a string and Diagnostics handles the rest.

(cherry picked from commit 2120490)

* [lldb] Contribute swift-healthcheck to the diagnostics bundle

The Swift language plugin added swift-healthcheck.log to the
diagnostics directory through the Diagnostics callback registry, which
the bundle series removed. Port it to the artifact provider mechanism
(llvm#210417): LogChannelSwift returns the health log's contents on demand
and Diagnostics writes the file, so it lands in the bundle's
attachments and the plugin no longer touches the filesystem itself.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants