diff --git a/lldb/include/lldb/Core/Debugger.h b/lldb/include/lldb/Core/Debugger.h --- a/lldb/include/lldb/Core/Debugger.h +++ b/lldb/include/lldb/Core/Debugger.h @@ -19,6 +19,7 @@ #include "lldb/Core/IOHandler.h" #include "lldb/Core/SourceManager.h" #include "lldb/Core/StreamFile.h" +#include "lldb/Core/Telemetry.h" #include "lldb/Core/UserSettingsController.h" #include "lldb/Host/HostThread.h" #include "lldb/Host/Terminal.h" @@ -203,6 +204,8 @@ PlatformList &GetPlatformList() { return m_platform_list; } + TelemetryLogger &GetTelemetryLogger() { return m_telemetry_logger; } + void DispatchInputInterrupt(); void DispatchInputEndOfFile(); @@ -524,6 +527,7 @@ /// Used for shadowing the input file when capturing a reproducer. repro::DataRecorder *m_input_recorder; + TelemetryLogger m_telemetry_logger; lldb::BroadcasterManagerSP m_broadcaster_manager_sp; // The debugger acts as a // broadcaster manager of diff --git a/lldb/include/lldb/Core/Telemetry.h b/lldb/include/lldb/Core/Telemetry.h new file mode 100644 --- /dev/null +++ b/lldb/include/lldb/Core/Telemetry.h @@ -0,0 +1,174 @@ +//===-- Telemetry.h ---------------------------------------------*- C++ -*-===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +#ifndef LLDB_CORE_TELEMETRY_H +#define LLDB_CORE_TELEMETRY_H + +#include +#include + +#include "lldb/Interpreter/CommandReturnObject.h" + +#include "llvm/ADT/StringExtras.h" +#include "llvm/ADT/StringRef.h" + +namespace lldb_private { + +// This struct contains all things that will be logged, and shared between +// Telemetry-related classes. It is a simple flat struct for demo purposes, and +// in the real implementation, this internal struct should be decoupled, e.g. so +// downstream implementers of telemetry can choose whatever struct-like +// representation they want (flat struct, json, proto, etc.). +struct TelemetryEntry { + std::string uuid; + + // Startup entries + std::string username; + std::string lldb_version; + std::string lldb_path; + std::string cwd; + std::string start_time; + + // Shutdown entries + int exit_code = 0; + + // Command entries + std::string original_command; + std::string parsed_command; + std::string args; + std::string canonical_command; + std::string canonical_subcommand; + std::string command_status_str; + + [[nodiscard]] std::string Dump() const { + return llvm::join_items( // + ", ", // + "uuid = " + uuid, // + "username = " + username, // + "lldb_version = " + lldb_version, // + "lldb_path = " + lldb_path, // + "cwd = " + cwd, // + "start_time = " + start_time, // + "exit_code = " + std::to_string(exit_code), // + "original_command = " + original_command, // + "parsed_command = " + parsed_command, // + "args = " + args, // + "canonical_command = " + canonical_command, // + "canonical_subcommand = " + canonical_subcommand, // + "command_status_str = " + command_status_str); + } +}; + +class TelemetryDestination { +public: + virtual ~TelemetryDestination() = default; + virtual void EmitEntry(const TelemetryEntry &entry) = 0; + virtual std::string name() const = 0; +}; + +class TelemetryDestinationStderr : public TelemetryDestination { +public: + virtual ~TelemetryDestinationStderr() = default; + void EmitEntry(const TelemetryEntry &entry) override; + std::string name() const override { return "stderr"; } +}; + +class TelemetryDestinationFile : public TelemetryDestination { +public: + virtual ~TelemetryDestinationFile() = default; + void EmitEntry(const TelemetryEntry &entry) override; + std::string name() const override { return "file"; } +}; + +/// RAII wrapper to log a command finishing +class CommandLogger { +public: + CommandLogger(TelemetryDestination &dest, TelemetryEntry entry, + const CommandReturnObject &m_result, bool enabled) + : m_cur_destination(dest), m_entry(std::move(entry)), m_result(m_result), + m_enabled(enabled) {} + ~CommandLogger(); + + // Move only -- we only want to be destructed once. + CommandLogger(CommandLogger &&) = default; + + void SetParsedCommand(const std::string &parsed_command) { + m_entry.parsed_command = parsed_command; + } + void SetCommandType(const std::string &command_type) { + llvm::SmallVector cmd_parts; + llvm::StringRef cmd = command_type; + cmd.split(cmd_parts, ' ', /*MaxSplit=*/1); + m_entry.canonical_command = cmd_parts[0]; + if (cmd_parts.size() > 1) + m_entry.canonical_subcommand = cmd_parts[1]; + } + void SetArgs(const std::string &args) { m_entry.args = args; } + +private: + TelemetryDestination &m_cur_destination; + TelemetryEntry m_entry; + const CommandReturnObject &m_result; + bool m_enabled; +}; + +class TelemetryLogger { +public: + TelemetryLogger(Debugger *debugger) + : m_uuid(GenerateUuid()), m_debugger(debugger) { + // This should have some decoupled registration mechanism, but this is just + // a demo, so hard code these two here. + AddDestination(std::make_unique()); + AddDestination(std::make_unique()); + SetDestination("stderr"); + } + + void AddDestination(std::unique_ptr dest) { + m_destinations.push_back(std::move(dest)); + } + + void LogStartup(); + void LogExit(); + + CommandLogger CreateCommandLogger(const std::string &command, + const CommandReturnObject &result); + + // Method for the telemetry command interface + bool IsEnabled() const { return m_enabled && m_cur_destination != nullptr; } + void Enable() { m_enabled = true; } + void Disable() { m_enabled = false; } + + std::vector GetDestinations() const { + std::vector ret; + for (const auto &destination : m_destinations) + ret.push_back(destination->name()); + return ret; + } + + std::string GetDestination() const { return m_cur_destination->name(); } + + void SetDestination(const std::string &destination) { + for (auto &d : m_destinations) { + if (d->name() == destination) { + m_cur_destination = d.get(); + return; + } + } + } + +private: + bool m_enabled = true; + std::string GenerateUuid(); + TelemetryEntry GenerateBaseEntry(); + std::string m_uuid; + Debugger *m_debugger = nullptr; + std::vector> m_destinations; + TelemetryDestination *m_cur_destination = nullptr; +}; +} // namespace lldb_private +#endif // LLDB_CORE_TELEMETRY_H diff --git a/lldb/source/Commands/CMakeLists.txt b/lldb/source/Commands/CMakeLists.txt --- a/lldb/source/Commands/CMakeLists.txt +++ b/lldb/source/Commands/CMakeLists.txt @@ -31,6 +31,7 @@ CommandObjectSource.cpp CommandObjectStats.cpp CommandObjectTarget.cpp + CommandObjectTelemetry.cpp CommandObjectThread.cpp CommandObjectThreadUtil.cpp CommandObjectTrace.cpp diff --git a/lldb/source/Commands/CommandObjectTelemetry.h b/lldb/source/Commands/CommandObjectTelemetry.h new file mode 100644 --- /dev/null +++ b/lldb/source/Commands/CommandObjectTelemetry.h @@ -0,0 +1,23 @@ +//===-- CommandObjectTelemetry.h --------------------------------*- C++ -*-===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +#ifndef LLDB_SOURCE_COMMANDS_COMMANDOBJECTTELEMETRY_H +#define LLDB_SOURCE_COMMANDS_COMMANDOBJECTTELEMETRY_H + +#include "lldb/Interpreter/CommandObjectMultiword.h" + +namespace lldb_private { +class CommandObjectTelemetry : public CommandObjectMultiword { +public: + CommandObjectTelemetry(CommandInterpreter &interpreter); + + ~CommandObjectTelemetry() override; +}; +} // namespace lldb_private + +#endif // LLDB_SOURCE_COMMANDS_COMMANDOBJECTTELEMETRY_H diff --git a/lldb/source/Commands/CommandObjectTelemetry.cpp b/lldb/source/Commands/CommandObjectTelemetry.cpp new file mode 100644 --- /dev/null +++ b/lldb/source/Commands/CommandObjectTelemetry.cpp @@ -0,0 +1,154 @@ +//===-- CommandObjectTelemetry.cpp ----------------------------------------===// +// +// 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 "CommandObjectTelemetry.h" +#include "lldb/Core/Debugger.h" +#include "lldb/Interpreter/CommandReturnObject.h" +#include "lldb/lldb-enumerations.h" +#include "lldb/lldb-private-enumerations.h" + +using namespace lldb; +using namespace lldb_private; + +class CommandObjectTelemetryEnable : public CommandObjectParsed { +public: + CommandObjectTelemetryEnable(CommandInterpreter &interpreter) + : CommandObjectParsed(interpreter, "enable", "Enable telemetry") {} + + ~CommandObjectTelemetryEnable() override = default; + +protected: + bool DoExecute(Args &, CommandReturnObject &result) override { + if (GetDebugger().GetTelemetryLogger().IsEnabled()) { + result.AppendWarning("Telemetry already enabled."); + } else { + result.AppendMessage("Telemetry enabled."); + } + GetDebugger().GetTelemetryLogger().Enable(); + result.SetStatus(eReturnStatusSuccessFinishResult); + return true; + } +}; + +class CommandObjectTelemetryDisable : public CommandObjectParsed { +public: + CommandObjectTelemetryDisable(CommandInterpreter &interpreter) + : CommandObjectParsed(interpreter, "disable", "Disable telemetry") {} + + ~CommandObjectTelemetryDisable() override = default; + +protected: + bool DoExecute(Args &, CommandReturnObject &result) override { + if (!GetDebugger().GetTelemetryLogger().IsEnabled()) { + result.AppendWarning("Telemetry already disabled."); + } else { + result.AppendMessage("Telemetry disabled."); + } + GetDebugger().GetTelemetryLogger().Disable(); + result.SetStatus(eReturnStatusSuccessFinishResult); + return true; + } +}; + +class CommandObjectTelemetryShow : public CommandObjectParsed { +public: + CommandObjectTelemetryShow(CommandInterpreter &interpreter) + : CommandObjectParsed(interpreter, "show", + "Show current telemetry settings") {} + + ~CommandObjectTelemetryShow() override = default; + +protected: + bool DoExecute(Args &, CommandReturnObject &result) override { + if (GetDebugger().GetTelemetryLogger().IsEnabled()) { + result.AppendMessageWithFormat( + "Telemetry is enabled with destination \"%s\".", + GetDebugger().GetTelemetryLogger().GetDestination().c_str()); + } else { + result.AppendMessage("Telemetry is disabled."); + } + result.SetStatus(eReturnStatusSuccessFinishResult); + return true; + } +}; + +class CommandObjectTelemetryRedirect : public CommandObjectParsed { +public: + CommandObjectTelemetryRedirect(CommandInterpreter &interpreter) + : CommandObjectParsed(interpreter, "redirect", + "Switch telemetry destination") { + + CommandArgumentEntry arg; + CommandArgumentData redirect_arg; + + redirect_arg.arg_type = eArgTypeName; + redirect_arg.arg_repetition = eArgRepeatPlain; + arg.push_back(redirect_arg); + m_arguments.push_back(arg); + } + + ~CommandObjectTelemetryRedirect() override = default; + +protected: + bool DoExecute(Args &args, CommandReturnObject &result) override { + if (args.GetArgumentCount() != 1) { + result.AppendErrorWithFormat( + "%s takes exactly one argument, the destination.\n", + m_cmd_name.c_str()); + return false; + } + llvm::StringRef requested_dest = args[0].ref(); + if (GetDebugger().GetTelemetryLogger().GetDestination() == requested_dest) { + result.AppendWarningWithFormat( + "Telemetry destination already set to \"%s\"", + requested_dest.str().c_str()); + result.SetStatus(eReturnStatusSuccessFinishResult); + return true; + } + std::vector destinations = + GetDebugger().GetTelemetryLogger().GetDestinations(); + for (const auto &destination : destinations) { + if (destination == requested_dest) { + GetDebugger().GetTelemetryLogger().SetDestination(destination); + result.AppendMessageWithFormat("Telemetry destination set to \"%s\"", + requested_dest.str().c_str()); + result.SetStatus(eReturnStatusSuccessFinishResult); + return true; + } + } + result.AppendErrorWithFormat("Unknown destination \"%s\"", + requested_dest.str().c_str()); + return false; + } + + void HandleArgumentCompletion(CompletionRequest &request, + OptionElementVector &) override { + std::vector destinations = + GetDebugger().GetTelemetryLogger().GetDestinations(); + for (const auto &destination : destinations) { + request.TryCompleteCurrentArg(destination); + } + } +}; + +CommandObjectTelemetry::CommandObjectTelemetry(CommandInterpreter &interpreter) + : CommandObjectMultiword(interpreter, "telemetry", "Configure telemetry", + "telemetry []") { + LoadSubCommand( + "enable", CommandObjectSP(new CommandObjectTelemetryEnable(interpreter))); + LoadSubCommand("disable", CommandObjectSP(new CommandObjectTelemetryDisable( + interpreter))); + LoadSubCommand("show", + CommandObjectSP(new CommandObjectTelemetryShow(interpreter))); + LoadSubCommand("redirect", CommandObjectSP(new CommandObjectTelemetryRedirect( + interpreter))); + // TODO: we would also like to have a "configure" subcommand to configure the + // telemetry redirect, e.g. "telemetry configure file filename /tmp/x.log" +} + +CommandObjectTelemetry::~CommandObjectTelemetry() = default; diff --git a/lldb/source/Core/CMakeLists.txt b/lldb/source/Core/CMakeLists.txt --- a/lldb/source/Core/CMakeLists.txt +++ b/lldb/source/Core/CMakeLists.txt @@ -54,6 +54,7 @@ SourceManager.cpp StreamAsynchronousIO.cpp StreamFile.cpp + Telemetry.cpp UserSettingsController.cpp Value.cpp ValueObject.cpp diff --git a/lldb/source/Core/Debugger.cpp b/lldb/source/Core/Debugger.cpp --- a/lldb/source/Core/Debugger.cpp +++ b/lldb/source/Core/Debugger.cpp @@ -646,6 +646,8 @@ } PluginManager::DebuggerInitialize(*this); + + m_telemetry_logger.LogStartup(); } DebuggerSP Debugger::CreateInstance(lldb::LogOutputCallback log_callback, @@ -742,7 +744,7 @@ m_input_file_sp(std::make_shared(stdin, false)), m_output_stream_sp(std::make_shared(stdout, false)), m_error_stream_sp(std::make_shared(stderr, false)), - m_input_recorder(nullptr), + m_input_recorder(nullptr), m_telemetry_logger(this), m_broadcaster_manager_sp(BroadcasterManager::MakeBroadcasterManager()), m_terminal_state(), m_target_list(*this), m_platform_list(), m_listener_sp(Listener::MakeListener("lldb.Debugger")), @@ -827,6 +829,7 @@ // static void Debugger::Destroy(lldb::DebuggerSP &debugger_sp); // static void Debugger::Terminate(); llvm::call_once(m_clear_once, [this]() { + m_telemetry_logger.LogExit(); ClearIOHandlers(); StopIOHandlerThread(); StopEventHandlerThread(); diff --git a/lldb/source/Core/Telemetry.cpp b/lldb/source/Core/Telemetry.cpp new file mode 100644 --- /dev/null +++ b/lldb/source/Core/Telemetry.cpp @@ -0,0 +1,118 @@ +//===-- Telemetry.cpp -----------------------------------------------------===// +// +// 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/Telemetry.h" + +#include + +#include +#include +#include + +#include "lldb/Core/Debugger.h" +#include "lldb/Host/HostInfo.h" +#include "lldb/Interpreter/CommandInterpreter.h" +#include "lldb/Utility/UUID.h" + +#include "llvm/Support/RandomNumberGenerator.h" + +#include "clang/Basic/Version.h" + +using namespace lldb; +using namespace lldb_private; + +void TelemetryDestinationStderr::EmitEntry(const TelemetryEntry &entry) { + std::cerr << "log entry = " << entry.Dump() << "\n"; +} + +void TelemetryDestinationFile::EmitEntry(const TelemetryEntry &entry) { + std::cerr << "note: writing a log entry to /tmp/lldb-telemetry-demo.txt\n"; + + std::ofstream dest_file("/tmp/lldb-telemetry-demo.txt", + std::ios::out | std::ios::app); + dest_file << "log entry = " << entry.Dump() << "\n"; +} + +CommandLogger::~CommandLogger() { + if (!m_enabled) + return; + // Note: we could add a util to lldb/source/Utility/State.cpp to get a more + // accurate str here. + m_entry.command_status_str = m_result.Succeeded() ? "succeeded" : "failed"; + m_cur_destination.EmitEntry(m_entry); +} + +void TelemetryLogger::LogStartup() { + if (!m_enabled) + return; + TelemetryEntry entry = GenerateBaseEntry(); + auto &resolver = HostInfo::GetUserIDResolver(); + auto opt_username = resolver.GetUserName(HostInfo::GetUserID()); + if (opt_username) + entry.username = *opt_username; + + entry.lldb_version = clang::getClangRevision(); + + // Note: auxval is linux only. + const char *progname = reinterpret_cast(getauxval(AT_EXECFN)); + entry.lldb_path = progname; + + entry.cwd = m_debugger->GetSelectedOrDummyTarget() + .GetPlatform() + ->GetWorkingDirectory() + .GetPath(); + + time_t Time; + struct tm LocalTime; + char TimeBuffer[26]; + + time(&Time); + localtime_r(&Time, &LocalTime); + strftime(TimeBuffer, sizeof(TimeBuffer), "%c", &LocalTime); + + entry.start_time = TimeBuffer; + + m_cur_destination->EmitEntry(entry); +} + +void TelemetryLogger::LogExit() { + if (!m_enabled) + return; + TelemetryEntry entry = GenerateBaseEntry(); + + // Read the exit value. This is 0 by default, or non-zero if the user ran + // "quit 42". + // TODO: also register a signal handler so we can log crashes via telemetry. + bool has_exit_value; + entry.exit_code = + m_debugger->GetCommandInterpreter().GetQuitExitCode(has_exit_value); + (void)has_exit_value; + m_cur_destination->EmitEntry(entry); +} + +CommandLogger +TelemetryLogger::CreateCommandLogger(const std::string &command, + const CommandReturnObject &result) { + TelemetryEntry entry = GenerateBaseEntry(); + entry.original_command = command; + return CommandLogger(*m_cur_destination, entry, result, m_enabled); +} + +std::string TelemetryLogger::GenerateUuid() { + uint8_t random_bytes[16]; + if (auto ec = llvm::getRandomBytes(random_bytes, 16)) + std::cerr << "entropy source failure: " + ec.message(); + auto uuid = UUID::fromData(random_bytes); + return uuid.GetAsString(); +} + +TelemetryEntry TelemetryLogger::GenerateBaseEntry() { + TelemetryEntry entry; + entry.uuid = m_uuid; + return entry; +} diff --git a/lldb/source/Interpreter/CommandInterpreter.cpp b/lldb/source/Interpreter/CommandInterpreter.cpp --- a/lldb/source/Interpreter/CommandInterpreter.cpp +++ b/lldb/source/Interpreter/CommandInterpreter.cpp @@ -36,6 +36,7 @@ #include "Commands/CommandObjectSource.h" #include "Commands/CommandObjectStats.h" #include "Commands/CommandObjectTarget.h" +#include "Commands/CommandObjectTelemetry.h" #include "Commands/CommandObjectThread.h" #include "Commands/CommandObjectTrace.h" #include "Commands/CommandObjectType.h" @@ -534,6 +535,7 @@ REGISTER_COMMAND_OBJECT("source", CommandObjectMultiwordSource); REGISTER_COMMAND_OBJECT("statistics", CommandObjectStats); REGISTER_COMMAND_OBJECT("target", CommandObjectMultiwordTarget); + REGISTER_COMMAND_OBJECT("telemetry", CommandObjectTelemetry); REGISTER_COMMAND_OBJECT("thread", CommandObjectMultiwordThread); REGISTER_COMMAND_OBJECT("trace", CommandObjectTrace); REGISTER_COMMAND_OBJECT("type", CommandObjectType); @@ -1837,6 +1839,8 @@ std::string command_string(command_line); std::string original_command_string(command_line); + auto command_logger = GetDebugger().GetTelemetryLogger().CreateCommandLogger( + command_line, result); Log *log = GetLog(LLDBLog::Commands); llvm::PrettyStackTraceFormat stack_trace("HandleCommand(command = \"%s\")", command_line); @@ -1949,6 +1953,10 @@ wants_raw_input ? "True" : "False"); } + command_logger.SetParsedCommand(command_string); + command_logger.SetCommandType(cmd_obj ? cmd_obj->GetCommandName().str() + : ""); + // Phase 2. // Take care of things like setting up the history command & calling the // appropriate Execute method on the CommandObject, with the appropriate @@ -1985,6 +1993,7 @@ log, "HandleCommand, command line after removing command name(s): '%s'", remainder.c_str()); + command_logger.SetArgs(remainder); cmd_obj->Execute(remainder.c_str(), result); }