Skip to content

Commit

Permalink
report: expose report public native apis
Browse files Browse the repository at this point in the history
Allows APM vendors to generate a diagnostic report without calling into
JavaScript. Like, from their own message channels interrupting the
isolate and generating a report on demand.

PR-URL: #44255
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Gireesh Punathil <gpunathi@in.ibm.com>
  • Loading branch information
legendecas committed Aug 24, 2022
1 parent d9b2d2c commit cb15fc5
Show file tree
Hide file tree
Showing 10 changed files with 404 additions and 140 deletions.
1 change: 1 addition & 0 deletions node.gyp
Expand Up @@ -993,6 +993,7 @@
'test/cctest/test_node_api.cc',
'test/cctest/test_per_process.cc',
'test/cctest/test_platform.cc',
'test/cctest/test_report.cc',
'test/cctest/test_json_utils.cc',
'test/cctest/test_sockaddr.cc',
'test/cctest/test_traced_value.cc',
Expand Down
30 changes: 29 additions & 1 deletion src/node.h
Expand Up @@ -75,8 +75,9 @@
#include "v8-platform.h" // NOLINT(build/include_order)
#include "node_version.h" // NODE_MODULE_VERSION

#include <memory>
#include <functional>
#include <memory>
#include <ostream>

// We cannot use __POSIX__ in this header because that's only defined when
// building Node.js.
Expand Down Expand Up @@ -617,6 +618,33 @@ NODE_EXTERN v8::MaybeLocal<v8::Value> PrepareStackTraceCallback(
v8::Local<v8::Value> exception,
v8::Local<v8::Array> trace);

// Writes a diagnostic report to a file. If filename is not provided, the
// default filename includes the date, time, PID, and a sequence number.
// The report's JavaScript stack trace is taken from err, if present.
// If isolate is nullptr, no information about the JavaScript environment
// is included in the report.
// Returns the filename of the written report.
NODE_EXTERN std::string TriggerNodeReport(v8::Isolate* isolate,
const char* message,
const char* trigger,
const std::string& filename,
v8::Local<v8::Value> error);
NODE_EXTERN std::string TriggerNodeReport(Environment* env,
const char* message,
const char* trigger,
const std::string& filename,
v8::Local<v8::Value> error);
NODE_EXTERN void GetNodeReport(v8::Isolate* isolate,
const char* message,
const char* trigger,
v8::Local<v8::Value> error,
std::ostream& out);
NODE_EXTERN void GetNodeReport(Environment* env,
const char* message,
const char* trigger,
v8::Local<v8::Value> error,
std::ostream& out);

// This returns the MultiIsolatePlatform used for an Environment or IsolateData
// instance, if one exists.
NODE_EXTERN MultiIsolatePlatform* GetMultiIsolatePlatform(Environment* env);
Expand Down
17 changes: 3 additions & 14 deletions src/node_errors.cc
Expand Up @@ -449,8 +449,7 @@ static void ReportFatalException(Environment* env,
}

if (env->isolate_data()->options()->report_uncaught_exception) {
report::TriggerNodeReport(
isolate, env, report_message.c_str(), "Exception", "", error);
TriggerNodeReport(env, report_message.c_str(), "Exception", "", error);
}

if (env->options()->trace_uncaught) {
Expand Down Expand Up @@ -482,19 +481,14 @@ void OnFatalError(const char* location, const char* message) {
}

Isolate* isolate = Isolate::TryGetCurrent();
Environment* env = nullptr;
if (isolate != nullptr) {
env = Environment::GetCurrent(isolate);
}
bool report_on_fatalerror;
{
Mutex::ScopedLock lock(node::per_process::cli_options_mutex);
report_on_fatalerror = per_process::cli_options->report_on_fatalerror;
}

if (report_on_fatalerror) {
report::TriggerNodeReport(
isolate, env, message, "FatalError", "", Local<Object>());
TriggerNodeReport(isolate, message, "FatalError", "", Local<Object>());
}

fflush(stderr);
Expand All @@ -512,19 +506,14 @@ void OOMErrorHandler(const char* location, bool is_heap_oom) {
}

Isolate* isolate = Isolate::TryGetCurrent();
Environment* env = nullptr;
if (isolate != nullptr) {
env = Environment::GetCurrent(isolate);
}
bool report_on_fatalerror;
{
Mutex::ScopedLock lock(node::per_process::cli_options_mutex);
report_on_fatalerror = per_process::cli_options->report_on_fatalerror;
}

if (report_on_fatalerror) {
report::TriggerNodeReport(
isolate, env, message, "OOMError", "", Local<Object>());
TriggerNodeReport(isolate, message, "OOMError", "", Local<Object>());
}

fflush(stderr);
Expand Down
243 changes: 137 additions & 106 deletions src/node_report.cc
@@ -1,8 +1,8 @@
#include "env-inl.h"
#include "json_utils.h"
#include "node_report.h"
#include "debug_utils-inl.h"
#include "diagnosticfilename-inl.h"
#include "env-inl.h"
#include "json_utils.h"
#include "node_internals.h"
#include "node_metadata.h"
#include "node_mutex.h"
Expand All @@ -29,8 +29,6 @@ constexpr double SEC_PER_MICROS = 1e-6;
constexpr int MAX_FRAME_COUNT = 10;

namespace node {
namespace report {

using node::worker::Worker;
using v8::Array;
using v8::Context;
Expand All @@ -53,6 +51,7 @@ using v8::TryCatch;
using v8::V8;
using v8::Value;

namespace report {
// Internal/static function declarations
static void WriteNodeReport(Isolate* isolate,
Environment* env,
Expand Down Expand Up @@ -83,102 +82,6 @@ static void PrintRelease(JSONWriter* writer);
static void PrintCpuInfo(JSONWriter* writer);
static void PrintNetworkInterfaceInfo(JSONWriter* writer);

// External function to trigger a report, writing to file.
std::string TriggerNodeReport(Isolate* isolate,
Environment* env,
const char* message,
const char* trigger,
const std::string& name,
Local<Value> error) {
std::string filename;

// Determine the required report filename. In order of priority:
// 1) supplied on API 2) configured on startup 3) default generated
if (!name.empty()) {
// Filename was specified as API parameter.
filename = name;
} else {
std::string report_filename;
{
Mutex::ScopedLock lock(per_process::cli_options_mutex);
report_filename = per_process::cli_options->report_filename;
}
if (report_filename.length() > 0) {
// File name was supplied via start-up option.
filename = report_filename;
} else {
filename = *DiagnosticFilename(env != nullptr ? env->thread_id() : 0,
"report", "json");
}
}

// Open the report file stream for writing. Supports stdout/err,
// user-specified or (default) generated name
std::ofstream outfile;
std::ostream* outstream;
if (filename == "stdout") {
outstream = &std::cout;
} else if (filename == "stderr") {
outstream = &std::cerr;
} else {
std::string report_directory;
{
Mutex::ScopedLock lock(per_process::cli_options_mutex);
report_directory = per_process::cli_options->report_directory;
}
// Regular file. Append filename to directory path if one was specified
if (report_directory.length() > 0) {
std::string pathname = report_directory;
pathname += kPathSeparator;
pathname += filename;
outfile.open(pathname, std::ios::out | std::ios::binary);
} else {
outfile.open(filename, std::ios::out | std::ios::binary);
}
// Check for errors on the file open
if (!outfile.is_open()) {
std::cerr << "\nFailed to open Node.js report file: " << filename;

if (report_directory.length() > 0)
std::cerr << " directory: " << report_directory;

std::cerr << " (errno: " << errno << ")" << std::endl;
return "";
}
outstream = &outfile;
std::cerr << "\nWriting Node.js report to file: " << filename;
}

bool compact;
{
Mutex::ScopedLock lock(per_process::cli_options_mutex);
compact = per_process::cli_options->report_compact;
}
WriteNodeReport(isolate, env, message, trigger, filename, *outstream,
error, compact);

// Do not close stdout/stderr, only close files we opened.
if (outfile.is_open()) {
outfile.close();
}

// Do not mix JSON and free-form text on stderr.
if (filename != "stderr") {
std::cerr << "\nNode.js report completed" << std::endl;
}
return filename;
}

// External function to trigger a report, writing to a supplied stream.
void GetNodeReport(Isolate* isolate,
Environment* env,
const char* message,
const char* trigger,
Local<Value> error,
std::ostream& out) {
WriteNodeReport(isolate, env, message, trigger, "", out, error, false);
}

// Internal function to coordinate and write the various
// sections of the report to the supplied stream
static void WriteNodeReport(Isolate* isolate,
Expand Down Expand Up @@ -319,12 +222,8 @@ static void WriteNodeReport(Isolate* isolate,
expected_results += w->RequestInterrupt([&](Environment* env) {
std::ostringstream os;

GetNodeReport(env->isolate(),
env,
"Worker thread subreport",
trigger,
Local<Value>(),
os);
GetNodeReport(
env, "Worker thread subreport", trigger, Local<Value>(), os);

Mutex::ScopedLock lock(workers_mutex);
worker_infos.emplace_back(os.str());
Expand Down Expand Up @@ -884,4 +783,136 @@ static void PrintRelease(JSONWriter* writer) {
}

} // namespace report

// External function to trigger a report, writing to file.
std::string TriggerNodeReport(Isolate* isolate,
const char* message,
const char* trigger,
const std::string& name,
Local<Value> error) {
Environment* env = nullptr;
if (isolate != nullptr) {
env = Environment::GetCurrent(isolate);
}
return TriggerNodeReport(env, message, trigger, name, error);
}

// External function to trigger a report, writing to file.
std::string TriggerNodeReport(Environment* env,
const char* message,
const char* trigger,
const std::string& name,
Local<Value> error) {
std::string filename;

// Determine the required report filename. In order of priority:
// 1) supplied on API 2) configured on startup 3) default generated
if (!name.empty()) {
// Filename was specified as API parameter.
filename = name;
} else {
std::string report_filename;
{
Mutex::ScopedLock lock(per_process::cli_options_mutex);
report_filename = per_process::cli_options->report_filename;
}
if (report_filename.length() > 0) {
// File name was supplied via start-up option.
filename = report_filename;
} else {
filename = *DiagnosticFilename(
env != nullptr ? env->thread_id() : 0, "report", "json");
}
}

// Open the report file stream for writing. Supports stdout/err,
// user-specified or (default) generated name
std::ofstream outfile;
std::ostream* outstream;
if (filename == "stdout") {
outstream = &std::cout;
} else if (filename == "stderr") {
outstream = &std::cerr;
} else {
std::string report_directory;
{
Mutex::ScopedLock lock(per_process::cli_options_mutex);
report_directory = per_process::cli_options->report_directory;
}
// Regular file. Append filename to directory path if one was specified
if (report_directory.length() > 0) {
std::string pathname = report_directory;
pathname += kPathSeparator;
pathname += filename;
outfile.open(pathname, std::ios::out | std::ios::binary);
} else {
outfile.open(filename, std::ios::out | std::ios::binary);
}
// Check for errors on the file open
if (!outfile.is_open()) {
std::cerr << "\nFailed to open Node.js report file: " << filename;

if (report_directory.length() > 0)
std::cerr << " directory: " << report_directory;

std::cerr << " (errno: " << errno << ")" << std::endl;
return "";
}
outstream = &outfile;
std::cerr << "\nWriting Node.js report to file: " << filename;
}

bool compact;
{
Mutex::ScopedLock lock(per_process::cli_options_mutex);
compact = per_process::cli_options->report_compact;
}

Isolate* isolate = nullptr;
if (env != nullptr) {
isolate = env->isolate();
}
report::WriteNodeReport(
isolate, env, message, trigger, filename, *outstream, error, compact);

// Do not close stdout/stderr, only close files we opened.
if (outfile.is_open()) {
outfile.close();
}

// Do not mix JSON and free-form text on stderr.
if (filename != "stderr") {
std::cerr << "\nNode.js report completed" << std::endl;
}
return filename;
}

// External function to trigger a report, writing to a supplied stream.
void GetNodeReport(Isolate* isolate,
const char* message,
const char* trigger,
Local<Value> error,
std::ostream& out) {
Environment* env = nullptr;
if (isolate != nullptr) {
env = Environment::GetCurrent(isolate);
}
report::WriteNodeReport(
isolate, env, message, trigger, "", out, error, false);
}

// External function to trigger a report, writing to a supplied stream.
void GetNodeReport(Environment* env,
const char* message,
const char* trigger,
Local<Value> error,
std::ostream& out) {
Isolate* isolate = nullptr;
if (env != nullptr) {
isolate = env->isolate();
}
report::WriteNodeReport(
isolate, env, message, trigger, "", out, error, false);
}

} // namespace node

0 comments on commit cb15fc5

Please sign in to comment.