diff --git a/wrenchboard/build/config.m4 b/wrenchboard/build/config.m4 index 9329dab0..00424ea9 100644 --- a/wrenchboard/build/config.m4 +++ b/wrenchboard/build/config.m4 @@ -2,13 +2,25 @@ PHP_ARG_ENABLE(wrenchboard_api, [Whether to enable the "wrenchboard_api" extension], [ --enable-wrenchboard_api Enable "wrenchboard_api" extension support]) +# *** Mac: +# brew install rapidjson boost +# c++ -DRAPIDJSON_HAS_STDSTRING=1 -I./include -I/usr/include -I/opt/homebrew/include -std=gnu++14 -o main.cpp.o -c main.cpp +# c++ main.cpp.o -o main -L/opt/homebrew/lib -lboost_system-mt -lboost_iostreams-mt -lpthread + +# *** Linux: +# dnf install rapidjson-devel boost boost-devel # centos +# apt install rapidjson-dev libboost-all-dev # ubuntu +# c++ -DRAPIDJSON_HAS_STDSTRING=1 -I./include -I/usr/include -std=gnu++14 -o main.cpp.o -c main.cpp +# c++ main.cpp.o -o main -lboost_system -lboost_iostreams -lpthread + + if test $PHP_WRENCHBOARD_API != "no"; then PHP_REQUIRE_CXX() PHP_SUBST(WRENCHBOARD_API_SHARED_LIBADD) - PHP_ADD_LIBRARY(stdc++, 1, WRENCHBOARD_API_SHARED_LIBADD) PHP_ADD_INCLUDE(/usr/include/pgsql) PHP_ADD_INCLUDE(/usr/include/postgresql) PHP_ADD_LIBRARY_WITH_PATH(pq, /usr/lib64, WRENCHBOARD_API_SHARED_LIBADD) + PHP_ADD_LIBRARY(stdc++, 1, WRENCHBOARD_API_SHARED_LIBADD) PHP_ADD_LIBRARY(curl, 1, WRENCHBOARD_API_SHARED_LIBADD) PHP_ADD_LIBRARY(config++, 1, WRENCHBOARD_API_SHARED_LIBADD) PHP_ADD_LIBRARY(json-c, 1, WRENCHBOARD_API_SHARED_LIBADD) @@ -16,6 +28,9 @@ if test $PHP_WRENCHBOARD_API != "no"; then PHP_ADD_LIBRARY(z, 1, WRENCHBOARD_API_SHARED_LIBADD) PHP_ADD_LIBRARY(pcrecpp, 1, WRENCHBOARD_API_SHARED_LIBADD) PHP_ADD_LIBRARY(gcc_s, 1, WRENCHBOARD_API_SHARED_LIBADD) + PHP_ADD_LIBRARY(boost_system, 1, WRENCHBOARD_API_SHARED_LIBADD) + PHP_ADD_LIBRARY(boost_iostreams, 1, WRENCHBOARD_API_SHARED_LIBADD) + PHP_ADD_LIBRARY(pthread, 1, WRENCHBOARD_API_SHARED_LIBADD) #LIBCFG_DIR="`pwd`/libconfig-1.3.1" @@ -25,7 +40,7 @@ if test $PHP_WRENCHBOARD_API != "no"; then wrenchboard_api, core/*.cc shared_tool/*.cc, $ext_shared, - ,-I@ext_srcdir@/libconfig-1.0.1) + ,-I@ext_srcdir@/include/gelfcpp -std=gnu++14 -DRAPIDJSON_HAS_STDSTRING=1) PHP_DEFINE([WRENCHBOARD_API_NS],[\"wrenchboard_api\"],[]) PHP_DEFINE([WRENCHBOARD_CONFIG],[\"`pwd|sed s/src//`etc/\"],[]) PHP_DEFINE([WRENCHBOARD_LOG],[\"`pwd|sed s/src//`logs/wrenchboard_api.log\"],[]) diff --git a/wrenchboard/src/core/clog.cc b/wrenchboard/src/core/clog.cc index fa9f291c..a5efb863 100644 --- a/wrenchboard/src/core/clog.cc +++ b/wrenchboard/src/core/clog.cc @@ -1,7 +1,12 @@ #include "clog.h" #include "php_wrenchboard_log.h" +//gelfcpp::output::GelfUDPOutput graylog("10.0.0.112", 12201); + void logfmt( TLogLevel level, const char * format, ... ) { + size_t n = 32678; + char buffer[n]; + bzero(buffer, n); try { //FILELog::ReportingLevel() = level; va_list args; @@ -15,10 +20,19 @@ void logfmt( TLogLevel level, const char * format, ... ) { FILELog::ToString(level).c_str(), getpid(), (level > logDEBUG ? level - logDEBUG : 0, '\t')); - vfprintf(f, format, args); + vsnprintf (buffer, n, format, args); + fprintf(f, buffer); fprintf(f, "\n"); } va_end (args); + // Graylog + /*if (strlen(buffer) > 0) { + GELF_MESSAGE(graylog) + (CurrentTimeStamp()) + ("level", FILELog::ToString(level).c_str()) + ("pid", getpid()) + (buffer); + } // */ } catch(const std::exception& e) { FILE_LOG(logERROR) << e.what(); } @@ -40,3 +54,36 @@ void logfmt( TLogLevel level, const char * format, ... ) { } */ +/* void GraylogStream(std::string raw) { + + std::string marker1 ("]: "); + std::string marker2 (" ["); + std::string marker3 (" "); + + std::size_t found = raw.find(marker1); + if (found != std::string::npos) { + std::string msg = raw.substr (found + 3); + std::string level ("FLOG_MAX"); + + found = raw.find(marker2); + if (found != std::string::npos) { + std::string str = raw.substr(0, found - 1); + boost::trim_right(str); + found = str.find(marker3); + while (found != std::string::npos) { + level = str.substr(found); + found = str.find(marker3); + } + } + + if (!msg.empty()) { + GELF_MESSAGE(graylog) + (CurrentTimeStamp()) + ("pid", getpid()) + ("level", level.c_str()) + (msg.c_str()); + } + } +} +// */ + diff --git a/wrenchboard/src/include/clog.h b/wrenchboard/src/include/clog.h index fecc959e..96746c34 100644 --- a/wrenchboard/src/include/clog.h +++ b/wrenchboard/src/include/clog.h @@ -4,6 +4,12 @@ #include "php_wrenchboard_log.h" #include "php_filelog_max_level.h" +#include +#include +#include + +#include + #include #include #include @@ -12,6 +18,8 @@ #include inline std::string NowTime(); +inline gelfcpp::decorator::CurrentTimestamp CurrentTimeStamp(); +void GraylogStream(std::string raw); enum TLogLevel {logERROR, logWARNING, logINFO, logDEBUG, logDEBUG1, logDEBUG2, logDEBUG3, logDEBUG4, logSQL, FLOG_MAX}; @@ -45,7 +53,7 @@ std::ostringstream& Log::Get(TLogLevel level) { os << "- " << NowTime(); os << " " << ToString(level); - os << " [" << getpid() << "]: "; + os << " [" << getpid() << "]: "; os << std::string(level > logDEBUG ? level - logDEBUG : 0, '\t'); return os; } @@ -53,6 +61,9 @@ std::ostringstream& Log::Get(TLogLevel level) template Log::~Log() { + // Graylog + //GraylogStream(os.str()); + os << std::endl; T::Output(os.str()); } @@ -182,6 +193,11 @@ inline std::string NowTime() #endif //WIN32 +inline gelfcpp::decorator::CurrentTimestamp CurrentTimeStamp() +{ + gelfcpp::decorator::CurrentTimestamp timestamp; + return timestamp; +} #endif //__LOG_H__ diff --git a/wrenchboard/src/include/gelfcpp/GelfMessage.hpp b/wrenchboard/src/include/gelfcpp/GelfMessage.hpp new file mode 100644 index 00000000..a9f94904 --- /dev/null +++ b/wrenchboard/src/include/gelfcpp/GelfMessage.hpp @@ -0,0 +1,176 @@ +#pragma once + +#include +#include +#include +#include + + +namespace gelfcpp +{ + +namespace detail +{ +struct DocumentAccessor; +} + +/** + * \brief A single GELF message, with write-only access to its fields. + * + * \note This class only supports GELF version 1.1 and treats deprecated standard fields as normal additional fields. + * + * Currently the following standard fields are handled: version, host, short_message, full_message, timestamp, level. + * Standard fields are included "as-is". All other fields are prefixed with "_". + */ +class GelfMessage +{ + friend struct detail::DocumentAccessor; + + struct FieldSetter + { + GelfMessage& owner; + std::string field; + + template + void operator=(T&& value) + { + owner.SetField(field, std::forward(value)); + } + }; + +public: + GelfMessage() : + doc_(rapidjson::kObjectType) + { + SetField("version", "1.1"); + } + + /** + * \brief Quick accessor for short_message field. + * + * Behaves like \code SetField("short_message", message). \endcode + * + * \param message message + */ + void SetMessage(const std::string& message) + { + SetField("short_message", message); + } + + /** + * \brief Quick accessor for full_message field. + * + * Behaves like \code SetField("full_message", message). \endcode + * + * \param message message + */ + void SetFullMessage(const std::string& message) + { + SetField("full_message", message); + } + + /** + * \brief Quick accessor for host field. + * + * Behaves like \code SetField("host", message). \endcode + * + * \param host hostname + */ + void SetHost(const std::string& host) + { + SetField("host", host); + } + + /** + * \brief Quick accessor for timestamp field. + * + * Behaves like \code SetField("timestamp", message). \endcode + * + * \param timestamp timestamp + */ + void SetTimestamp(double timestamp) + { + SetField("timestamp", timestamp); + } + +#ifndef GELFCPP_DOXYGEN_RUNNING + + template + auto SetField(const std::string& name, T value) -> std::enable_if_t::value && !std::is_same::value> + { + rapidjson::Value json(std::forward(value)); + SetField(name, std::move(json)); + } + + void SetField(const std::string& name, bool value) + { + SetField(name, value ? 1 : 0); + } + + void SetField(const std::string& name, const char* value) + { + rapidjson::Value json(value, doc_.GetAllocator()); + SetField(name, std::move(json)); + } + + void SetField(const std::string& name, const std::string& value) + { + rapidjson::Value json(value, doc_.GetAllocator()); + SetField(name, std::move(json)); + } + +#else + /** + * \brief Adds a field to the message. + * + * \note If a field was already assigned the old value is overwritten. + * + * \tparam T value type, supported: bool, integral, floating point, strings + * \param name field name, the "_" prefix is added automatically + * \param value field value of any supported type + */ + template void SetField(const std::string& name, T value) {} +#endif + + /** + * \brief Adds a field to the message. + * + * This behaves like SetField(const std::string&, T) but allows map/array like syntax. + * + * Example usage: + * \code{.cpp} + * GelfMessage message; + * message["field_name"] = field_value; + * \endcode + * + * \param field field name, the "_" prefix is added automatically + * \return wrapper to allow assignment, see the usage example. + */ + FieldSetter operator[](const std::string& field) + { + return { *this, field }; + } + +private: + void SetField(const std::string& field, rapidjson::Value&& value) + { + static const std::unordered_set BUILDIN_FIELDS{ "version", "host", "short_message", "full_message", "timestamp", "level" }; + + rapidjson::Value key; + if (BUILDIN_FIELDS.find(field) != BUILDIN_FIELDS.end()) + key.SetString(field.c_str(), static_cast(field.size()), doc_.GetAllocator()); + else + key.SetString(("_" + field).c_str(), static_cast(field.size() + 1), doc_.GetAllocator()); + + auto find = doc_.FindMember(key); + if (find != doc_.MemberEnd()) + find->value = std::move(value); + else + doc_.AddMember(std::move(key), std::move(value), doc_.GetAllocator()); + } + +private: + rapidjson::Document doc_; +}; + +} diff --git a/wrenchboard/src/include/gelfcpp/GelfMessageBuilder.hpp b/wrenchboard/src/include/gelfcpp/GelfMessageBuilder.hpp new file mode 100644 index 00000000..f4495d7b --- /dev/null +++ b/wrenchboard/src/include/gelfcpp/GelfMessageBuilder.hpp @@ -0,0 +1,144 @@ +#pragma once + +#include +#include +#include + + +namespace gelfcpp +{ + +/** + * \brief Builder class for GelfMessage instances. + * + * Allows convenient construction for GelfMessages with additional Decorator support. + * The builder is implicitly convertible to a GelfMessage and can be directly passed to an Output. + * + * \note The internal GelfMessage is not copy-constructible, thus \code GelfMessage msg = builder; \endcode is invalid! + * But move-construction from an temporary GelfMessageBuilder is allowed. + * + * Example usage: + * \code + * GelfMessageBuilder builder; + * builder("This is the short_message"); + * builder("field_1", true)("field_2", "chains are possible too!"); + * + * output->Write(builder); + * \endcode + * + * Decorators are also supported. + * A decorator is a functional having the following syntax: + * \code + * void operator()(GelfMessage& message) + * \endcode + * This allows to dynamically set common values via a globally defined decorator. + * Common use cases are e.g. adding the current timestamp, setting the "host" field. + * + * Example useage: + * \code + * auto add_common_fields = [](GelfMessage& message) + * { + * message.SetTimestamp(std::time(nullptr)); + * message.SetHost("my-host"); + * }; + * builder(add_common_fields); + * \endcode + */ +struct GelfMessageBuilder +{ +public: +#ifndef GELFCPP_DOXYGEN_RUNNING + + GelfMessageBuilder&& operator()(const std::string& message) && + { + message_.SetMessage(message); + return std::move(*this); + } + + template + GelfMessageBuilder&& operator()(const std::string& field, T&& value) && + { + message_.SetField(field, std::forward(value)); + return std::move(*this); + } + + GelfMessageBuilder& operator()(const std::string& message) & + { + message_.SetMessage(message); + return *this; + } + + template + GelfMessageBuilder& operator()(const std::string& field, T&& value) & + { + message_.SetField(field, std::forward(value)); + return *this; + } + + template()(std::declval()))> + GelfMessageBuilder& operator()(Decorator&& decorator) & + { + std::forward(decorator)(message_); + return *this; + } + + template()(std::declval()))> + GelfMessageBuilder&& operator()(Decorator&& decorator) && + { + std::forward(decorator)(message_); + return std::move(*this); + } + + operator const GelfMessage&() const& + { + return message_; + } + + operator GelfMessage&&() && + { + return std::move(message_); + } + +#else + /** + * \brief Sets the short_message field + * + * Equivalent to \c gelf_message.SetMessage(message). + * + * \param message message + * \return \c *this , for chaining + * \see GelfMessage::SetMessage() + */ + GelfMessageBuilder& operator()(const std::string& message) {} + + /** + * \brief Sets a field to the given value + * + * Equivalent to \c gelf_message.SetField(field, value). + * + * \tparam T any supported field type, \see GelfMessage::SetField() + * \param field field name + * \param value field value + * \return \c *this , for chaining + * \see GelfMessage::SetField() + */ + template GelfMessageBuilder& operator()(const std::string& field, T value) {} + + /** + * \brief Applies a decorator to the message + * + * Equivalent to \c decorator(gelf_message). + * + * \tparam Decorator decorator type + * \param decorator decorator function + * \return \c *this , for chaining + */ + template GelfMessageBuilder& operator()(Decorator decorator) {} +#endif + +private: + GelfMessage message_; +}; + + +} diff --git a/wrenchboard/src/include/gelfcpp/GelfMessageStream.hpp b/wrenchboard/src/include/gelfcpp/GelfMessageStream.hpp new file mode 100644 index 00000000..9bb1f7b4 --- /dev/null +++ b/wrenchboard/src/include/gelfcpp/GelfMessageStream.hpp @@ -0,0 +1,94 @@ +#pragma once + +#include +#include +#include + + +namespace gelfcpp +{ + +/** + * \brief Allows stream like sending of GelfMessage + * + * Usually used in conjunction with a convenience macro like GELF_MESSAGE to allow conditional sending. + * + * Allows contextual-conversion to bool to check wether the output is valid. + * An output is valid if it can accept messages, meaning: + * * output is a reference to an output type + * * output is a non-null pointer to an output type + * * output is a valid smart pointer (shared, unique) to an output type + * + * \note + * Send explicitly requires to provide the output again, since it is not stored inside the stream. + * This is due to the need to support arbitrary, non-virtual output types. + * + * Usage example: + * \code + * GelfMessageStream stream(output); + * if (stream) + * { + * stream.Send(output) = message; + * // or + * stream.Send(output) << message_1 << message_2; + * } + * \endcode + */ +struct GelfMessageStream +{ +public: + /** + * \brief Creates a new stream + * + * Checks wether the output is valid and thus invalidates this stream. + * + * \tparam T output type + * \param output output + */ + template + explicit GelfMessageStream(T&& output) : + valid_(detail::Sender>::IsValid(output)) {} + + /** + * \brief Checks if this stream is associated to a valid output + * + * \return \c true if output is valid, \c false otherwise + */ + explicit operator bool() + { + return valid_; + } + + /** + * \brief Opens a stream to an output + * + * \note After calling Send() once this stream is considered invalid. + * + * \tparam T output type + * \param output output + * \return Sink for GelfMessages, which are send to the output + */ + template + detail::Sender> Send(T&& output) + { + valid_ = false; + return { std::forward(output) }; + } + +private: + bool valid_; +}; +} + +/** + * \ingroup Marcos + * \brief Convenience macro for easy stream-like creation and sending of GelfMessages + * + * Usage: + * \code + * GELF_MESSAGE()("short_message")("field_1", value); + * \endcode + */ +#define GELF_MESSAGE(output) \ + for (::gelfcpp::GelfMessageStream stream(output); stream ;) \ + stream.Send(output) = ::gelfcpp::GelfMessageBuilder() diff --git a/wrenchboard/src/include/gelfcpp/decorator/Host.hpp b/wrenchboard/src/include/gelfcpp/decorator/Host.hpp new file mode 100644 index 00000000..b9a4a7fa --- /dev/null +++ b/wrenchboard/src/include/gelfcpp/decorator/Host.hpp @@ -0,0 +1,56 @@ +#pragma once + +#include +#include + + +namespace gelfcpp +{ +namespace decorator +{ + +/** + * \brief Decorator which appends the "hostname" + * + * Defaults to the current hostname of the system. + */ +class Host +{ +public: + /** + * \brief Uses the systems current hostname + */ + Host() : Host(boost::asio::ip::host_name()) {} + + /** + * \brief Explicitly sets the hostname + * \param host the hostname + */ + Host(const std::string& host) : host_(host) {} + + /** + * \brief Resets the hostname to the system current hostname + */ + void SetDefault() { SetHost(boost::asio::ip::host_name()); } + + /** + * \brief Explicitly sets the hostname + * \param host the hostname + */ + void SetHost(const std::string& host) { host_ = host; } + + /** + * \brief Decorator apply operator + * \param message the message + */ + void operator()(GelfMessage& message) const + { + message.SetField("host", host_); + } + +private: + std::string host_; +}; + +} +} diff --git a/wrenchboard/src/include/gelfcpp/decorator/StaticDecoratorSet.hpp b/wrenchboard/src/include/gelfcpp/decorator/StaticDecoratorSet.hpp new file mode 100644 index 00000000..ad7e95a5 --- /dev/null +++ b/wrenchboard/src/include/gelfcpp/decorator/StaticDecoratorSet.hpp @@ -0,0 +1,76 @@ +#pragma once + +#include +#include + + +namespace gelfcpp +{ +namespace decorator +{ + +/** + * \brief Statically combines multiple decorators + * + * \note The decorators are applied in definition order. + * + * \tparam Decorators decorators that should be applied + */ +template +class StaticDecoratorSet +{ + using DecoratorSet = std::tuple; +public: + /** + * \brief Decorator apply operator + * \param message the message + */ + void operator()(GelfMessage& message) + { + operator()(message, std::make_index_sequence::value>{}); + } + +#ifndef GELFCPP_DOXYGEN_RUNNING + + template + auto get() -> std::add_lvalue_reference_t> + { + return std::get(decorators_); + } + + template + auto get() -> std::add_lvalue_reference_t + { + return std::get(decorators_); + } + +#else + /** + * \brief Accesses the decorator in the set by index + * \tparam I decorator index + * \return the decorator + */ + template Decorator& get(); + + /** + * \brief Accesses the decorator in the set by name + * \tparam T decorator type + * \return the decorator + */ + template Decorator& get(); +#endif + +private: + template + void operator()(GelfMessage& message, std::index_sequence) + { + int dummy[] = { (std::get(decorators_)(message), 0)... }; + (void) dummy; + } + +private: + DecoratorSet decorators_; +}; + +} +} diff --git a/wrenchboard/src/include/gelfcpp/decorator/Timestamp.hpp b/wrenchboard/src/include/gelfcpp/decorator/Timestamp.hpp new file mode 100644 index 00000000..8c6ba68e --- /dev/null +++ b/wrenchboard/src/include/gelfcpp/decorator/Timestamp.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include + + +namespace gelfcpp +{ +namespace decorator +{ + +/** + * \brief Decorator which appends the current timestamp + * + * Uses UnixTime with sub-second resolution. + */ +class CurrentTimestamp +{ +public: + /** + * \brief Decorator apply operator + * \param message the message + */ + void operator()(GelfMessage& message) const + { + message.SetField("timestamp", GetCurrentTime()); + } + + /** + * \brief Returns the current timestamp as UnixTime + * + * \return unixtime with sub-second resolution + */ + static double GetCurrentTime() + { + using clock = std::chrono::system_clock; + using unixtime = std::chrono::duration; + return std::chrono::duration_cast(clock::now().time_since_epoch()).count(); + } +}; + +} +} diff --git a/wrenchboard/src/include/gelfcpp/detail/DocumentAccessor.hpp b/wrenchboard/src/include/gelfcpp/detail/DocumentAccessor.hpp new file mode 100644 index 00000000..fe94c713 --- /dev/null +++ b/wrenchboard/src/include/gelfcpp/detail/DocumentAccessor.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include + + +namespace gelfcpp +{ +namespace detail +{ + +struct DocumentAccessor +{ + static const rapidjson::Document& get(const GelfMessage& message) + { + return message.doc_; + } +}; + +} +} diff --git a/wrenchboard/src/include/gelfcpp/detail/GelfSerializer.hpp b/wrenchboard/src/include/gelfcpp/detail/GelfSerializer.hpp new file mode 100644 index 00000000..7aeeb2da --- /dev/null +++ b/wrenchboard/src/include/gelfcpp/detail/GelfSerializer.hpp @@ -0,0 +1,132 @@ +#pragma once + +#include +#include + +#include +#include +#ifndef GELFCPP_NO_COMPRESSION +# include +# include +# include +# include +#endif +#include +#include +#include + + +namespace gelfcpp +{ +namespace detail +{ +/** + * \brief Handles chunking and compression + * + * \todo Merge this into GelfUDPOutput, for convenience and performance. + * + * \see GelfUDPOutput + */ +class GelfSerializer +{ + static constexpr std::size_t DEFAULT_CHUNK_SIZE = 2096; + static constexpr std::size_t DEFAULT_COMPRESS_THRESHOLD = 0; + +public: + GelfSerializer() : + random_(std::random_device{}()), + chunk_size_(DEFAULT_CHUNK_SIZE), + compress_threshold_(DEFAULT_COMPRESS_THRESHOLD) {} + + /** + * \brief Serialize JSON message into chunks, maybe gzip compressed. + * + * \param message the message + * \return list of chunks to send + */ + std::vector Serialize(const GelfMessage& message) + { + std::vector out; + + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + detail::DocumentAccessor::get(message).Accept(writer); + +#ifndef GELFCPP_NO_COMPRESSION + if (buffer.GetSize() >= compress_threshold_) + { + std::string compressed = Compress(buffer.GetString(), buffer.GetSize()); + if (compressed.size() > chunk_size_) + ToChunks(compressed.c_str(), compressed.size(), out); + else + out.emplace_back(std::move(compressed)); + } + else +#endif + { + if (buffer.GetSize() > chunk_size_) + ToChunks(buffer.GetString(), buffer.GetSize(), out); + else + out.emplace_back(buffer.GetString(), buffer.GetSize()); + } + + return out; + } + +private: +#ifndef GELFCPP_NO_COMPRESSION + + std::string Compress(const char* message, std::size_t size) + { + boost::iostreams::array_source input(message, size); + boost::iostreams::filtering_istreambuf compressor; + compressor.push(boost::iostreams::gzip_compressor()); + compressor.push(input); + std::ostringstream out; + boost::iostreams::copy(compressor, out); + return out.str(); + } + +#endif + + uint64_t GenerateMessageID() + { + uint64_t random = random_(); + uint64_t time = static_cast(std::chrono::steady_clock::now().time_since_epoch().count()); + return random ^ time; + } + + void ToChunks(const char* message, size_t size, std::vector& out) + { + static constexpr char CHUNKED_GELF_MAGIC[2] = { 0x1e, 0x0f }; + static constexpr std::size_t HEADER_SIZE = 2 + 8 + 1 + 1; + + std::size_t count = size / chunk_size_ + 1; + const char* message_ptr = message; + + uint64_t message_id = GenerateMessageID(); + + for (std::size_t i = 0; i < count; ++i, message_ptr += chunk_size_) + { + std::string chunk; + chunk.reserve(count + HEADER_SIZE); + + chunk.append(CHUNKED_GELF_MAGIC, 2); + chunk.append(reinterpret_cast(&message_id), 8); + chunk.push_back(static_cast(i & 0x7F)); + chunk.push_back(static_cast(count & 0x7F)); + + chunk.append(message_ptr, chunk_size_); + + out.emplace_back(std::move(chunk)); + } + } + +private: + std::mt19937_64 random_; + std::size_t chunk_size_; + std::size_t compress_threshold_; +}; + +} +} diff --git a/wrenchboard/src/include/gelfcpp/detail/Sender.hpp b/wrenchboard/src/include/gelfcpp/detail/Sender.hpp new file mode 100644 index 00000000..972f893d --- /dev/null +++ b/wrenchboard/src/include/gelfcpp/detail/Sender.hpp @@ -0,0 +1,109 @@ +#pragma once + +#include +#include + + +namespace gelfcpp +{ +namespace detail +{ + +template +class Sender +{ +public: + Sender(const Sender&) = delete; + Sender& operator=(const Sender&) = delete; + + static bool IsValid(const T&) + { + return true; + } + + Sender(T& output) : + output_(output) {} + + void operator=(const GelfMessage& message) + { + output_.Write(message); + } + +private: + T& output_; +}; + +template +class Sender +{ +public: + Sender(const Sender&) = delete; + Sender& operator=(const Sender&) = delete; + + static bool IsValid(const T* output) + { + return output != nullptr; + } + + Sender(T* output) : + output_(output) {} + + void operator=(const GelfMessage& message) + { + output_->Write(message); + } + +private: + T* output_; +}; + +template +class Sender> +{ +public: + Sender(const Sender&) = delete; + Sender& operator=(const Sender&) = delete; + + static bool IsValid(const std::shared_ptr& output) + { + return output != nullptr; + } + + Sender(const std::shared_ptr& output) : + output_(output) {} + + void operator=(const GelfMessage& message) + { + output_->Write(message); + } + +private: + const std::shared_ptr& output_; +}; + +template +class Sender> +{ +public: + Sender(const Sender&) = delete; + Sender& operator=(const Sender&) = delete; + + static bool IsValid(const std::unique_ptr& output) + { + return output != nullptr; + } + + Sender(const std::unique_ptr& output) : + output_(output) {} + + void operator=(const GelfMessage& message) + { + output_->Write(message); + } + +private: + const std::unique_ptr& output_; +}; + +} +} diff --git a/wrenchboard/src/include/gelfcpp/output/GelfJSONOutput.hpp b/wrenchboard/src/include/gelfcpp/output/GelfJSONOutput.hpp new file mode 100644 index 00000000..c242eea0 --- /dev/null +++ b/wrenchboard/src/include/gelfcpp/output/GelfJSONOutput.hpp @@ -0,0 +1,64 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace gelfcpp +{ +namespace output +{ +/** + * \brief Writes GelfMessages as JSON to given std::ostream + * + * Uses short notation, which minimizes the space requirements + * No newline is appended, the output is not flushed. + */ +class GelfJSONOutput +{ +public: + /** + * \brief Constructs a new output on the given stream + * + * \param output underlying stream + */ + GelfJSONOutput(std::ostream& output) : + output_(output) {} + + /** + * \brief Serializes a message to the underlying stream + * + * \param message the message + */ + void Write(const GelfMessage& message) + { + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + detail::DocumentAccessor::get(message).Accept(writer); + + output_.write(buffer.GetString(), buffer.GetSize()); + } + +private: + std::ostream& output_; +}; + +} + +/** + * \brief Serializes a message to the given stream + * + * \param os the stream + * \param message the message + * \return the stream + * \see GelfJSONOutput + */ +inline std::ostream& operator<<(std::ostream& os, const GelfMessage& message) +{ + output::GelfJSONOutput(os).Write(message); + return os; +} + +} diff --git a/wrenchboard/src/include/gelfcpp/output/GelfUDPOutput.hpp b/wrenchboard/src/include/gelfcpp/output/GelfUDPOutput.hpp new file mode 100644 index 00000000..324826c0 --- /dev/null +++ b/wrenchboard/src/include/gelfcpp/output/GelfUDPOutput.hpp @@ -0,0 +1,58 @@ +#pragma once + +#include +#include +#include +#include +#include + + +namespace gelfcpp +{ +namespace output +{ +/** + * \brief Writes GelfMessages to an UDP socket + * + * Serializes and sends messages to e.g. an Graylog Node, sending is non-blocking. + * + * Uses chunking for big messages due to size limitations of UDP. + * Additionally uses compression if enabled (by setting \c GELFCPP_WITH_COMPRESSION in CMake). + */ +class GelfUDPOutput +{ +public: + /** + * \brief Creates an new output socket to given host and port + * + * \param host remote hostname + * \param port remote UDP port + */ + GelfUDPOutput(const std::string& host, uint16_t port) + { + boost::asio::ip::udp::resolver resolver(service_); + boost::asio::ip::udp::resolver::query query(host, std::to_string(port)); + endpoint_ = *resolver.resolve(query); + socket_.reset(new boost::asio::ip::udp::socket(service_, endpoint_.protocol())); + } + + /** + * \brief Serializes a message to the UDP stream + * + * \param message the message + */ + void Write(const GelfMessage& message) + { + for (const std::string& chunk : serializer_.Serialize(message)) + socket_->async_send_to(boost::asio::buffer(chunk), endpoint_, [](const boost::system::error_code&, unsigned long int) {}); + } + +private: + detail::GelfSerializer serializer_; + boost::asio::io_context service_; + boost::asio::ip::udp::endpoint endpoint_; + std::unique_ptr socket_; +}; + +} +}