From 173caa42aa9cddcaf7acf4ee855b1b5709d3a24b Mon Sep 17 00:00:00 2001 From: Tobias Deiminger Date: Fri, 10 Sep 2021 20:54:31 +0200 Subject: [PATCH] Add a JournaldLogger As proposed in #8857, this adds a Logger subclass that writes structured log messages via journald's native protocol by calling sd_journal_sendv. The feature therefore depends on the systemd library. sd_journal_sendv is available since the early days (systemd v38), so a version check is probably superflous. We add the following fields to each record: - MESSAGE: The log message - PRIORITY (aka severity): Numeric severity as in RFC5424 section 6.2.1 - SYSLOG_FACILITY: Numeric facility as in RFC5424 section 6.2.1 - SYSLOG_IDENTIFIER: If provided, use value from configuration. Else use systemd's default behaior, which is to determine the field by using libc's program_invocation_short_name, resulting in "icinga2". - ICINGA2_FACILITY: Facility as in Log::Log(..., String facility, ...), e.g. "ApiListener" - some more fields are added automatically by systemd Fields are stored indexed, so we can do fast queries for certain field values. Example: $ journalctl -t icinga2 ICINGA2_FACILITY=ApiListener -n 5 Syslog compatiblity is ratained because good old tag, severity and facility is stored along, and systemd can forward to syslog daemons. See also https://systemd.io/JOURNAL_NATIVE_PROTOCOL/. --- lib/base/CMakeLists.txt | 3 ++ lib/base/journaldlogger.cpp | 87 +++++++++++++++++++++++++++++++++++++ lib/base/journaldlogger.hpp | 44 +++++++++++++++++++ lib/base/journaldlogger.ti | 21 +++++++++ 4 files changed, 155 insertions(+) create mode 100644 lib/base/journaldlogger.cpp create mode 100644 lib/base/journaldlogger.hpp create mode 100644 lib/base/journaldlogger.ti diff --git a/lib/base/CMakeLists.txt b/lib/base/CMakeLists.txt index 9707d2935..f8176092f 100644 --- a/lib/base/CMakeLists.txt +++ b/lib/base/CMakeLists.txt @@ -6,6 +6,7 @@ mkclass_target(configuration.ti configuration-ti.cpp configuration-ti.hpp) mkclass_target(datetime.ti datetime-ti.cpp datetime-ti.hpp) mkclass_target(filelogger.ti filelogger-ti.cpp filelogger-ti.hpp) mkclass_target(function.ti function-ti.cpp function-ti.hpp) +mkclass_target(journaldlogger.ti journaldlogger-ti.cpp journaldlogger-ti.hpp) mkclass_target(logger.ti logger-ti.cpp logger-ti.hpp) mkclass_target(perfdatavalue.ti perfdatavalue-ti.cpp perfdatavalue-ti.hpp) mkclass_target(streamlogger.ti streamlogger-ti.cpp streamlogger-ti.hpp) @@ -36,6 +37,7 @@ set(base_SOURCES function.cpp function.hpp function-ti.hpp function-script.cpp functionwrapper.hpp initialize.cpp initialize.hpp io-engine.cpp io-engine.hpp + journaldlogger.cpp journaldlogger.hpp journaldlogger-ti.hpp json.cpp json.hpp json-script.cpp lazy-init.hpp library.cpp library.hpp @@ -123,6 +125,7 @@ if(HAVE_SYSTEMD) NAMES systemd/sd-daemon.h HINTS ${SYSTEMD_ROOT_DIR}) include_directories(${SYSTEMD_INCLUDE_DIR}) + add_definitions(-DSD_JOURNAL_SUPPRESS_LOCATION) endif() add_library(base OBJECT ${base_SOURCES}) diff --git a/lib/base/journaldlogger.cpp b/lib/base/journaldlogger.cpp new file mode 100644 index 000000000..92d6af7a5 --- /dev/null +++ b/lib/base/journaldlogger.cpp @@ -0,0 +1,87 @@ +/* Icinga 2 | (c) 2021 Icinga GmbH | GPLv2+ */ + +#include "base/i2-base.hpp" +#if !defined(_WIN32) && defined(HAVE_SYSTEMD) +#include "base/journaldlogger.hpp" +#include "base/journaldlogger-ti.cpp" +#include "base/configtype.hpp" +#include "base/statsfunction.hpp" +#include "base/sysloglogger.hpp" +#include + +using namespace icinga; + +REGISTER_TYPE(JournaldLogger); + +REGISTER_STATSFUNCTION(JournaldLogger, &JournaldLogger::StatsFunc); + +void JournaldLogger::StatsFunc(const Dictionary::Ptr& status, const Array::Ptr&) +{ + DictionaryData nodes; + + for (const JournaldLogger::Ptr& journaldlogger : ConfigType::GetObjectsByType()) { + nodes.emplace_back(journaldlogger->GetName(), 1); //add more stats + } + + status->Set("journaldlogger", new Dictionary(std::move(nodes))); +} + +void JournaldLogger::OnConfigLoaded() +{ + ObjectImpl::OnConfigLoaded(); + m_ConfiguredJournalFields.clear(); + m_ConfiguredJournalFields.push_back( + String("SYSLOG_FACILITY=") + Value(SyslogHelper::FacilityToNumber(GetFacility()))); + const String identifier = GetIdentifier(); + if (!identifier.IsEmpty()) { + m_ConfiguredJournalFields.push_back(String("SYSLOG_IDENTIFIER=" + identifier)); + } +} + +void JournaldLogger::ValidateFacility(const Lazy& lvalue, const ValidationUtils& utils) +{ + ObjectImpl::ValidateFacility(lvalue, utils); + if (!SyslogHelper::ValidateFacility(lvalue())) + BOOST_THROW_EXCEPTION(ValidationError(this, { "facility" }, "Invalid facility specified.")); +} + +/** + * Processes a log entry and outputs it to journald. + * + * @param entry The log entry. + */ +void JournaldLogger::ProcessLogEntry(const LogEntry& entry) +{ + const std::vector sdFields { + String("MESSAGE=") + entry.Message.GetData(), + String("PRIORITY=") + Value(SyslogHelper::SeverityToNumber(entry.Severity)), + String("ICINGA2_FACILITY=") + entry.Facility, + }; + SystemdJournalSend(sdFields); +} + +void JournaldLogger::Flush() +{ + /* Nothing to do here. */ +} + +void JournaldLogger::SystemdJournalSend(const std::vector& varJournalFields) const +{ + struct iovec iovec[m_ConfiguredJournalFields.size() + varJournalFields.size()]; + int iovecCount = 0; + + for (const String& journalField: m_ConfiguredJournalFields) { + iovec[iovecCount] = IovecFromString(journalField); + iovecCount++; + } + for (const String& journalField: varJournalFields) { + iovec[iovecCount] = IovecFromString(journalField); + iovecCount++; + } + sd_journal_sendv(iovec, iovecCount); +} + +struct iovec JournaldLogger::IovecFromString(const String& s) { + return { const_cast(s.CStr()), s.GetLength() }; +} +#endif /* !_WIN32 && HAVE_SYSTEMD */ diff --git a/lib/base/journaldlogger.hpp b/lib/base/journaldlogger.hpp new file mode 100644 index 000000000..373dd1a60 --- /dev/null +++ b/lib/base/journaldlogger.hpp @@ -0,0 +1,44 @@ +/* Icinga 2 | (c) 2021 Icinga GmbH | GPLv2+ */ + +#ifndef JOURNALDLOGGER_H +#define JOURNALDLOGGER_H + +#include "base/i2-base.hpp" +#if !defined(_WIN32) && defined(HAVE_SYSTEMD) +#include "base/journaldlogger-ti.hpp" +#include + +namespace icinga +{ + +/** + * A logger that logs to systemd journald. + * + * @ingroup base + */ +class JournaldLogger final : public ObjectImpl +{ +public: + DECLARE_OBJECT(JournaldLogger); + DECLARE_OBJECTNAME(JournaldLogger); + + static void StaticInitialize(); + static void StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata); + + void OnConfigLoaded() override; + void ValidateFacility(const Lazy& lvalue, const ValidationUtils& utils) override; + +protected: + void SystemdJournalSend(const std::vector& varJournalFields) const; + static struct iovec IovecFromString(const String& s); + + std::vector m_ConfiguredJournalFields; + + void ProcessLogEntry(const LogEntry& entry) override; + void Flush() override; +}; + +} +#endif /* !_WIN32 && HAVE_SYSTEMD */ + +#endif /* JOURNALDLOGGER_H */ diff --git a/lib/base/journaldlogger.ti b/lib/base/journaldlogger.ti new file mode 100644 index 000000000..88e9ca1e3 --- /dev/null +++ b/lib/base/journaldlogger.ti @@ -0,0 +1,21 @@ +/* Icinga 2 | (c) 2021 Icinga GmbH | GPLv2+ */ + +#include "base/logger.hpp" + +library base; + +namespace icinga +{ + +class JournaldLogger : Logger +{ + activation_priority -100; + + [config] String facility { + default {{{ return "LOG_USER"; }}} + }; + + [config] String identifier; +}; + +}