From de7808e32c352f82d6b82a8554f08c2e8b2d1b31 Mon Sep 17 00:00:00 2001 From: Tobias Deiminger Date: Sat, 11 Sep 2021 13:48:11 +0200 Subject: [PATCH 1/6] Make syslog facility handling reusable The upcoming JournaldLogger will need the same syslog validation and conversion logic, so factor it out from SyslogLogger to make it reusable. Also explicitely include syslog.h, which defines the syslog() function. --- lib/base/sysloglogger.cpp | 88 +++++++++++++++++++++------------------ lib/base/sysloglogger.hpp | 19 ++++++++- 2 files changed, 64 insertions(+), 43 deletions(-) diff --git a/lib/base/sysloglogger.cpp b/lib/base/sysloglogger.cpp index 44babc31c..c21445eca 100644 --- a/lib/base/sysloglogger.cpp +++ b/lib/base/sysloglogger.cpp @@ -5,6 +5,7 @@ #include "base/sysloglogger-ti.cpp" #include "base/configtype.hpp" #include "base/statsfunction.hpp" +#include using namespace icinga; @@ -12,11 +13,11 @@ REGISTER_TYPE(SyslogLogger); REGISTER_STATSFUNCTION(SyslogLogger, &SyslogLogger::StatsFunc); -INITIALIZE_ONCE(&SyslogLogger::StaticInitialize); +INITIALIZE_ONCE(&SyslogHelper::StaticInitialize); -std::map SyslogLogger::m_FacilityMap; +std::map SyslogHelper::m_FacilityMap; -void SyslogLogger::StaticInitialize() +void SyslogHelper::StaticInitialize() { ScriptGlobal::Set("System.FacilityAuth", "LOG_AUTH", true); ScriptGlobal::Set("System.FacilityAuthPriv", "LOG_AUTHPRIV", true); @@ -61,6 +62,44 @@ void SyslogLogger::StaticInitialize() m_FacilityMap["LOG_UUCP"] = LOG_UUCP; } +bool SyslogHelper::ValidateFacility(const String& facility) +{ + if (m_FacilityMap.find(facility) == m_FacilityMap.end()) { + try { + Convert::ToLong(facility); + } catch (const std::exception&) { + return false; + } + } + return true; +} + +int SyslogHelper::SeverityToNumber(LogSeverity severity) +{ + switch (severity) { + case LogDebug: + return LOG_DEBUG; + case LogNotice: + return LOG_NOTICE; + case LogWarning: + return LOG_WARNING; + case LogCritical: + return LOG_CRIT; + case LogInformation: + default: + return LOG_INFO; + } +} + +int SyslogHelper::FacilityToNumber(const String& facility) +{ + auto it = m_FacilityMap.find(facility); + if (it != m_FacilityMap.end()) + return it->second; + else + return Convert::ToLong(facility); +} + void SyslogLogger::StatsFunc(const Dictionary::Ptr& status, const Array::Ptr&) { DictionaryData nodes; @@ -75,28 +114,14 @@ void SyslogLogger::StatsFunc(const Dictionary::Ptr& status, const Array::Ptr&) void SyslogLogger::OnConfigLoaded() { ObjectImpl::OnConfigLoaded(); - - String facilityString = GetFacility(); - - auto it = m_FacilityMap.find(facilityString); - - if (it != m_FacilityMap.end()) - m_Facility = it->second; - else - m_Facility = Convert::ToLong(facilityString); + m_Facility = SyslogHelper::FacilityToNumber(GetFacility()); } void SyslogLogger::ValidateFacility(const Lazy& lvalue, const ValidationUtils& utils) { ObjectImpl::ValidateFacility(lvalue, utils); - - if (m_FacilityMap.find(lvalue()) == m_FacilityMap.end()) { - try { - Convert::ToLong(lvalue()); - } catch (const std::exception&) { - BOOST_THROW_EXCEPTION(ValidationError(this, { "facility" }, "Invalid facility specified.")); - } - } + if (!SyslogHelper::ValidateFacility(lvalue())) + BOOST_THROW_EXCEPTION(ValidationError(this, { "facility" }, "Invalid facility specified.")); } /** @@ -106,27 +131,8 @@ void SyslogLogger::ValidateFacility(const Lazy& lvalue, const Validation */ void SyslogLogger::ProcessLogEntry(const LogEntry& entry) { - int severity; - switch (entry.Severity) { - case LogDebug: - severity = LOG_DEBUG; - break; - case LogNotice: - severity = LOG_NOTICE; - break; - case LogWarning: - severity = LOG_WARNING; - break; - case LogCritical: - severity = LOG_CRIT; - break; - case LogInformation: - default: - severity = LOG_INFO; - break; - } - - syslog(severity | m_Facility, "%s", entry.Message.CStr()); + syslog(SyslogHelper::SeverityToNumber(entry.Severity) | m_Facility, + "%s", entry.Message.CStr()); } void SyslogLogger::Flush() diff --git a/lib/base/sysloglogger.hpp b/lib/base/sysloglogger.hpp index 168c5d9a5..d1d685907 100644 --- a/lib/base/sysloglogger.hpp +++ b/lib/base/sysloglogger.hpp @@ -10,6 +10,23 @@ namespace icinga { +/** + * Helper class to handle syslog facility strings and numbers. + * + * @ingroup base + */ +class SyslogHelper final +{ +public: + static void StaticInitialize(); + static bool ValidateFacility(const String& facility); + static int SeverityToNumber(LogSeverity severity); + static int FacilityToNumber(const String& facility); + +private: + static std::map m_FacilityMap; +}; + /** * A logger that logs to syslog. * @@ -21,14 +38,12 @@ public: DECLARE_OBJECT(SyslogLogger); DECLARE_OBJECTNAME(SyslogLogger); - 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: - static std::map m_FacilityMap; int m_Facility; void ProcessLogEntry(const LogEntry& entry) override; From 173caa42aa9cddcaf7acf4ee855b1b5709d3a24b Mon Sep 17 00:00:00 2001 From: Tobias Deiminger Date: Fri, 10 Sep 2021 20:54:31 +0200 Subject: [PATCH 2/6] 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; +}; + +} From 4d678fcef08782638f6b6ae8086c8f2f67920d75 Mon Sep 17 00:00:00 2001 From: Tobias Deiminger Date: Sun, 12 Sep 2021 15:20:04 +0200 Subject: [PATCH 3/6] Add available feature journald.conf in /etc Similar to the other loggers, add a predefined object JournaldLogger in features-available that will log messages >= warning, with default facility LOG_USER. It is disabled by default, and can be enabled with: $ sudo icing2 feature enable journald --- etc/CMakeLists.txt | 3 +++ etc/icinga2/features-available/journald.conf | 7 +++++++ 2 files changed, 10 insertions(+) create mode 100644 etc/icinga2/features-available/journald.conf diff --git a/etc/CMakeLists.txt b/etc/CMakeLists.txt index ff138bd4c..40e181a0a 100644 --- a/etc/CMakeLists.txt +++ b/etc/CMakeLists.txt @@ -39,6 +39,9 @@ install_if_not_exists(icinga2/features-available/debuglog.conf ${ICINGA2_CONFIGD install_if_not_exists(icinga2/features-available/mainlog.conf ${ICINGA2_CONFIGDIR}/features-available) if(NOT WIN32) install_if_not_exists(icinga2/features-available/syslog.conf ${ICINGA2_CONFIGDIR}/features-available) + if(HAVE_SYSTEMD) + install_if_not_exists(icinga2/features-available/journald.conf ${ICINGA2_CONFIGDIR}/features-available) + endif() else() install_if_not_exists(icinga2/features-available/windowseventlog.conf ${ICINGA2_CONFIGDIR}/features-available) endif() diff --git a/etc/icinga2/features-available/journald.conf b/etc/icinga2/features-available/journald.conf new file mode 100644 index 000000000..e0b36f7cf --- /dev/null +++ b/etc/icinga2/features-available/journald.conf @@ -0,0 +1,7 @@ +/** + * The JournaldLogger type writes log information to the systemd journal. + */ + +object JournaldLogger "journald" { + severity = "warning" +} From 1f13af957d4e7051906efd71f6e96df15409fe83 Mon Sep 17 00:00:00 2001 From: Tobias Deiminger Date: Sat, 11 Sep 2021 17:22:30 +0200 Subject: [PATCH 4/6] Update nano and vim syntax for JournaldLogger --- tools/syntax/nano/icinga2.nanorc | 2 +- tools/syntax/vim/syntax/icinga2.vim | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/syntax/nano/icinga2.nanorc b/tools/syntax/nano/icinga2.nanorc index cbc743b10..359790e9c 100644 --- a/tools/syntax/nano/icinga2.nanorc +++ b/tools/syntax/nano/icinga2.nanorc @@ -10,7 +10,7 @@ icolor brightgreen "object[ \t]+(timeperiod|scheduleddowntime|dependency|perfd icolor brightgreen "object[ \t]+(graphitewriter|idomysqlconnection|idomysqlconnection)" icolor brightgreen "object[ \t]+(livestatuslistener|statusdatawriter|externalcommandlistener)" icolor brightgreen "object[ \t]+(compatlogger|checkresultreader|checkcomponent|notificationcomponent)" -icolor brightgreen "object[ \t]+(filelogger|sysloglogger|apilistener|endpoint|zone)" +icolor brightgreen "object[ \t]+(filelogger|sysloglogger|journaldlogger|apilistener|endpoint|zone)" ## apply def icolor brightgreen "apply[ \t]+(Service|Dependency|Notification|ScheduledDowntime)" diff --git a/tools/syntax/vim/syntax/icinga2.vim b/tools/syntax/vim/syntax/icinga2.vim index 3cbb8e632..5201d6fc8 100644 --- a/tools/syntax/vim/syntax/icinga2.vim +++ b/tools/syntax/vim/syntax/icinga2.vim @@ -57,7 +57,8 @@ syn keyword icinga2ObjType Comment Dependency Downtime ElasticsearchWriter syn keyword icinga2ObjType Endpoint EventCommand ExternalCommandListener syn keyword icinga2ObjType FileLogger GelfWriter GraphiteWriter Host HostGroup syn keyword icinga2ObjType IcingaApplication IdoMysqlConnection IdoPgsqlConnection -syn keyword icinga2ObjType InfluxdbWriter Influxdb2Writer LivestatusListener Notification NotificationCommand +syn keyword icinga2ObjType InfluxdbWriter Influxdb2Writer JournaldLogger +syn keyword icinga2ObjType LivestatusListener Notification NotificationCommand syn keyword icinga2ObjType NotificationComponent OpenTsdbWriter PerfdataWriter syn keyword icinga2ObjType ScheduledDowntime Service ServiceGroup SyslogLogger syn keyword icinga2ObjType TimePeriod User UserGroup WindowsEventLogLogger Zone From 272191840f04a6de13bcd94f1101095932c74571 Mon Sep 17 00:00:00 2001 From: Tobias Deiminger Date: Sat, 11 Sep 2021 17:16:29 +0200 Subject: [PATCH 5/6] Update documentation for JournaldLogger --- doc/09-object-types.md | 27 +++++++++++++++++++++++++++ doc/14-features.md | 1 + 2 files changed, 28 insertions(+) diff --git a/doc/09-object-types.md b/doc/09-object-types.md index ca69cdb74..a4123a9e2 100644 --- a/doc/09-object-types.md +++ b/doc/09-object-types.md @@ -1754,6 +1754,33 @@ to InfluxDB. Experiment with the setting, if you are processing more than 1024 m or similar. +### JournaldLogger + +Specifies Icinga 2 logging to the systemd journal using its native interface. +This configuration object is available as `journald` [logging feature](14-features.md#logging). + +Resulting journal records have fields as described in +[journal fields](https://www.freedesktop.org/software/systemd/man/systemd.journal-fields.html), +and an additional custom field `ICINGA2_FACILITY` with the detailed message origin (e.g. "ApiListener"). + +Example: + +``` +object JournaldLogger "journald" { + severity = "warning" +} +``` + +Configuration Attributes: + +Name | Type | Description +--------------------------|-----------------------|---------------------------------- +severity | String | **Optional.** The minimum syslog compatible severity for this log. Can be "debug", "notice", "information", "warning" or "critical". Defaults to "information". +facility | String | **Optional.** Defines the syslog compatible facility to use for journal entries. This can be a facility constant like `FacilityDaemon`. Defaults to `FacilityUser`. +identifier | String | **Optional.** Defines the syslog compatible identifier (also known as "tag") to use for journal entries. If not given, systemd's default behavior is used and usually results in "icinga2". + +Facility Constants are the same as for [SyslogLogger](09-object-types.md#objecttype-sysloglogger). + ### LiveStatusListener diff --git a/doc/14-features.md b/doc/14-features.md index 5e5eea0a7..392f98aa6 100644 --- a/doc/14-features.md +++ b/doc/14-features.md @@ -14,6 +14,7 @@ and `icinga2 feature disable` commands to configure loggers: Feature | Description ----------------|------------ debuglog | Debug log (path: `/var/log/icinga2/debug.log`, severity: `debug` or higher) +journald | Systemd Journal (severity: `warning` or higher) mainlog | Main log (path: `/var/log/icinga2/icinga2.log`, severity: `information` or higher) syslog | Syslog (severity: `warning` or higher) windowseventlog | Windows Event Log (severity: `information` or higher) From eb8f67335e0ea61fe7d1e8a8a6bec92a16e894da Mon Sep 17 00:00:00 2001 From: Tobias Deiminger Date: Tue, 14 Sep 2021 21:49:55 +0200 Subject: [PATCH 6/6] Define SD_JOURNAL_SUPPRESS_LOCATION more locally add_definitions would set SD_JOURNAL_SUPPRESS_LOCATION for all targets in directory and sub-directories. However, another future target might want the opposite, so define it as local as possible to journaldlogger.cpp. To make this work, we must take journaldlogger.cpp out of the unity build, because all files from a unity of share compiler definitions. --- lib/base/CMakeLists.txt | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/base/CMakeLists.txt b/lib/base/CMakeLists.txt index f8176092f..4ffadb7a0 100644 --- a/lib/base/CMakeLists.txt +++ b/lib/base/CMakeLists.txt @@ -114,7 +114,10 @@ if(WIN32) install(TARGETS eventprovider LIBRARY DESTINATION ${CMAKE_INSTALL_SBINDIR}) endif() -set_property(SOURCE ${CMAKE_CURRENT_SOURCE_DIR}/application-version.cpp PROPERTY EXCLUDE_UNITY_BUILD TRUE) +set_property( + SOURCE ${CMAKE_CURRENT_SOURCE_DIR}/application-version.cpp ${CMAKE_CURRENT_SOURCE_DIR}/journaldlogger.cpp + PROPERTY EXCLUDE_UNITY_BUILD TRUE +) if(ICINGA2_UNITY_BUILD) mkunity_target(base base base_SOURCES) @@ -125,7 +128,11 @@ if(HAVE_SYSTEMD) NAMES systemd/sd-daemon.h HINTS ${SYSTEMD_ROOT_DIR}) include_directories(${SYSTEMD_INCLUDE_DIR}) - add_definitions(-DSD_JOURNAL_SUPPRESS_LOCATION) + set_property( + SOURCE ${CMAKE_CURRENT_SOURCE_DIR}/journaldlogger.cpp + APPEND PROPERTY COMPILE_DEFINITIONS + SD_JOURNAL_SUPPRESS_LOCATION + ) endif() add_library(base OBJECT ${base_SOURCES})