#include #include #include #include #include #include #include #include using namespace eosio::trace_api; using namespace eosio::trace_api::configuration_utils; using boost::signals2::scoped_connection; namespace { static auto plugin_reg = application::register_plugin(); const std::string logger_name("trace_api"); fc::logger _log; std::string to_detail_string(const std::exception_ptr& e) { try { std::rethrow_exception(e); } catch (fc::exception& er) { return er.to_detail_string(); } catch (const std::exception& e) { fc::exception fce( FC_LOG_MESSAGE(warn, "std::exception: ${what}: ", ("what", e.what())), fc::std_exception_code, BOOST_CORE_TYPEID(e).name(), e.what()); return fce.to_detail_string(); } catch (...) { fc::unhandled_exception ue( FC_LOG_MESSAGE(warn, "unknown: ",), std::current_exception()); return ue.to_detail_string(); } } void log_exception( const exception_with_context& e, fc::log_level level ) { if( _log.is_enabled( level ) ) { auto detail_string = to_detail_string(std::get<0>(e)); auto context = fc::log_context( level, std::get<1>(e), std::get<2>(e), std::get<3>(e) ); _log.log(fc::log_message( context, detail_string )); } } /** * The exception_handler provided to the extraction sub-system throws `yield_exception` as a signal that * Something has gone wrong and the extraction process needs to terminate immediately * * This templated method is used to wrap signal handlers for `chain_controller` so that the plugin-internal * `yield_exception` can be translated to a `chain::controller_emit_signal_exception`. * * The goal is that the currently applied block will be rolled-back before the shutdown takes effect leaving * the system in a better state for restart. */ template void emit_killer(F&& f) { try { f(); } catch (const yield_exception& ) { EOS_THROW(chain::controller_emit_signal_exception, "Trace API encountered an Error which it cannot recover from. Please resolve the error and relaunch the process") } } template struct shared_store_provider { explicit shared_store_provider(const std::shared_ptr& store) :store(store) {} template void append( const BlockTrace& trace ) { store->append(trace); } void append_lib( uint32_t new_lib ) { store->append_lib(new_lib); } get_block_t get_block(uint32_t height) { return store->get_block(height); } void append_trx_ids(block_trxs_entry tt){ store->append_trx_ids(std::move(tt)); } std::shared_ptr store; }; } namespace eosio { /** * A common source for information shared between the extraction process and the RPC process */ struct trace_api_common_impl { static void set_program_options(appbase::options_description& cli, appbase::options_description& cfg) { auto cfg_options = cfg.add_options(); cfg_options("trace-dir", bpo::value()->default_value("traces"), "the location of the trace directory (absolute path or relative to application data dir)"); cfg_options("trace-slice-stride", bpo::value()->default_value(10'000), "the number of blocks each \"slice\" of trace data will contain on the filesystem"); cfg_options("trace-minimum-irreversible-history-blocks", boost::program_options::value()->default_value(-1), "Number of blocks to ensure are kept past LIB for retrieval before \"slice\" files can be automatically removed.\n" "A value of -1 indicates that automatic removal of \"slice\" files will be turned off."); cfg_options("trace-minimum-uncompressed-irreversible-history-blocks", boost::program_options::value()->default_value(-1), "Number of blocks to ensure are uncompressed past LIB. Compressed \"slice\" files are still accessible but may carry a performance loss on retrieval\n" "A value of -1 indicates that automatic compression of \"slice\" files will be turned off."); } void plugin_initialize(const appbase::variables_map& options) { auto dir_option = options.at("trace-dir").as(); if (dir_option.is_relative()) trace_dir = app().data_dir() / dir_option; else trace_dir = dir_option; if (auto resmon_plugin = app().find_plugin()) resmon_plugin->monitor_directory(trace_dir); slice_stride = options.at("trace-slice-stride").as(); const int32_t blocks = options.at("trace-minimum-irreversible-history-blocks").as(); EOS_ASSERT(blocks >= -1, chain::plugin_config_exception, "\"trace-minimum-irreversible-history-blocks\" must be greater to or equal to -1."); if (blocks > manual_slice_file_value) { minimum_irreversible_history_blocks = blocks; } const int32_t uncompressed_blocks = options.at("trace-minimum-uncompressed-irreversible-history-blocks").as(); EOS_ASSERT(uncompressed_blocks >= -1, chain::plugin_config_exception, "\"trace-minimum-uncompressed-irreversible-history-blocks\" must be greater to or equal to -1."); if (uncompressed_blocks > manual_slice_file_value) { minimum_uncompressed_irreversible_history_blocks = uncompressed_blocks; } store = std::make_shared( trace_dir, slice_stride, minimum_irreversible_history_blocks, minimum_uncompressed_irreversible_history_blocks, compression_seek_point_stride ); } void plugin_startup() { store->start_maintenance_thread([](const std::string& msg ){ fc_dlog( _log, msg ); }); } void plugin_shutdown() { store->stop_maintenance_thread(); } // common configuration paramters std::filesystem::path trace_dir; uint32_t slice_stride = 0; std::optional minimum_irreversible_history_blocks; std::optional minimum_uncompressed_irreversible_history_blocks; static constexpr int32_t manual_slice_file_value = -1; static constexpr uint32_t compression_seek_point_stride = 6 * 1024 * 1024; // 6 MiB strides for clog seek points std::shared_ptr store; }; /** * Interface with the RPC process */ struct trace_api_rpc_plugin_impl : public std::enable_shared_from_this { explicit trace_api_rpc_plugin_impl( const std::shared_ptr& common ) :common(common) {} static void set_program_options(appbase::options_description& cli, appbase::options_description& cfg) { auto cfg_options = cfg.add_options(); cfg_options("trace-rpc-abi", bpo::value>()->composing(), "ABIs used when decoding trace RPC responses.\n" "There must be at least one ABI specified OR the flag trace-no-abis must be used.\n" "ABIs are specified as \"Key=Value\" pairs in the form =\n" "Where can be:\n" " an absolute path to a file containing a valid JSON-encoded ABI\n" " a relative path from `data-dir` to a file containing a valid JSON-encoded ABI\n" ); cfg_options("trace-no-abis", "Use to indicate that the RPC responses will not use ABIs.\n" "Failure to specify this option when there are no trace-rpc-abi configuations will result in an Error.\n" "This option is mutually exclusive with trace-rpc-api" ); } void plugin_initialize(const appbase::variables_map& options) { ilog("initializing trace api rpc plugin"); std::shared_ptr data_handler = std::make_shared([](const exception_with_context& e){ log_exception(e, fc::log_level::debug); if (std::get<0>(e)) { // rethrow so caller is notified of error std::rethrow_exception(std::get<0>(e)); } }); if( options.count("trace-rpc-abi") ) { EOS_ASSERT(options.count("trace-no-abis") == 0, chain::plugin_config_exception, "Trace API is configured with ABIs however trace-no-abis is set"); const std::vector key_value_pairs = options["trace-rpc-abi"].as>(); for (const auto& entry : key_value_pairs) { try { auto kv = parse_kv_pairs(entry); auto account = chain::name(kv.first); auto abi = abi_def_from_file(kv.second, app().data_dir()); data_handler->add_abi(account, std::move(abi)); } catch (...) { elog("Malformed trace-rpc-abi provider: \"${val}\"", ("val", entry)); throw; } } } else { EOS_ASSERT(options.count("trace-no-abis") != 0, chain::plugin_config_exception, "Trace API is not configured with ABIs and trace-no-abis is not set"); } req_handler = std::make_shared( shared_store_provider(common->store), abi_data_handler::shared_provider(data_handler), [](const std::string& msg ) { fc_dlog( _log, msg ); } ); } void plugin_startup() { auto& http = app().get_plugin(); http.add_async_handler({"/v1/trace_api/get_block", api_category::trace_api, [this](std::string, std::string body, url_response_callback cb) { auto block_number = ([&body]() -> std::optional { if (body.empty()) { return {}; } try { auto input = fc::json::from_string(body); auto block_num = input.get_object()["block_num"].as_uint64(); if (block_num > std::numeric_limits::max()) { return {}; } return block_num; } catch (...) { return {}; } })(); if (!block_number) { error_results results{400, "Bad or missing block_num"}; cb( 400, fc::variant( results )); return; } try { auto resp = req_handler->get_block_trace(*block_number); if (resp.is_null()) { error_results results{404, "Trace API: block trace missing"}; cb( 404, fc::variant( results )); } else { cb( 200, std::move(resp) ); } } catch (...) { http_plugin::handle_exception("trace_api", "get_block", body, cb); } }}); http.add_async_handler({"/v1/trace_api/get_transaction_trace", api_category::trace_api, [this](std::string, std::string body, url_response_callback cb) { auto trx_id = ([&body]() -> std::optional { if (body.empty()) { return {}; } try { auto input = fc::json::from_string(body); auto trxid = input.get_object()["id"].as_string(); if (trxid.size() < 8 || trxid.size() > 64) { return {}; } return transaction_id_type(trxid); } catch (...) { return {}; } })(); if (!trx_id) { error_results results{400, "Bad or missing transaction ID"}; cb( 400, fc::variant( results )); return; } try { // search for the block that contains the transaction get_block_n blk_num = common->store->get_trx_block_number(*trx_id); if (!blk_num.has_value()){ error_results results{404, "Trace API: transaction id missing in the transaction id log files"}; cb( 404, fc::variant( results )); } else { auto resp = req_handler->get_transaction_trace(*trx_id, *blk_num); if (resp.is_null()) { error_results results{404, "Trace API: transaction trace missing"}; cb( 404, fc::variant( results )); } else { cb( 200, std::move(resp) ); } } } catch (...) { http_plugin::handle_exception("trace_api", "get_transaction", body, cb); } }}); } void plugin_shutdown() { } std::shared_ptr common; using request_handler_t = request_handler, abi_data_handler::shared_provider>; std::shared_ptr req_handler; }; struct trace_api_plugin_impl { explicit trace_api_plugin_impl( const std::shared_ptr& common ) :common(common) {} void plugin_initialize(const appbase::variables_map& options) { ilog("initializing trace api plugin"); auto log_exceptions_and_shutdown = [](const exception_with_context& e) { log_exception(e, fc::log_level::error); app().quit(); throw yield_exception("shutting down"); }; extraction = std::make_shared(shared_store_provider(common->store), log_exceptions_and_shutdown); auto& chain = app().find_plugin()->chain(); applied_transaction_connection.emplace( chain.applied_transaction().connect([this](std::tuple t) { emit_killer([&](){ extraction->signal_applied_transaction(std::get<0>(t), std::get<1>(t)); }); })); block_start_connection.emplace( chain.block_start().connect([this](uint32_t block_num) { emit_killer([&](){ extraction->signal_block_start(block_num); }); })); accepted_block_connection.emplace( chain.accepted_block().connect([this](const chain::block_signal_params& t) { emit_killer([&](){ const auto& [ block, id ] = t; extraction->signal_accepted_block(block, id); }); })); irreversible_block_connection.emplace( chain.irreversible_block().connect([this](const chain::block_signal_params& t) { const auto& [ block, id ] = t; emit_killer([&](){ extraction->signal_irreversible_block(block->block_num()); }); })); } void plugin_startup() { common->plugin_startup(); } void plugin_shutdown() { common->plugin_shutdown(); } std::shared_ptr common; using chain_extraction_t = chain_extraction_impl_type>; std::shared_ptr extraction; std::optional applied_transaction_connection; std::optional block_start_connection; std::optional accepted_block_connection; std::optional irreversible_block_connection; }; trace_api_plugin::trace_api_plugin() = default; trace_api_plugin::~trace_api_plugin() = default; void trace_api_plugin::set_program_options(appbase::options_description& cli, appbase::options_description& cfg) { trace_api_common_impl::set_program_options(cli, cfg); trace_api_rpc_plugin_impl::set_program_options(cli, cfg); } void trace_api_plugin::plugin_initialize(const appbase::variables_map& options) { handle_sighup(); // setup logging auto common = std::make_shared(); common->plugin_initialize(options); my = std::make_shared(common); my->plugin_initialize(options); rpc = std::make_shared(common); rpc->plugin_initialize(options); } void trace_api_plugin::plugin_startup() { my->plugin_startup(); rpc->plugin_startup(); } void trace_api_plugin::plugin_shutdown() { my->plugin_shutdown(); rpc->plugin_shutdown(); fc_dlog( _log, "exit shutdown"); } void trace_api_plugin::handle_sighup() { fc::logger::update( logger_name, _log ); } trace_api_rpc_plugin::trace_api_rpc_plugin() = default; trace_api_rpc_plugin::~trace_api_rpc_plugin() = default; void trace_api_rpc_plugin::set_program_options(appbase::options_description& cli, appbase::options_description& cfg) { trace_api_common_impl::set_program_options(cli, cfg); trace_api_rpc_plugin_impl::set_program_options(cli, cfg); } void trace_api_rpc_plugin::plugin_initialize(const appbase::variables_map& options) { auto common = std::make_shared(); common->plugin_initialize(options); rpc = std::make_shared(common); rpc->plugin_initialize(options); } void trace_api_rpc_plugin::plugin_startup() { rpc->plugin_startup(); } void trace_api_rpc_plugin::plugin_shutdown() { rpc->plugin_shutdown(); } void trace_api_rpc_plugin::handle_sighup() { fc::logger::update( logger_name, _log ); } }