Skip to content

Commit

Permalink
src,cli: support compact (one-line) JSON reports
Browse files Browse the repository at this point in the history
Multi-line JSON is more human readable, but harder for log aggregators
to consume, they usually require a log message per line, particularly
for JSON. Compact output will be consumable by aggregators such as EFK
(Elastic Search-Fluentd-Kibana), LogDNA, DataDog, etc.

PR-URL: #32254
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
  • Loading branch information
sam-github authored and targos committed Apr 28, 2020
1 parent fbd3943 commit 4ec25b4
Show file tree
Hide file tree
Showing 14 changed files with 158 additions and 20 deletions.
10 changes: 10 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,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 @@ -1160,6 +1169,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
Original file line number Diff line number Diff line change
Expand Up @@ -1767,6 +1767,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
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,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
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,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
Original file line number Diff line number Diff line change
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
28 changes: 28 additions & 0 deletions lib/internal/validators.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

const {
ArrayIsArray,
NumberIsInteger,
NumberMAX_SAFE_INTEGER,
NumberMIN_SAFE_INTEGER,
Expand Down Expand Up @@ -123,6 +124,30 @@ function validateNumber(value, name) {
throw new ERR_INVALID_ARG_TYPE(name, 'number', value);
}

function validateBoolean(value, name) {
if (typeof value !== 'boolean')
throw new ERR_INVALID_ARG_TYPE(name, 'boolean', value);
}

const validateObject = hideStackFrames(
(value, name, { nullable = false } = {}) => {
if ((!nullable && value === null) ||
ArrayIsArray(value) ||
typeof value !== 'object') {
throw new ERR_INVALID_ARG_TYPE(name, 'Object', value);
}
});

const validateArray = hideStackFrames((value, name, { minLength = 0 } = {}) => {
if (!ArrayIsArray(value)) {
throw new ERR_INVALID_ARG_TYPE(name, 'Array', value);
}
if (value.length < minLength) {
const reason = `must be longer than ${minLength}`;
throw new ERR_INVALID_ARG_VALUE(name, value, reason);
}
});

function validateSignalName(signal, name = 'signal') {
if (typeof signal !== 'string')
throw new ERR_INVALID_ARG_TYPE(name, 'string', signal);
Expand Down Expand Up @@ -162,10 +187,13 @@ module.exports = {
isInt32,
isUint32,
parseMode,
validateArray,
validateBoolean,
validateBuffer,
validateInt32,
validateInteger,
validateNumber,
validateObject,
validatePort,
validateSignalName,
validateString,
Expand Down
4 changes: 4 additions & 0 deletions src/node_options.cc
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,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
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading

0 comments on commit 4ec25b4

Please sign in to comment.