dncurrency/nano/node/stats.hpp

493 lines
14 KiB
C++

#pragma once
#include <atomic>
#include <boost/circular_buffer.hpp>
#include <boost/property_tree/ptree.hpp>
#include <chrono>
#include <map>
#include <memory>
#include <mutex>
#include <nano/lib/errors.hpp>
#include <nano/lib/jsonconfig.hpp>
#include <nano/lib/utility.hpp>
#include <string>
#include <unordered_map>
namespace nano
{
class node;
/**
* Serialize and deserialize the 'statistics' node from config.json
* All configuration values have defaults. In particular, file logging of statistics
* is disabled by default.
*/
class stat_config
{
public:
/** Reads the JSON statistics node */
nano::error deserialize_json (nano::jsonconfig & json);
/** If true, sampling of counters is enabled */
bool sampling_enabled{ false };
/** How many sample intervals to keep in the ring buffer */
size_t capacity{ 0 };
/** Sample interval in milliseconds */
size_t interval{ 0 };
/** How often to log sample array, in milliseconds. Default is 0 (no logging) */
size_t log_interval_samples{ 0 };
/** How often to log counters, in milliseconds. Default is 0 (no logging) */
size_t log_interval_counters{ 0 };
/** Maximum number of log outputs before rotating the file */
size_t log_rotation_count{ 100 };
/** If true, write headers on each counter or samples writeout. The header contains log type and the current wall time. */
bool log_headers{ true };
/** Filename for the counter log */
std::string log_counters_filename{ "counters.stat" };
/** Filename for the sampling log */
std::string log_samples_filename{ "samples.stat" };
};
/** Value and wall time of measurement */
class stat_datapoint
{
public:
stat_datapoint () = default;
stat_datapoint (stat_datapoint const & other_a)
{
std::lock_guard<std::mutex> lock (other_a.datapoint_mutex);
value = other_a.value;
timestamp = other_a.timestamp;
}
stat_datapoint & operator= (stat_datapoint const & other_a)
{
std::lock_guard<std::mutex> lock (other_a.datapoint_mutex);
value = other_a.value;
timestamp = other_a.timestamp;
return *this;
}
uint64_t get_value ()
{
std::lock_guard<std::mutex> lock (datapoint_mutex);
return value;
}
void set_value (uint64_t value_a)
{
std::lock_guard<std::mutex> lock (datapoint_mutex);
value = value_a;
}
std::chrono::system_clock::time_point get_timestamp ()
{
std::lock_guard<std::mutex> lock (datapoint_mutex);
return timestamp;
}
void set_timestamp (std::chrono::system_clock::time_point timestamp_a)
{
std::lock_guard<std::mutex> lock (datapoint_mutex);
timestamp = timestamp_a;
}
/** Add \addend to the current value and optionally update the timestamp */
void add (uint64_t addend, bool update_timestamp = true)
{
std::lock_guard<std::mutex> lock (datapoint_mutex);
value += addend;
if (update_timestamp)
{
timestamp = std::chrono::system_clock::now ();
}
}
private:
mutable std::mutex datapoint_mutex;
/** Value of the sample interval */
uint64_t value{ 0 };
/** When the sample was added. This is wall time (system_clock), suitable for display purposes. */
std::chrono::system_clock::time_point timestamp{ std::chrono::system_clock::now () };
};
/** Bookkeeping of statistics for a specific type/detail/direction combination */
class stat_entry
{
public:
stat_entry (size_t capacity, size_t interval) :
samples (capacity), sample_interval (interval)
{
}
/** Optional samples. Note that this doesn't allocate any memory unless sampling is configured, which sets the capacity. */
boost::circular_buffer<stat_datapoint> samples;
/** Start time of current sample interval. This is a steady clock for measuring interval; the datapoint contains the wall time. */
std::chrono::steady_clock::time_point sample_start_time{ std::chrono::steady_clock::now () };
/** Sample interval in milliseconds. If 0, sampling is disabled. */
size_t sample_interval;
/** Value within the current sample interval */
stat_datapoint sample_current;
/** Counting value for this entry, including the time of last update. This is never reset and only increases. */
stat_datapoint counter;
/** Zero or more observers for samples. Called at the end of the sample interval. */
nano::observer_set<boost::circular_buffer<stat_datapoint> &> sample_observers;
/** Observers for count. Called on each update. */
nano::observer_set<uint64_t, uint64_t> count_observers;
};
/** Log sink interface */
class stat_log_sink
{
public:
virtual ~stat_log_sink () = default;
/** Returns a reference to the log output stream */
virtual std::ostream & out () = 0;
/** Called before logging starts */
virtual void begin ()
{
}
/** Called after logging is completed */
virtual void finalize ()
{
}
/** Write a header enrty to the log */
virtual void write_header (std::string header, std::chrono::system_clock::time_point & walltime)
{
}
/** Write a counter or sampling entry to the log */
virtual void write_entry (tm & tm, std::string type, std::string detail, std::string dir, uint64_t value)
{
}
/** Rotates the log (e.g. empty file). This is a no-op for sinks where rotation is not supported. */
virtual void rotate ()
{
}
/** Returns a reference to the log entry counter */
size_t & entries ()
{
return log_entries;
}
/** Returns the string representation of the log. If not supported, an empty string is returned. */
virtual std::string to_string ()
{
return "";
}
/**
* Returns the object representation of the log result. The type depends on the sink used.
* @returns Object, or nullptr if no object result is available.
*/
virtual void * to_object ()
{
return nullptr;
}
protected:
std::string tm_to_string (tm & tm);
size_t log_entries{ 0 };
};
/**
* Collects counts and samples for inbound and outbound traffic, blocks, errors, and so on.
* Stats can be queried and observed on a type level (such as message and ledger) as well as a more
* specific detail level (such as send blocks)
*/
class stat
{
public:
/** Primary statistics type */
enum class type : uint8_t
{
traffic,
traffic_bootstrap,
error,
message,
block,
ledger,
rollback,
bootstrap,
vote,
http_callback,
peering,
ipc,
udp
};
/** Optional detail type */
enum class detail : uint8_t
{
all = 0,
// error specific
bad_sender,
insufficient_work,
http_callback,
unreachable_host,
// ledger, block, bootstrap
send,
receive,
open,
change,
state_block,
epoch_block,
fork,
// message specific
keepalive,
publish,
republish_vote,
confirm_req,
confirm_ack,
node_id_handshake,
// bootstrap, callback
initiate,
initiate_lazy,
initiate_wallet_lazy,
// bootstrap specific
bulk_pull,
bulk_push,
bulk_pull_account,
frontier_req,
// vote specific
vote_valid,
vote_replay,
vote_invalid,
vote_overflow,
// udp
blocking,
overflow,
invalid_magic,
invalid_network,
invalid_header,
invalid_message_type,
invalid_keepalive_message,
invalid_publish_message,
invalid_confirm_req_message,
invalid_confirm_ack_message,
invalid_node_id_handshake_message,
outdated_version,
// ipc
invocations,
// peering
handshake,
};
/** Direction of the stat. If the direction is irrelevant, use in */
enum class dir : uint8_t
{
in,
out
};
/** Constructor using the default config values */
stat ()
{
}
/**
* Initialize stats with a config.
* @param config Configuration object; deserialized from config.json
*/
stat (nano::stat_config config);
/**
* Call this to override the default sample interval and capacity, for a specific stat entry.
* This must be called before any stat entries are added, as part of the node initialiation.
*/
void configure (stat::type type, stat::detail detail, stat::dir dir, size_t interval, size_t capacity)
{
get_entry (key_of (type, detail, dir), interval, capacity);
}
/**
* Disables sampling for a given type/detail/dir combination
*/
void disable_sampling (stat::type type, stat::detail detail, stat::dir dir)
{
auto entry = get_entry (key_of (type, detail, dir));
entry->sample_interval = 0;
}
/** Increments the given counter */
void inc (stat::type type, stat::dir dir = stat::dir::in)
{
add (type, dir, 1);
}
/** Increments the counter for \detail, but doesn't update at the type level */
void inc_detail_only (stat::type type, stat::detail detail, stat::dir dir = stat::dir::in)
{
add (type, detail, dir, 1);
}
/** Increments the given counter */
void inc (stat::type type, stat::detail detail, stat::dir dir = stat::dir::in)
{
add (type, detail, dir, 1);
}
/** Adds \p value to the given counter */
void add (stat::type type, stat::dir dir, uint64_t value)
{
add (type, detail::all, dir, value);
}
/**
* Add \p value to stat. If sampling is configured, this will update the current sample and
* call any sample observers if the interval is over.
*
* @param type Main statistics type
* @param detail Detail type, or detail::none to register on type-level only
* @param dir Direction
* @param value The amount to add
* @param detail_only If true, only update the detail-level counter
*/
void add (stat::type type, stat::detail detail, stat::dir dir, uint64_t value, bool detail_only = false)
{
constexpr uint32_t no_detail_mask = 0xffff00ff;
uint32_t key = key_of (type, detail, dir);
update (key, value);
// Optionally update at type-level as well
if (!detail_only && (key & no_detail_mask) != key)
{
update (key & no_detail_mask, value);
}
}
/**
* Add a sampling observer for a given counter.
* The observer receives a snapshot of the current sampling. Accessing the sample buffer is thus thread safe.
* To avoid recursion, the observer callback must only use the received data point snapshop, not query the stat object.
* @param observer The observer receives a snapshot of the current samples.
*/
void observe_sample (stat::type type, stat::detail detail, stat::dir dir, std::function<void(boost::circular_buffer<stat_datapoint> &)> observer)
{
get_entry (key_of (type, detail, dir))->sample_observers.add (observer);
}
void observe_sample (stat::type type, stat::dir dir, std::function<void(boost::circular_buffer<stat_datapoint> &)> observer)
{
observe_sample (type, stat::detail::all, dir, observer);
}
/**
* Add count observer for a given type, detail and direction combination. The observer receives old and new value.
* To avoid recursion, the observer callback must only use the received counts, not query the stat object.
* @param observer The observer receives the old and the new count.
*/
void observe_count (stat::type type, stat::detail detail, stat::dir dir, std::function<void(uint64_t, uint64_t)> observer)
{
get_entry (key_of (type, detail, dir))->count_observers.add (observer);
}
/** Returns a potentially empty list of the last N samples, where N is determined by the 'capacity' configuration */
boost::circular_buffer<stat_datapoint> * samples (stat::type type, stat::detail detail, stat::dir dir)
{
return &get_entry (key_of (type, detail, dir))->samples;
}
/** Returns current value for the given counter at the type level */
uint64_t count (stat::type type, stat::dir dir = stat::dir::in)
{
return count (type, stat::detail::all, dir);
}
/** Returns current value for the given counter at the detail level */
uint64_t count (stat::type type, stat::detail detail, stat::dir dir = stat::dir::in)
{
return get_entry (key_of (type, detail, dir))->counter.get_value ();
}
/** Returns the number of seconds since clear() was last called, or node startup if it's never called. */
std::chrono::seconds last_reset ();
/** Clear all stats */
void clear ();
/** Log counters to the given log link */
void log_counters (stat_log_sink & sink);
/** Log samples to the given log sink */
void log_samples (stat_log_sink & sink);
/** Returns a new JSON log sink */
std::unique_ptr<stat_log_sink> log_sink_json ();
/** Returns a new file log sink */
std::unique_ptr<stat_log_sink> log_sink_file (std::string filename);
private:
static std::string type_to_string (uint32_t key);
static std::string detail_to_string (uint32_t key);
static std::string dir_to_string (uint32_t key);
/** Constructs a key given type, detail and direction. This is used as input to update(...) and get_entry(...) */
uint32_t key_of (stat::type type, stat::detail detail, stat::dir dir) const
{
return static_cast<uint8_t> (type) << 16 | static_cast<uint8_t> (detail) << 8 | static_cast<uint8_t> (dir);
}
/** Get entry for key, creating a new entry if necessary, using interval and sample count from config */
std::shared_ptr<nano::stat_entry> get_entry (uint32_t key);
/** Get entry for key, creating a new entry if necessary */
std::shared_ptr<nano::stat_entry> get_entry (uint32_t key, size_t sample_interval, size_t max_samples);
/** Unlocked implementation of get_entry() */
std::shared_ptr<nano::stat_entry> get_entry_impl (uint32_t key, size_t sample_interval, size_t max_samples);
/**
* Update count and sample and call any observers on the key
* @param key a key constructor from stat::type, stat::detail and stat::direction
* @value Amount to add to the counter
*/
void update (uint32_t key, uint64_t value);
/** Unlocked implementation of log_counters() to avoid using recursive locking */
void log_counters_impl (stat_log_sink & sink);
/** Unlocked implementation of log_samples() to avoid using recursive locking */
void log_samples_impl (stat_log_sink & sink);
/** Time of last clear() call */
std::chrono::steady_clock::time_point timestamp{ std::chrono::steady_clock::now () };
/** Configuration deserialized from config.json */
nano::stat_config config;
/** Stat entries are sorted by key to simplify processing of log output */
std::map<uint32_t, std::shared_ptr<nano::stat_entry>> entries;
std::chrono::steady_clock::time_point log_last_count_writeout{ std::chrono::steady_clock::now () };
std::chrono::steady_clock::time_point log_last_sample_writeout{ std::chrono::steady_clock::now () };
/** All access to stat is thread safe, including calls from observers on the same thread */
std::mutex stat_mutex;
};
}