Skip to content

Commit 4ec25b4

Browse files
sam-githubtargos
authored andcommitted
src,cli: support compact (one-line) JSON reports
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>
1 parent fbd3943 commit 4ec25b4

File tree

14 files changed

+158
-20
lines changed

14 files changed

+158
-20
lines changed

doc/api/cli.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,15 @@ file will be created if it does not exist, and will be appended to if it does.
615615
If an error occurs while attempting to write the warning to the file, the
616616
warning will be written to stderr instead.
617617

618+
### `--report-compact`
619+
<!-- YAML
620+
added: REPLACEME
621+
-->
622+
623+
Write reports in a compact format, single-line JSON, more easily consumable
624+
by log processing systems than the default multi-line format designed for
625+
human consumption.
626+
618627
### `--report-directory=directory`
619628
<!-- YAML
620629
added: v11.8.0
@@ -1160,6 +1169,7 @@ Node.js options that are allowed are:
11601169
* `--preserve-symlinks`
11611170
* `--prof-process`
11621171
* `--redirect-warnings`
1172+
* `--report-compact`
11631173
* `--report-directory`
11641174
* `--report-filename`
11651175
* `--report-on-fatalerror`

doc/api/process.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1767,6 +1767,21 @@ changes:
17671767
reports for the current process. Additional documentation is available in the
17681768
[report documentation][].
17691769

1770+
### `process.report.compact`
1771+
<!-- YAML
1772+
added: REPLACEME
1773+
-->
1774+
1775+
* {boolean}
1776+
1777+
Write reports in a compact format, single-line JSON, more easily consumable
1778+
by log processing systems than the default multi-line format designed for
1779+
human consumption.
1780+
1781+
```js
1782+
console.log(`Reports are compact? ${process.report.compact}`);
1783+
```
1784+
17701785
### `process.report.directory`
17711786
<!-- YAML
17721787
added: v11.12.0

doc/api/report.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,10 @@ that leads to termination of the application. Useful to inspect various
420420
diagnostic data elements such as heap, stack, event loop state, resource
421421
consumption etc. to reason about the fatal error.
422422

423+
* `--report-compact` Write reports in a compact format, single-line JSON, more
424+
easily consumable by log processing systems than the default multi-line format
425+
designed for human consumption.
426+
423427
* `--report-directory` Location at which the report will be
424428
generated.
425429

doc/node.1

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,11 @@ Write process warnings to the given
290290
.Ar file
291291
instead of printing to stderr.
292292
.
293+
.It Fl -report-compact
294+
Write
295+
.Sy diagnostic reports
296+
in a compact format, single-line JSON.
297+
.
293298
.It Fl -report-directory
294299
Location at which the
295300
.Sy diagnostic report

lib/internal/process/report.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ const {
33
ERR_INVALID_ARG_TYPE,
44
ERR_SYNTHETIC
55
} = require('internal/errors').codes;
6-
const { validateSignalName, validateString } = require('internal/validators');
6+
const {
7+
validateSignalName,
8+
validateString,
9+
validateBoolean,
10+
} = require('internal/validators');
711
const nr = internalBinding('report');
812
const {
913
JSONParse,
@@ -45,6 +49,13 @@ const report = {
4549
validateString(name, 'filename');
4650
nr.setFilename(name);
4751
},
52+
get compact() {
53+
return nr.getCompact();
54+
},
55+
set compact(b) {
56+
validateBoolean(b, 'compact');
57+
nr.setCompact(b);
58+
},
4859
get signal() {
4960
return nr.getSignal();
5061
},

lib/internal/validators.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict';
22

33
const {
4+
ArrayIsArray,
45
NumberIsInteger,
56
NumberMAX_SAFE_INTEGER,
67
NumberMIN_SAFE_INTEGER,
@@ -123,6 +124,30 @@ function validateNumber(value, name) {
123124
throw new ERR_INVALID_ARG_TYPE(name, 'number', value);
124125
}
125126

127+
function validateBoolean(value, name) {
128+
if (typeof value !== 'boolean')
129+
throw new ERR_INVALID_ARG_TYPE(name, 'boolean', value);
130+
}
131+
132+
const validateObject = hideStackFrames(
133+
(value, name, { nullable = false } = {}) => {
134+
if ((!nullable && value === null) ||
135+
ArrayIsArray(value) ||
136+
typeof value !== 'object') {
137+
throw new ERR_INVALID_ARG_TYPE(name, 'Object', value);
138+
}
139+
});
140+
141+
const validateArray = hideStackFrames((value, name, { minLength = 0 } = {}) => {
142+
if (!ArrayIsArray(value)) {
143+
throw new ERR_INVALID_ARG_TYPE(name, 'Array', value);
144+
}
145+
if (value.length < minLength) {
146+
const reason = `must be longer than ${minLength}`;
147+
throw new ERR_INVALID_ARG_VALUE(name, value, reason);
148+
}
149+
});
150+
126151
function validateSignalName(signal, name = 'signal') {
127152
if (typeof signal !== 'string')
128153
throw new ERR_INVALID_ARG_TYPE(name, 'string', signal);
@@ -162,10 +187,13 @@ module.exports = {
162187
isInt32,
163188
isUint32,
164189
parseMode,
190+
validateArray,
191+
validateBoolean,
165192
validateBuffer,
166193
validateInt32,
167194
validateInteger,
168195
validateNumber,
196+
validateObject,
169197
validatePort,
170198
validateSignalName,
171199
validateString,

src/node_options.cc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -617,6 +617,10 @@ PerIsolateOptionsParser::PerIsolateOptionsParser(
617617
"generate diagnostic report on uncaught exceptions",
618618
&PerIsolateOptions::report_uncaught_exception,
619619
kAllowedInEnvironment);
620+
AddOption("--report-compact",
621+
"output compact single-line JSON",
622+
&PerIsolateOptions::report_compact,
623+
kAllowedInEnvironment);
620624
AddOption("--report-on-signal",
621625
"generate diagnostic report upon receiving signals",
622626
&PerIsolateOptions::report_on_signal,

src/node_options.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ class PerIsolateOptions : public Options {
188188
bool report_uncaught_exception = false;
189189
bool report_on_signal = false;
190190
bool report_on_fatalerror = false;
191+
bool report_compact = false;
191192
std::string report_signal = "SIGUSR2";
192193
std::string report_filename;
193194
std::string report_directory;

src/node_report.cc

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ static void WriteNodeReport(Isolate* isolate,
5252
const char* trigger,
5353
const std::string& filename,
5454
std::ostream& out,
55-
Local<String> stackstr);
55+
Local<String> stackstr,
56+
bool compact);
5657
static void PrintVersionInformation(JSONWriter* writer);
5758
static void PrintJavaScriptStack(JSONWriter* writer,
5859
Isolate* isolate,
@@ -126,8 +127,9 @@ std::string TriggerNodeReport(Isolate* isolate,
126127
std::cerr << "\nWriting Node.js report to file: " << filename;
127128
}
128129

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

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

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

172-
JSONWriter writer(out);
175+
JSONWriter writer(out, compact);
173176
writer.json_start();
174177
writer.json_objectstart("header");
175178
writer.json_keyvalue("reportVersion", NODE_REPORT_VERSION);

src/node_report.h

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -65,25 +65,37 @@ extern double prog_start_time;
6565
// JSON compiler definitions.
6666
class JSONWriter {
6767
public:
68-
explicit JSONWriter(std::ostream& out) : out_(out) {}
68+
JSONWriter(std::ostream& out, bool compact)
69+
: out_(out), compact_(compact) {}
6970

71+
private:
7072
inline void indent() { indent_ += 2; }
7173
inline void deindent() { indent_ -= 2; }
7274
inline void advance() {
75+
if (compact_) return;
7376
for (int i = 0; i < indent_; i++) out_ << ' ';
7477
}
78+
inline void write_one_space() {
79+
if (compact_) return;
80+
out_ << ' ';
81+
}
82+
inline void write_new_line() {
83+
if (compact_) return;
84+
out_ << '\n';
85+
}
7586

87+
public:
7688
inline void json_start() {
7789
if (state_ == kAfterValue) out_ << ',';
78-
out_ << '\n';
90+
write_new_line();
7991
advance();
8092
out_ << '{';
8193
indent();
8294
state_ = kObjectStart;
8395
}
8496

8597
inline void json_end() {
86-
out_ << '\n';
98+
write_new_line();
8799
deindent();
88100
advance();
89101
out_ << '}';
@@ -92,34 +104,42 @@ class JSONWriter {
92104
template <typename T>
93105
inline void json_objectstart(T key) {
94106
if (state_ == kAfterValue) out_ << ',';
95-
out_ << '\n';
107+
write_new_line();
96108
advance();
97109
write_string(key);
98-
out_ << ": {";
110+
out_ << ':';
111+
write_one_space();
112+
out_ << '{';
99113
indent();
100114
state_ = kObjectStart;
101115
}
102116

103117
template <typename T>
104118
inline void json_arraystart(T key) {
105119
if (state_ == kAfterValue) out_ << ',';
106-
out_ << '\n';
120+
write_new_line();
107121
advance();
108122
write_string(key);
109-
out_ << ": [";
123+
out_ << ':';
124+
write_one_space();
125+
out_ << '[';
110126
indent();
111127
state_ = kObjectStart;
112128
}
113129
inline void json_objectend() {
114-
out_ << '\n';
130+
write_new_line();
115131
deindent();
116132
advance();
117133
out_ << '}';
134+
if (indent_ == 0) {
135+
// Top-level object is complete, so end the line.
136+
out_ << '\n';
137+
}
118138
state_ = kAfterValue;
119139
}
120140

121141
inline void json_arrayend() {
122-
out_ << '\n';
142+
write_new_line();
123143
deindent();
124144
advance();
125145
out_ << ']';
@@ -128,18 +148,19 @@ class JSONWriter {
128148
template <typename T, typename U>
129149
inline void json_keyvalue(const T& key, const U& value) {
130150
if (state_ == kAfterValue) out_ << ',';
131-
out_ << '\n';
151+
write_new_line();
132152
advance();
133153
write_string(key);
134-
out_ << ": ";
154+
out_ << ':';
155+
write_one_space();
135156
write_value(value);
136157
state_ = kAfterValue;
137158
}
138159

139160
template <typename U>
140161
inline void json_element(const U& value) {
141162
if (state_ == kAfterValue) out_ << ',';
142-
out_ << '\n';
163+
write_new_line();
143164
advance();
144165
write_value(value);
145166
state_ = kAfterValue;
@@ -177,6 +198,7 @@ class JSONWriter {
177198

178199
enum JSONState { kObjectStart, kAfterValue };
179200
std::ostream& out_;
201+
bool compact_;
180202
int indent_ = 0;
181203
int state_ = kObjectStart;
182204
};

src/node_report_module.cc

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,18 @@ void GetReport(const FunctionCallbackInfo<Value>& info) {
6868
.ToLocalChecked());
6969
}
7070

71+
static void GetCompact(const FunctionCallbackInfo<Value>& info) {
72+
Environment* env = Environment::GetCurrent(info);
73+
info.GetReturnValue().Set(env->isolate_data()->options()->report_compact);
74+
}
75+
76+
static void SetCompact(const FunctionCallbackInfo<Value>& info) {
77+
Environment* env = Environment::GetCurrent(info);
78+
Isolate* isolate = env->isolate();
79+
bool compact = info[0]->ToBoolean(isolate)->Value();
80+
env->isolate_data()->options()->report_compact = compact;
81+
}
82+
7183
static void GetDirectory(const FunctionCallbackInfo<Value>& info) {
7284
Environment* env = Environment::GetCurrent(info);
7385
std::string directory = env->isolate_data()->options()->report_directory;
@@ -161,6 +173,8 @@ static void Initialize(Local<Object> exports,
161173

162174
env->SetMethod(exports, "writeReport", WriteReport);
163175
env->SetMethod(exports, "getReport", GetReport);
176+
env->SetMethod(exports, "getCompact", GetCompact);
177+
env->SetMethod(exports, "setCompact", SetCompact);
164178
env->SetMethod(exports, "getDirectory", GetDirectory);
165179
env->SetMethod(exports, "setDirectory", SetDirectory);
166180
env->SetMethod(exports, "getFilename", GetFilename);

test/common/report.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,12 @@ function findReports(pid, dir) {
2525
}
2626

2727
function validate(filepath) {
28-
validateContent(JSON.parse(fs.readFileSync(filepath, 'utf8')));
28+
const report = fs.readFileSync(filepath, 'utf8');
29+
if (process.report.compact) {
30+
const end = report.indexOf('\n');
31+
assert.strictEqual(end, report.length - 1);
32+
}
33+
validateContent(JSON.parse(report));
2934
}
3035

3136
function validateContent(report) {

0 commit comments

Comments
 (0)