Skip to content

Commit eee841a

Browse files
committed
Add json-log-path setting
This setting specifies a path (which can be a regular file or Unix domain socket) that receives a copy of all Nix log messages (in JSON format).
1 parent 6f2ec5f commit eee841a

File tree

8 files changed

+213
-7
lines changed

8 files changed

+213
-7
lines changed

doc/manual/rl-next/json-logger.md

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

src/libstore/daemon.cc

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include "nix/store/derivations.hh"
1616
#include "nix/util/args.hh"
1717
#include "nix/util/git.hh"
18+
#include "nix/util/logging.hh"
1819

1920
#ifndef _WIN32 // TODO need graceful async exit support on Windows?
2021
# include "nix/util/monitor-fd.hh"
@@ -1050,6 +1051,7 @@ void processConnection(
10501051
if (!recursive) {
10511052
prevLogger_ = std::move(logger);
10521053
logger = std::move(tunnelLogger_);
1054+
applyJSONLogger();
10531055
}
10541056

10551057
unsigned int opCount = 0;

src/libutil/include/nix/util/logging.hh

+24-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
#include "nix/util/file-descriptor.hh"
77
#include "nix/util/finally.hh"
88

9+
#include <filesystem>
10+
911
#include <nlohmann/json_fwd.hpp>
1012

1113
namespace nix {
@@ -49,6 +51,14 @@ struct LoggerSettings : Config
4951
Whether Nix should print out a stack trace in case of Nix
5052
expression evaluation errors.
5153
)"};
54+
55+
Setting<Path> jsonLogPath{
56+
this, "", "json-log-path",
57+
R"(
58+
A path to which JSON records of Nix's log output will be
59+
written, in the same format as `--log-format internal-json`
60+
(without the `@nix ` prefixes on each line).
61+
)"};
5262
};
5363

5464
extern LoggerSettings loggerSettings;
@@ -196,7 +206,20 @@ extern std::unique_ptr<Logger> logger;
196206

197207
std::unique_ptr<Logger> makeSimpleLogger(bool printBuildLogs = true);
198208

199-
std::unique_ptr<Logger> makeJSONLogger(Descriptor fd);
209+
/**
210+
* Create a logger that sends log messages to `mainLogger` and the
211+
* list of loggers in `extraLoggers`. Only `mainLogger` is used for
212+
* writing to stdout and getting user input.
213+
*/
214+
std::unique_ptr<Logger> makeTeeLogger(
215+
std::unique_ptr<Logger> mainLogger,
216+
std::vector<std::unique_ptr<Logger>> && extraLoggers);
217+
218+
std::unique_ptr<Logger> makeJSONLogger(Descriptor fd, bool includeNixPrefix = true);
219+
220+
std::unique_ptr<Logger> makeJSONLogger(const std::filesystem::path & path, bool includeNixPrefix = true);
221+
222+
void applyJSONLogger();
200223

201224
/**
202225
* @param source A noun phrase describing the source of the message, e.g. "the builder".

src/libutil/logging.cc

+62-6
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include "nix/util/source-path.hh"
88
#include "nix/util/position.hh"
99
#include "nix/util/sync.hh"
10+
#include "nix/util/unix-domain-socket.hh"
1011

1112
#include <atomic>
1213
#include <sstream>
@@ -182,8 +183,12 @@ void to_json(nlohmann::json & json, std::shared_ptr<Pos> pos)
182183

183184
struct JSONLogger : Logger {
184185
Descriptor fd;
186+
bool includeNixPrefix;
185187

186-
JSONLogger(Descriptor fd) : fd(fd) { }
188+
JSONLogger(Descriptor fd, bool includeNixPrefix)
189+
: fd(fd)
190+
, includeNixPrefix(includeNixPrefix)
191+
{ }
187192

188193
bool isVerbose() override {
189194
return true;
@@ -204,20 +209,31 @@ struct JSONLogger : Logger {
204209

205210
struct State
206211
{
212+
bool enabled = true;
207213
};
208214

209215
Sync<State> _state;
210216

211217
void write(const nlohmann::json & json)
212218
{
213219
auto line =
214-
"@nix " +
220+
(includeNixPrefix ? "@nix " : "") +
215221
json.dump(-1, ' ', false, nlohmann::json::error_handler_t::replace);
216222

217223
/* Acquire a lock to prevent log messages from clobbering each
218224
other. */
219-
auto state(_state.lock());
220-
writeLine(fd, line);
225+
try {
226+
auto state(_state.lock());
227+
if (state->enabled)
228+
writeLine(fd, line);
229+
} catch (...) {
230+
bool enabled = false;
231+
std::swap(_state.lock()->enabled, enabled);
232+
if (enabled) {
233+
ignoreExceptionExceptInterrupt();
234+
logger->warn("disabling JSON logger due to write errors");
235+
}
236+
}
221237
}
222238

223239
void log(Verbosity lvl, std::string_view s) override
@@ -289,9 +305,49 @@ struct JSONLogger : Logger {
289305
}
290306
};
291307

292-
std::unique_ptr<Logger> makeJSONLogger(Descriptor fd)
308+
std::unique_ptr<Logger> makeJSONLogger(Descriptor fd, bool includeNixPrefix)
309+
{
310+
return std::make_unique<JSONLogger>(fd, includeNixPrefix);
311+
}
312+
313+
std::unique_ptr<Logger> makeJSONLogger(const std::filesystem::path & path, bool includeNixPrefix)
314+
{
315+
struct JSONFileLogger : JSONLogger {
316+
AutoCloseFD fd;
317+
318+
JSONFileLogger(AutoCloseFD && fd, bool includeNixPrefix)
319+
: JSONLogger(fd.get(), includeNixPrefix)
320+
, fd(std::move(fd))
321+
{ }
322+
};
323+
324+
AutoCloseFD fd =
325+
std::filesystem::is_socket(path)
326+
? connect(path)
327+
: toDescriptor(open(path.c_str(), O_CREAT | O_APPEND | O_WRONLY, 0644));
328+
if (!fd)
329+
throw SysError("opening log file %1%", path);
330+
331+
return std::make_unique<JSONFileLogger>(std::move(fd), includeNixPrefix);
332+
}
333+
334+
void applyJSONLogger()
293335
{
294-
return std::make_unique<JSONLogger>(fd);
336+
if (!loggerSettings.jsonLogPath.get().empty()) {
337+
try {
338+
std::vector<std::unique_ptr<Logger>> loggers;
339+
loggers.push_back(makeJSONLogger(std::filesystem::path(loggerSettings.jsonLogPath.get()), false));
340+
try {
341+
logger = makeTeeLogger(std::move(logger), std::move(loggers));
342+
} catch (...) {
343+
// `logger` is now gone so give up.
344+
abort();
345+
}
346+
} catch (...) {
347+
ignoreExceptionExceptInterrupt();
348+
}
349+
350+
}
295351
}
296352

297353
static Logger::Fields getFields(nlohmann::json & json)

src/libutil/meson.build

+1
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ sources = [config_priv_h] + files(
146146
'strings.cc',
147147
'suggestions.cc',
148148
'tarfile.cc',
149+
'tee-logger.cc',
149150
'terminal.cc',
150151
'thread-pool.cc',
151152
'union-source-accessor.cc',

src/libutil/tee-logger.cc

+107
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
#include "nix/util/logging.hh"
2+
3+
namespace nix {
4+
5+
struct TeeLogger : Logger
6+
{
7+
std::vector<std::unique_ptr<Logger>> loggers;
8+
9+
TeeLogger(std::vector<std::unique_ptr<Logger>> && loggers)
10+
: loggers(std::move(loggers))
11+
{
12+
}
13+
14+
void stop() override
15+
{
16+
for (auto & logger : loggers)
17+
logger->stop();
18+
};
19+
20+
void pause() override
21+
{
22+
for (auto & logger : loggers)
23+
logger->pause();
24+
};
25+
26+
void resume() override
27+
{
28+
for (auto & logger : loggers)
29+
logger->resume();
30+
};
31+
32+
void log(Verbosity lvl, std::string_view s) override
33+
{
34+
for (auto & logger : loggers)
35+
logger->log(lvl, s);
36+
}
37+
38+
void logEI(const ErrorInfo & ei) override
39+
{
40+
for (auto & logger : loggers)
41+
logger->logEI(ei);
42+
}
43+
44+
void startActivity(
45+
ActivityId act,
46+
Verbosity lvl,
47+
ActivityType type,
48+
const std::string & s,
49+
const Fields & fields,
50+
ActivityId parent) override
51+
{
52+
for (auto & logger : loggers)
53+
logger->startActivity(act, lvl, type, s, fields, parent);
54+
}
55+
56+
void stopActivity(ActivityId act) override
57+
{
58+
for (auto & logger : loggers)
59+
logger->stopActivity(act);
60+
}
61+
62+
void result(ActivityId act, ResultType type, const Fields & fields) override
63+
{
64+
for (auto & logger : loggers)
65+
logger->result(act, type, fields);
66+
}
67+
68+
void writeToStdout(std::string_view s) override
69+
{
70+
for (auto & logger : loggers) {
71+
/* Let only the first logger write to stdout to avoid
72+
duplication. This means that the first logger needs to
73+
be the one managing stdout/stderr
74+
(e.g. `ProgressBar`). */
75+
logger->writeToStdout(s);
76+
break;
77+
}
78+
}
79+
80+
std::optional<char> ask(std::string_view s) override
81+
{
82+
for (auto & logger : loggers) {
83+
auto c = logger->ask(s);
84+
if (c)
85+
return c;
86+
}
87+
return std::nullopt;
88+
}
89+
90+
void setPrintBuildLogs(bool printBuildLogs) override
91+
{
92+
for (auto & logger : loggers)
93+
logger->setPrintBuildLogs(printBuildLogs);
94+
}
95+
};
96+
97+
std::unique_ptr<Logger>
98+
makeTeeLogger(std::unique_ptr<Logger> mainLogger, std::vector<std::unique_ptr<Logger>> && extraLoggers)
99+
{
100+
std::vector<std::unique_ptr<Logger>> allLoggers;
101+
allLoggers.push_back(std::move(mainLogger));
102+
for (auto & l : extraLoggers)
103+
allLoggers.push_back(std::move(l));
104+
return std::make_unique<TeeLogger>(std::move(allLoggers));
105+
}
106+
107+
}

src/nix/main.cc

+2
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,8 @@ void mainWrapped(int argc, char * * argv)
493493
if (!args.helpRequested && !args.completions) throw;
494494
}
495495

496+
applyJSONLogger();
497+
496498
if (args.helpRequested) {
497499
std::vector<std::string> subcommand;
498500
MultiCommand * command = &args;

tests/functional/logging.sh

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

0 commit comments

Comments
 (0)