Skip to content

Commit 3971117

Browse files
sam-githubMylesBorins
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 <[email protected]> Reviewed-By: Matteo Collina <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Colin Ihrig <[email protected]>
1 parent 1e58c8b commit 3971117

13 files changed

+130
-20
lines changed

doc/api/cli.md

+10
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,15 @@ file will be created if it does not exist, and will be appended to if it does.
592592
If an error occurs while attempting to write the warning to the file, the
593593
warning will be written to stderr instead.
594594

595+
### `--report-compact`
596+
<!-- YAML
597+
added: REPLACEME
598+
-->
599+
600+
Write reports in a compact format, single-line JSON, more easily consumable
601+
by log processing systems than the default multi-line format designed for
602+
human consumption.
603+
595604
### `--report-directory=directory`
596605
<!-- YAML
597606
added: v11.8.0
@@ -1136,6 +1145,7 @@ Node.js options that are allowed are:
11361145
* `--preserve-symlinks`
11371146
* `--prof-process`
11381147
* `--redirect-warnings`
1148+
* `--report-compact`
11391149
* `--report-directory`
11401150
* `--report-filename`
11411151
* `--report-on-fatalerror`

doc/api/process.md

+15
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

+4
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,10 @@ that leads to termination of the application. Useful to inspect various
419419
diagnostic data elements such as heap, stack, event loop state, resource
420420
consumption etc. to reason about the fatal error.
421421

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

doc/node.1

+5
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,11 @@ Write process warnings to the given
281281
.Ar file
282282
instead of printing to stderr.
283283
.
284+
.It Fl -report-compact
285+
Write
286+
.Sy diagnostic reports
287+
in a compact format, single-line JSON.
288+
.
284289
.It Fl -report-directory
285290
Location at which the
286291
.Sy diagnostic report

lib/internal/process/report.js

+12-1
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
},

src/node_options.cc

+4
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,10 @@ PerIsolateOptionsParser::PerIsolateOptionsParser(
577577
"generate diagnostic report on uncaught exceptions",
578578
&PerIsolateOptions::report_uncaught_exception,
579579
kAllowedInEnvironment);
580+
AddOption("--report-compact",
581+
"output compact single-line JSON",
582+
&PerIsolateOptions::report_compact,
583+
kAllowedInEnvironment);
580584
AddOption("--report-on-signal",
581585
"generate diagnostic report upon receiving signals",
582586
&PerIsolateOptions::report_on_signal,

src/node_options.h

+1
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ class PerIsolateOptions : public Options {
187187
bool report_uncaught_exception = false;
188188
bool report_on_signal = false;
189189
bool report_on_fatalerror = false;
190+
bool report_compact = false;
190191
std::string report_signal = "SIGUSR2";
191192
std::string report_filename;
192193
std::string report_directory;

src/node_report.cc

+8-5
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

+34-12
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

+14
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

+6-1
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) {

test/report/test-report-config.js

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Flags: --report-on-fatalerror --report-on-signal --report-uncaught-exception
1+
// Flags: --report-on-fatalerror --report-on-signal --report-uncaught-exception --report-compact
22
'use strict';
33
const common = require('../common');
44
const assert = require('assert');
@@ -55,6 +55,17 @@ assert.throws(() => {
5555
}, { code: 'ERR_INVALID_ARG_TYPE' });
5656
assert.strictEqual(process.report.reportOnSignal, true);
5757

58+
// Verify that process.report.reportCompact behaves properly.
59+
assert.strictEqual(process.report.compact, true);
60+
process.report.compact = false;
61+
assert.strictEqual(process.report.compact, false);
62+
process.report.compact = true;
63+
assert.strictEqual(process.report.compact, true);
64+
assert.throws(() => {
65+
process.report.compact = {};
66+
}, { code: 'ERR_INVALID_ARG_TYPE' });
67+
assert.strictEqual(process.report.compact, true);
68+
5869
if (!common.isWindows) {
5970
// Verify that process.report.signal behaves properly.
6071
assert.strictEqual(process.report.signal, 'SIGUSR2');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// Flags: --experimental-report --report-uncaught-exception --report-compact
2+
'use strict';
3+
// Test producing a compact report on uncaught exception.
4+
require('../common');
5+
require('./test-report-uncaught-exception.js');

0 commit comments

Comments
 (0)