report: expose report public native apis

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: https://github.com/nodejs/node/pull/44255
Reviewed-By: Anna Henningsen <anna@addaleax.net>
Reviewed-By: Gireesh Punathil <gpunathi@in.ibm.com>
This commit is contained in:
Chengzhong Wu 2022-08-25 01:02:26 +08:00 committed by GitHub
parent d9b2d2c0dd
commit cb15fc56d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 404 additions and 140 deletions

View File

@ -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',

View File

@ -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.
@ -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);

View File

@ -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) {
@ -482,10 +481,6 @@ 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);
@ -493,8 +488,7 @@ void OnFatalError(const char* location, const char* message) {
}
if (report_on_fatalerror) {
report::TriggerNodeReport(
isolate, env, message, "FatalError", "", Local<Object>());
TriggerNodeReport(isolate, message, "FatalError", "", Local<Object>());
}
fflush(stderr);
@ -512,10 +506,6 @@ 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);
@ -523,8 +513,7 @@ void OOMErrorHandler(const char* location, bool is_heap_oom) {
}
if (report_on_fatalerror) {
report::TriggerNodeReport(
isolate, env, message, "OOMError", "", Local<Object>());
TriggerNodeReport(isolate, message, "OOMError", "", Local<Object>());
}
fflush(stderr);

View File

@ -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"
@ -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;
@ -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,
@ -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,
@ -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());
@ -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

View File

@ -14,24 +14,10 @@
#endif
#include <iomanip>
#include <sstream>
namespace node {
namespace report {
// Function declarations - functions in src/node_report.cc
std::string TriggerNodeReport(v8::Isolate* isolate,
Environment* env,
const char* message,
const char* trigger,
const std::string& name,
v8::Local<v8::Value> error);
void GetNodeReport(v8::Isolate* isolate,
Environment* env,
const char* message,
const char* trigger,
v8::Local<v8::Value> error,
std::ostream& out);
// Function declarations - utility functions in src/node_report_utils.cc
void WalkHandle(uv_handle_t* h, void* arg);

View File

@ -44,8 +44,7 @@ void WriteReport(const FunctionCallbackInfo<Value>& info) {
else
error = Local<Value>();
filename = TriggerNodeReport(
isolate, env, *message, *trigger, filename, error);
filename = TriggerNodeReport(env, *message, *trigger, filename, error);
// Return value is the report filename
info.GetReturnValue().Set(
String::NewFromUtf8(isolate, filename.c_str()).ToLocalChecked());
@ -65,8 +64,7 @@ void GetReport(const FunctionCallbackInfo<Value>& info) {
else
error = Local<Object>();
GetNodeReport(
isolate, env, "JavaScript API", __func__, error, out);
GetNodeReport(env, "JavaScript API", __func__, error, out);
// Return value is the contents of a report as a string.
info.GetReturnValue().Set(

View File

@ -0,0 +1,53 @@
#include <node.h>
#include <v8.h>
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::Value;
void TriggerReport(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
node::TriggerNodeReport(
isolate, "FooMessage", "BarTrigger", std::string(), Local<Value>());
}
void TriggerReportNoIsolate(const FunctionCallbackInfo<Value>& args) {
node::TriggerNodeReport(static_cast<Isolate*>(nullptr),
"FooMessage",
"BarTrigger",
std::string(),
Local<Value>());
}
void TriggerReportEnv(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
node::TriggerNodeReport(
node::GetCurrentEnvironment(isolate->GetCurrentContext()),
"FooMessage",
"BarTrigger",
std::string(),
Local<Value>());
}
void TriggerReportNoEnv(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
node::TriggerNodeReport(static_cast<node::Environment*>(nullptr),
"FooMessage",
"BarTrigger",
std::string(),
Local<Value>());
}
void init(Local<Object> exports) {
NODE_SET_METHOD(exports, "triggerReport", TriggerReport);
NODE_SET_METHOD(exports, "triggerReportNoIsolate", TriggerReportNoIsolate);
NODE_SET_METHOD(exports, "triggerReportEnv", TriggerReportEnv);
NODE_SET_METHOD(exports, "triggerReportNoEnv", TriggerReportNoEnv);
}
NODE_MODULE(NODE_GYP_MODULE_NAME, init)

View File

@ -0,0 +1,9 @@
{
'targets': [
{
'target_name': 'binding',
'sources': [ 'binding.cc' ],
'includes': ['../common.gypi'],
}
]
}

View File

@ -0,0 +1,44 @@
'use strict';
const common = require('../../common');
const assert = require('assert');
const path = require('path');
const helper = require('../../common/report.js');
const tmpdir = require('../../common/tmpdir');
const binding = path.resolve(__dirname, `./build/${common.buildType}/binding`);
const addon = require(binding);
function myAddonMain(method, hasJavaScriptFrames) {
tmpdir.refresh();
process.report.directory = tmpdir.path;
addon[method]();
const reports = helper.findReports(process.pid, tmpdir.path);
assert.strictEqual(reports.length, 1);
const report = reports[0];
helper.validate(report);
const content = require(report);
assert.strictEqual(content.header.event, 'FooMessage');
assert.strictEqual(content.header.trigger, 'BarTrigger');
// Check that the javascript stack is present.
if (hasJavaScriptFrames) {
assert.strictEqual(content.javascriptStack.stack.findIndex((frame) => frame.match('myAddonMain')), 0);
} else {
assert.strictEqual(content.javascriptStack, undefined);
}
}
const methods = [
['triggerReport', true],
['triggerReportNoIsolate', false],
['triggerReportEnv', true],
['triggerReportNoEnv', false],
];
for (const [method, hasJavaScriptFrames] of methods) {
myAddonMain(method, hasJavaScriptFrames);
}

125
test/cctest/test_report.cc Normal file
View File

@ -0,0 +1,125 @@
#include "node.h"
#include <string>
#include "gtest/gtest.h"
#include "node_test_fixture.h"
using node::Environment;
using v8::Context;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::HandleScope;
using v8::Isolate;
using v8::Local;
using v8::SealHandleScope;
using v8::String;
using v8::Value;
bool report_callback_called = false;
class ReportTest : public EnvironmentTestFixture {
private:
void TearDown() override {
NodeTestFixture::TearDown();
report_callback_called = false;
}
};
TEST_F(ReportTest, ReportWithNoIsolate) {
SealHandleScope handle_scope(isolate_);
std::ostringstream oss;
node::GetNodeReport(static_cast<Isolate*>(nullptr),
"FooMessage",
"BarTrigger",
Local<Value>(),
oss);
// Simple checks on the output string contains the message and trigger.
std::string actual = oss.str();
EXPECT_NE(actual.find("FooMessage"), std::string::npos);
EXPECT_NE(actual.find("BarTrigger"), std::string::npos);
}
TEST_F(ReportTest, ReportWithNoEnv) {
SealHandleScope handle_scope(isolate_);
std::ostringstream oss;
node::GetNodeReport(static_cast<Environment*>(nullptr),
"FooMessage",
"BarTrigger",
Local<Value>(),
oss);
// Simple checks on the output string contains the message and trigger.
std::string actual = oss.str();
EXPECT_NE(actual.find("FooMessage"), std::string::npos);
EXPECT_NE(actual.find("BarTrigger"), std::string::npos);
}
TEST_F(ReportTest, ReportWithIsolate) {
const HandleScope handle_scope(isolate_);
const Argv argv;
Env env{handle_scope, argv};
Local<Context> context = isolate_->GetCurrentContext();
Local<Function> fn =
Function::New(context, [](const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
HandleScope scope(isolate);
std::ostringstream oss;
node::GetNodeReport(isolate, "FooMessage", "BarTrigger", args[0], oss);
// Simple checks on the output string contains the message and trigger.
std::string actual = oss.str();
EXPECT_NE(actual.find("FooMessage"), std::string::npos);
EXPECT_NE(actual.find("BarTrigger"), std::string::npos);
report_callback_called = true;
}).ToLocalChecked();
context->Global()
->Set(context, String::NewFromUtf8(isolate_, "foo").ToLocalChecked(), fn)
.FromJust();
node::LoadEnvironment(*env, "foo()").ToLocalChecked();
EXPECT_TRUE(report_callback_called);
}
TEST_F(ReportTest, ReportWithEnv) {
const HandleScope handle_scope(isolate_);
const Argv argv;
Env env{handle_scope, argv};
Local<Context> context = isolate_->GetCurrentContext();
Local<Function> fn =
Function::New(context, [](const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
HandleScope scope(isolate);
std::ostringstream oss;
node::GetNodeReport(
node::GetCurrentEnvironment(isolate->GetCurrentContext()),
"FooMessage",
"BarTrigger",
args[0],
oss);
// Simple checks on the output string contains the message and trigger.
std::string actual = oss.str();
EXPECT_NE(actual.find("FooMessage"), std::string::npos);
EXPECT_NE(actual.find("BarTrigger"), std::string::npos);
report_callback_called = true;
}).ToLocalChecked();
context->Global()
->Set(context, String::NewFromUtf8(isolate_, "foo").ToLocalChecked(), fn)
.FromJust();
node::LoadEnvironment(*env, "foo()").ToLocalChecked();
EXPECT_TRUE(report_callback_called);
}