Skip to content
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

src,cli: support compact (one-line) JSON reports #32254

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 10 additions & 0 deletions doc/api/cli.md
Expand Up @@ -592,6 +592,15 @@ file will be created if it does not exist, and will be appended to if it does.
If an error occurs while attempting to write the warning to the file, the
warning will be written to stderr instead.

### `--report-compact`
<!-- YAML
added: REPLACEME
-->

Write reports in a compact format, single-line JSON, more easily consumable
by log processing systems than the default multi-line format designed for
human consumption.

### `--report-directory=directory`
<!-- YAML
added: v11.8.0
Expand Down Expand Up @@ -1136,6 +1145,7 @@ Node.js options that are allowed are:
* `--preserve-symlinks`
* `--prof-process`
* `--redirect-warnings`
* `--report-compact`
* `--report-directory`
* `--report-filename`
* `--report-on-fatalerror`
Expand Down
15 changes: 15 additions & 0 deletions doc/api/process.md
Expand Up @@ -1795,6 +1795,21 @@ changes:
reports for the current process. Additional documentation is available in the
[report documentation][].

### `process.report.compact`
<!-- YAML
added: REPLACEME
-->

* {boolean}

Write reports in a compact format, single-line JSON, more easily consumable
by log processing systems than the default multi-line format designed for
human consumption.

```js
console.log(`Reports are compact? ${process.report.compact}`);
```

### `process.report.directory`
<!-- YAML
added: v11.12.0
Expand Down
4 changes: 4 additions & 0 deletions doc/api/report.md
Expand Up @@ -419,6 +419,10 @@ that leads to termination of the application. Useful to inspect various
diagnostic data elements such as heap, stack, event loop state, resource
consumption etc. to reason about the fatal error.

* `--report-compact` Write reports in a compact format, single-line JSON, more
easily consumable by log processing systems than the default multi-line format
designed for human consumption.

* `--report-directory` Location at which the report will be
generated.

Expand Down
5 changes: 5 additions & 0 deletions doc/node.1
Expand Up @@ -281,6 +281,11 @@ Write process warnings to the given
.Ar file
instead of printing to stderr.
.
.It Fl -report-compact
Write
.Sy diagnostic reports
in a compact format, single-line JSON.
.
.It Fl -report-directory
Location at which the
.Sy diagnostic report
Expand Down
13 changes: 12 additions & 1 deletion lib/internal/process/report.js
Expand Up @@ -3,7 +3,11 @@ const {
ERR_INVALID_ARG_TYPE,
ERR_SYNTHETIC
} = require('internal/errors').codes;
const { validateSignalName, validateString } = require('internal/validators');
const {
validateSignalName,
validateString,
validateBoolean,
} = require('internal/validators');
const nr = internalBinding('report');
const {
JSONParse,
Expand Down Expand Up @@ -45,6 +49,13 @@ const report = {
validateString(name, 'filename');
nr.setFilename(name);
},
get compact() {
return nr.getCompact();
},
set compact(b) {
validateBoolean(b, 'compact');
nr.setCompact(b);
},
get signal() {
return nr.getSignal();
},
Expand Down
4 changes: 4 additions & 0 deletions src/node_options.cc
Expand Up @@ -577,6 +577,10 @@ PerIsolateOptionsParser::PerIsolateOptionsParser(
"generate diagnostic report on uncaught exceptions",
&PerIsolateOptions::report_uncaught_exception,
kAllowedInEnvironment);
AddOption("--report-compact",
"output compact single-line JSON",
&PerIsolateOptions::report_compact,
kAllowedInEnvironment);
AddOption("--report-on-signal",
"generate diagnostic report upon receiving signals",
&PerIsolateOptions::report_on_signal,
Expand Down
1 change: 1 addition & 0 deletions src/node_options.h
Expand Up @@ -187,6 +187,7 @@ class PerIsolateOptions : public Options {
bool report_uncaught_exception = false;
bool report_on_signal = false;
bool report_on_fatalerror = false;
bool report_compact = false;
std::string report_signal = "SIGUSR2";
std::string report_filename;
std::string report_directory;
Expand Down
13 changes: 8 additions & 5 deletions src/node_report.cc
Expand Up @@ -52,7 +52,8 @@ static void WriteNodeReport(Isolate* isolate,
const char* trigger,
const std::string& filename,
std::ostream& out,
Local<String> stackstr);
Local<String> stackstr,
bool compact);
static void PrintVersionInformation(JSONWriter* writer);
static void PrintJavaScriptStack(JSONWriter* writer,
Isolate* isolate,
Expand Down Expand Up @@ -126,8 +127,9 @@ std::string TriggerNodeReport(Isolate* isolate,
std::cerr << "\nWriting Node.js report to file: " << filename;
}

bool compact = env != nullptr ? options->report_compact : true;
WriteNodeReport(isolate, env, message, trigger, filename, *outstream,
stackstr);
stackstr, compact);

// Do not close stdout/stderr, only close files we opened.
if (outfile.is_open()) {
Expand All @@ -145,7 +147,7 @@ void GetNodeReport(Isolate* isolate,
const char* trigger,
Local<String> stackstr,
std::ostream& out) {
WriteNodeReport(isolate, env, message, trigger, "", out, stackstr);
WriteNodeReport(isolate, env, message, trigger, "", out, stackstr, false);
}

// Internal function to coordinate and write the various
Expand All @@ -156,7 +158,8 @@ static void WriteNodeReport(Isolate* isolate,
const char* trigger,
const std::string& filename,
std::ostream& out,
Local<String> stackstr) {
Local<String> stackstr,
bool compact) {
// Obtain the current time and the pid.
TIME_TYPE tm_struct;
DiagnosticFilename::LocalTime(&tm_struct);
Expand All @@ -169,7 +172,7 @@ static void WriteNodeReport(Isolate* isolate,
// File stream opened OK, now start printing the report content:
// the title and header information (event, filename, timestamp and pid)

JSONWriter writer(out);
JSONWriter writer(out, compact);
writer.json_start();
writer.json_objectstart("header");
writer.json_keyvalue("reportVersion", NODE_REPORT_VERSION);
Expand Down
46 changes: 34 additions & 12 deletions src/node_report.h
Expand Up @@ -65,25 +65,37 @@ extern double prog_start_time;
// JSON compiler definitions.
class JSONWriter {
public:
explicit JSONWriter(std::ostream& out) : out_(out) {}
JSONWriter(std::ostream& out, bool compact)
: out_(out), compact_(compact) {}

private:
inline void indent() { indent_ += 2; }
inline void deindent() { indent_ -= 2; }
inline void advance() {
if (compact_) return;
for (int i = 0; i < indent_; i++) out_ << ' ';
}
inline void write_one_space() {
if (compact_) return;
out_ << ' ';
}
inline void write_new_line() {
if (compact_) return;
out_ << '\n';
}

public:
inline void json_start() {
if (state_ == kAfterValue) out_ << ',';
out_ << '\n';
write_new_line();
advance();
out_ << '{';
indent();
state_ = kObjectStart;
}

inline void json_end() {
out_ << '\n';
write_new_line();
deindent();
advance();
out_ << '}';
Expand All @@ -92,34 +104,42 @@ class JSONWriter {
template <typename T>
inline void json_objectstart(T key) {
if (state_ == kAfterValue) out_ << ',';
out_ << '\n';
write_new_line();
advance();
write_string(key);
out_ << ": {";
out_ << ':';
write_one_space();
out_ << '{';
indent();
state_ = kObjectStart;
}

template <typename T>
inline void json_arraystart(T key) {
if (state_ == kAfterValue) out_ << ',';
out_ << '\n';
write_new_line();
advance();
write_string(key);
out_ << ": [";
out_ << ':';
write_one_space();
out_ << '[';
indent();
state_ = kObjectStart;
}
inline void json_objectend() {
out_ << '\n';
write_new_line();
deindent();
advance();
out_ << '}';
if (indent_ == 0) {
// Top-level object is complete, so end the line.
out_ << '\n';
}
state_ = kAfterValue;
}

inline void json_arrayend() {
out_ << '\n';
write_new_line();
deindent();
advance();
out_ << ']';
Expand All @@ -128,18 +148,19 @@ class JSONWriter {
template <typename T, typename U>
inline void json_keyvalue(const T& key, const U& value) {
if (state_ == kAfterValue) out_ << ',';
out_ << '\n';
write_new_line();
advance();
write_string(key);
out_ << ": ";
out_ << ':';
write_one_space();
write_value(value);
state_ = kAfterValue;
}

template <typename U>
inline void json_element(const U& value) {
if (state_ == kAfterValue) out_ << ',';
out_ << '\n';
write_new_line();
advance();
write_value(value);
state_ = kAfterValue;
Expand Down Expand Up @@ -177,6 +198,7 @@ class JSONWriter {

enum JSONState { kObjectStart, kAfterValue };
std::ostream& out_;
bool compact_;
int indent_ = 0;
int state_ = kObjectStart;
};
Expand Down
14 changes: 14 additions & 0 deletions src/node_report_module.cc
Expand Up @@ -68,6 +68,18 @@ void GetReport(const FunctionCallbackInfo<Value>& info) {
.ToLocalChecked());
}

static void GetCompact(const FunctionCallbackInfo<Value>& info) {
Environment* env = Environment::GetCurrent(info);
info.GetReturnValue().Set(env->isolate_data()->options()->report_compact);
}

static void SetCompact(const FunctionCallbackInfo<Value>& info) {
Environment* env = Environment::GetCurrent(info);
Isolate* isolate = env->isolate();
bool compact = info[0]->ToBoolean(isolate)->Value();
env->isolate_data()->options()->report_compact = compact;
}

static void GetDirectory(const FunctionCallbackInfo<Value>& info) {
Environment* env = Environment::GetCurrent(info);
std::string directory = env->isolate_data()->options()->report_directory;
Expand Down Expand Up @@ -161,6 +173,8 @@ static void Initialize(Local<Object> exports,

env->SetMethod(exports, "writeReport", WriteReport);
env->SetMethod(exports, "getReport", GetReport);
env->SetMethod(exports, "getCompact", GetCompact);
env->SetMethod(exports, "setCompact", SetCompact);
env->SetMethod(exports, "getDirectory", GetDirectory);
env->SetMethod(exports, "setDirectory", SetDirectory);
env->SetMethod(exports, "getFilename", GetFilename);
Expand Down
7 changes: 6 additions & 1 deletion test/common/report.js
Expand Up @@ -25,7 +25,12 @@ function findReports(pid, dir) {
}

function validate(filepath) {
validateContent(JSON.parse(fs.readFileSync(filepath, 'utf8')));
const report = fs.readFileSync(filepath, 'utf8');
if (process.report.compact) {
const end = report.indexOf('\n');
assert.strictEqual(end, report.length - 1);
}
validateContent(JSON.parse(report));
}

function validateContent(report) {
Expand Down
13 changes: 12 additions & 1 deletion test/report/test-report-config.js
@@ -1,4 +1,4 @@
// Flags: --report-on-fatalerror --report-on-signal --report-uncaught-exception
// Flags: --report-on-fatalerror --report-on-signal --report-uncaught-exception --report-compact
'use strict';
const common = require('../common');
const assert = require('assert');
Expand Down Expand Up @@ -55,6 +55,17 @@ assert.throws(() => {
}, { code: 'ERR_INVALID_ARG_TYPE' });
assert.strictEqual(process.report.reportOnSignal, true);

// Verify that process.report.reportCompact behaves properly.
assert.strictEqual(process.report.compact, true);
process.report.compact = false;
assert.strictEqual(process.report.compact, false);
process.report.compact = true;
assert.strictEqual(process.report.compact, true);
assert.throws(() => {
process.report.compact = {};
}, { code: 'ERR_INVALID_ARG_TYPE' });
assert.strictEqual(process.report.compact, true);

if (!common.isWindows) {
// Verify that process.report.signal behaves properly.
assert.strictEqual(process.report.signal, 'SIGUSR2');
Expand Down
5 changes: 5 additions & 0 deletions test/report/test-report-uncaught-exception-compat.js
@@ -0,0 +1,5 @@
// Flags: --experimental-report --report-uncaught-exception --report-compact
'use strict';
// Test producing a compact report on uncaught exception.
require('../common');
require('./test-report-uncaught-exception.js');