This commit is contained in:
Eelco Dolstra 2025-04-13 22:47:46 +08:00 committed by GitHub
commit fc85e3cc2f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 228 additions and 10 deletions

View File

@ -0,0 +1,6 @@
---
synopsis: "`json-log-path` setting"
prs: [13003]
---
New setting `json-log-path` that sends a copy of all Nix log messages (in JSON format) to a file or Unix domain socket.

View File

@ -15,6 +15,7 @@
#include "nix/store/derivations.hh"
#include "nix/util/args.hh"
#include "nix/util/git.hh"
#include "nix/util/logging.hh"
#ifndef _WIN32 // TODO need graceful async exit support on Windows?
# include "nix/util/monitor-fd.hh"
@ -1050,6 +1051,7 @@ void processConnection(
if (!recursive) {
prevLogger_ = std::move(logger);
logger = std::move(tunnelLogger_);
applyJSONLogger();
}
unsigned int opCount = 0;

View File

@ -84,9 +84,7 @@ ref<RemoteStore::Connection> UDSRemoteStore::openConnection()
auto conn = make_ref<Connection>();
/* Connect to a daemon that does the privileged work for us. */
conn->fd = createUnixDomainSocket();
nix::connect(toSocket(conn->fd.get()), path);
conn->fd = nix::connect(path);
conn->from.fd = conn->fd.get();
conn->to.fd = conn->fd.get();

View File

@ -6,6 +6,8 @@
#include "nix/util/file-descriptor.hh"
#include "nix/util/finally.hh"
#include <filesystem>
#include <nlohmann/json_fwd.hpp>
namespace nix {
@ -49,6 +51,14 @@ struct LoggerSettings : Config
Whether Nix should print out a stack trace in case of Nix
expression evaluation errors.
)"};
Setting<Path> jsonLogPath{
this, "", "json-log-path",
R"(
A path to which JSON records of Nix's log output will be
written, in the same format as `--log-format internal-json`
(without the `@nix ` prefixes on each line).
)"};
};
extern LoggerSettings loggerSettings;
@ -196,7 +206,20 @@ extern std::unique_ptr<Logger> logger;
std::unique_ptr<Logger> makeSimpleLogger(bool printBuildLogs = true);
std::unique_ptr<Logger> makeJSONLogger(Descriptor fd);
/**
* Create a logger that sends log messages to `mainLogger` and the
* list of loggers in `extraLoggers`. Only `mainLogger` is used for
* writing to stdout and getting user input.
*/
std::unique_ptr<Logger> makeTeeLogger(
std::unique_ptr<Logger> mainLogger,
std::vector<std::unique_ptr<Logger>> && extraLoggers);
std::unique_ptr<Logger> makeJSONLogger(Descriptor fd, bool includeNixPrefix = true);
std::unique_ptr<Logger> makeJSONLogger(const std::filesystem::path & path, bool includeNixPrefix = true);
void applyJSONLogger();
/**
* @param source A noun phrase describing the source of the message, e.g. "the builder".

View File

@ -9,6 +9,8 @@
#endif
#include <unistd.h>
#include <filesystem>
namespace nix {
/**
@ -80,4 +82,9 @@ void bind(Socket fd, const std::string & path);
*/
void connect(Socket fd, const std::string & path);
/**
* Connect to a Unix domain socket.
*/
AutoCloseFD connect(const std::filesystem::path & path);
}

View File

@ -7,6 +7,7 @@
#include "nix/util/source-path.hh"
#include "nix/util/position.hh"
#include "nix/util/sync.hh"
#include "nix/util/unix-domain-socket.hh"
#include <atomic>
#include <sstream>
@ -182,8 +183,12 @@ void to_json(nlohmann::json & json, std::shared_ptr<Pos> pos)
struct JSONLogger : Logger {
Descriptor fd;
bool includeNixPrefix;
JSONLogger(Descriptor fd) : fd(fd) { }
JSONLogger(Descriptor fd, bool includeNixPrefix)
: fd(fd)
, includeNixPrefix(includeNixPrefix)
{ }
bool isVerbose() override {
return true;
@ -204,6 +209,7 @@ struct JSONLogger : Logger {
struct State
{
bool enabled = true;
};
Sync<State> _state;
@ -211,13 +217,23 @@ struct JSONLogger : Logger {
void write(const nlohmann::json & json)
{
auto line =
"@nix " +
(includeNixPrefix ? "@nix " : "") +
json.dump(-1, ' ', false, nlohmann::json::error_handler_t::replace);
/* Acquire a lock to prevent log messages from clobbering each
other. */
auto state(_state.lock());
writeLine(fd, line);
try {
auto state(_state.lock());
if (state->enabled)
writeLine(fd, line);
} catch (...) {
bool enabled = false;
std::swap(_state.lock()->enabled, enabled);
if (enabled) {
ignoreExceptionExceptInterrupt();
logger->warn("disabling JSON logger due to write errors");
}
}
}
void log(Verbosity lvl, std::string_view s) override
@ -289,9 +305,49 @@ struct JSONLogger : Logger {
}
};
std::unique_ptr<Logger> makeJSONLogger(Descriptor fd)
std::unique_ptr<Logger> makeJSONLogger(Descriptor fd, bool includeNixPrefix)
{
return std::make_unique<JSONLogger>(fd);
return std::make_unique<JSONLogger>(fd, includeNixPrefix);
}
std::unique_ptr<Logger> makeJSONLogger(const std::filesystem::path & path, bool includeNixPrefix)
{
struct JSONFileLogger : JSONLogger {
AutoCloseFD fd;
JSONFileLogger(AutoCloseFD && fd, bool includeNixPrefix)
: JSONLogger(fd.get(), includeNixPrefix)
, fd(std::move(fd))
{ }
};
AutoCloseFD fd =
std::filesystem::is_socket(path)
? connect(path)
: toDescriptor(open(path.c_str(), O_CREAT | O_APPEND | O_WRONLY, 0644));
if (!fd)
throw SysError("opening log file %1%", path);
return std::make_unique<JSONFileLogger>(std::move(fd), includeNixPrefix);
}
void applyJSONLogger()
{
if (!loggerSettings.jsonLogPath.get().empty()) {
try {
std::vector<std::unique_ptr<Logger>> loggers;
loggers.push_back(makeJSONLogger(std::filesystem::path(loggerSettings.jsonLogPath.get()), false));
try {
logger = makeTeeLogger(std::move(logger), std::move(loggers));
} catch (...) {
// `logger` is now gone so give up.
abort();
}
} catch (...) {
ignoreExceptionExceptInterrupt();
}
}
}
static Logger::Fields getFields(nlohmann::json & json)

View File

@ -146,6 +146,7 @@ sources = [config_priv_h] + files(
'strings.cc',
'suggestions.cc',
'tarfile.cc',
'tee-logger.cc',
'terminal.cc',
'thread-pool.cc',
'union-source-accessor.cc',

107
src/libutil/tee-logger.cc Normal file
View File

@ -0,0 +1,107 @@
#include "nix/util/logging.hh"
namespace nix {
struct TeeLogger : Logger
{
std::vector<std::unique_ptr<Logger>> loggers;
TeeLogger(std::vector<std::unique_ptr<Logger>> && loggers)
: loggers(std::move(loggers))
{
}
void stop() override
{
for (auto & logger : loggers)
logger->stop();
};
void pause() override
{
for (auto & logger : loggers)
logger->pause();
};
void resume() override
{
for (auto & logger : loggers)
logger->resume();
};
void log(Verbosity lvl, std::string_view s) override
{
for (auto & logger : loggers)
logger->log(lvl, s);
}
void logEI(const ErrorInfo & ei) override
{
for (auto & logger : loggers)
logger->logEI(ei);
}
void startActivity(
ActivityId act,
Verbosity lvl,
ActivityType type,
const std::string & s,
const Fields & fields,
ActivityId parent) override
{
for (auto & logger : loggers)
logger->startActivity(act, lvl, type, s, fields, parent);
}
void stopActivity(ActivityId act) override
{
for (auto & logger : loggers)
logger->stopActivity(act);
}
void result(ActivityId act, ResultType type, const Fields & fields) override
{
for (auto & logger : loggers)
logger->result(act, type, fields);
}
void writeToStdout(std::string_view s) override
{
for (auto & logger : loggers) {
/* Let only the first logger write to stdout to avoid
duplication. This means that the first logger needs to
be the one managing stdout/stderr
(e.g. `ProgressBar`). */
logger->writeToStdout(s);
break;
}
}
std::optional<char> ask(std::string_view s) override
{
for (auto & logger : loggers) {
auto c = logger->ask(s);
if (c)
return c;
}
return std::nullopt;
}
void setPrintBuildLogs(bool printBuildLogs) override
{
for (auto & logger : loggers)
logger->setPrintBuildLogs(printBuildLogs);
}
};
std::unique_ptr<Logger>
makeTeeLogger(std::unique_ptr<Logger> mainLogger, std::vector<std::unique_ptr<Logger>> && extraLoggers)
{
std::vector<std::unique_ptr<Logger>> allLoggers;
allLoggers.push_back(std::move(mainLogger));
for (auto & l : extraLoggers)
allLoggers.push_back(std::move(l));
return std::make_unique<TeeLogger>(std::move(allLoggers));
}
}

View File

@ -114,4 +114,11 @@ void connect(Socket fd, const std::string & path)
bindConnectProcHelper("connect", ::connect, fd, path);
}
AutoCloseFD connect(const std::filesystem::path & path)
{
auto fd = createUnixDomainSocket();
nix::connect(toSocket(fd.get()), path);
return fd;
}
}

View File

@ -460,6 +460,8 @@ void mainWrapped(int argc, char * * argv)
if (!args.helpRequested && !args.completions) throw;
}
applyJSONLogger();
if (args.helpRequested) {
std::vector<std::string> subcommand;
MultiCommand * command = &args;

View File

@ -33,3 +33,12 @@ if isDaemonNewer "2.26"; then
# Build works despite ill-formed structured build log entries.
expectStderr 0 nix build -f ./logging/unusual-logging.nix --no-link | grepQuiet 'warning: Unable to handle a JSON message from the derivation builder:'
fi
# Test json-log-path.
if [[ "$NIX_REMOTE" != "daemon" ]]; then
clearStore
nix build -vv --file dependencies.nix --no-link --json-log-path "$TEST_ROOT/log.json" 2>&1 | grepQuiet 'building.*dependencies-top.drv'
jq < "$TEST_ROOT/log.json"
grep '{"action":"start","fields":\[".*-dependencies-top.drv","",1,1\],"id":.*,"level":3,"parent":0' "$TEST_ROOT/log.json" >&2
(( $(grep '{"action":"msg","level":5,"msg":"executing builder .*"}' "$TEST_ROOT/log.json" | wc -l) == 5 ))
fi