Skip to content

Add json-log-path setting #13003

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions doc/manual/rl-next/json-logger.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 2 additions & 0 deletions src/libstore/daemon.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -1050,6 +1051,7 @@ void processConnection(
if (!recursive) {
prevLogger_ = std::move(logger);
logger = std::move(tunnelLogger_);
applyJSONLogger();
}

unsigned int opCount = 0;
Expand Down
4 changes: 1 addition & 3 deletions src/libstore/uds-remote-store.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
25 changes: 24 additions & 1 deletion src/libutil/include/nix/util/logging.hh
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
#include "nix/util/file-descriptor.hh"
#include "nix/util/finally.hh"

#include <filesystem>

#include <nlohmann/json_fwd.hpp>

namespace nix {
Expand Down Expand Up @@ -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
Copy link
Member

@Mic92 Mic92 Apr 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
A path to which JSON records of Nix's log output will be
A file or unix socket 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).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional as suggested by the reviewer:

Suggested change
(without the `@nix ` prefixes on each line).
(without the `@nix ` prefixes on each line).
When writing to a file be aware that the output can in the order of 100MBs per build.

)"};
};

extern LoggerSettings loggerSettings;
Expand Down Expand Up @@ -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.
*/
Comment on lines +209 to +213
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought, no action required:
Would be nice to split these concerns at some point. A UI is a logger, but a logger is not a UI, something like that.

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".
Expand Down
7 changes: 7 additions & 0 deletions src/libutil/include/nix/util/unix-domain-socket.hh
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
#endif
#include <unistd.h>

#include <filesystem>

namespace nix {

/**
Expand Down Expand Up @@ -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);

}
68 changes: 62 additions & 6 deletions src/libutil/logging.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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;
Expand All @@ -204,20 +209,31 @@ struct JSONLogger : Logger {

struct State
{
bool enabled = true;
};

Sync<State> _state;

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
Expand Down Expand Up @@ -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, 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));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will allow multiple Nix instances to append to the same file simultaneously, but the writing isn't synchronized between them, which will lead to interleaved (corrupted) entries, especially if some of the log lines are larger than the page size (e.g. long gcc invocation logs from make, but could be anything)

if (!fd)
throw SysError("opening log file %1%", path);

return std::make_unique<JSONFileLogger>(std::move(fd), includeNixPrefix);
}

void applyJSONLogger()
{
return std::make_unique<JSONLogger>(fd);
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)
Expand Down
1 change: 1 addition & 0 deletions src/libutil/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
107 changes: 107 additions & 0 deletions src/libutil/tee-logger.cc
Original file line number Diff line number Diff line change
@@ -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));
}

}
7 changes: 7 additions & 0 deletions src/libutil/unix-domain-socket.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

}
2 changes: 2 additions & 0 deletions src/nix/main.cc
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,8 @@ void mainWrapped(int argc, char * * argv)
if (!args.helpRequested && !args.completions) throw;
}

applyJSONLogger();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
applyJSONLogger();
/** Apply `loggerSettings` to the global `logger`. This should not be called after any activities have been created (ie before or not at all), or you will generate log items about unknown activities. */
applyJSONLogger();


if (args.helpRequested) {
std::vector<std::string> subcommand;
MultiCommand * command = &args;
Expand Down
9 changes: 9 additions & 0 deletions tests/functional/logging.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading