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) 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" +} diff --git a/lib/base/CMakeLists.txt b/lib/base/CMakeLists.txt index 9707d2935..4ffadb7a0 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 @@ -112,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) @@ -123,6 +128,11 @@ if(HAVE_SYSTEMD) NAMES systemd/sd-daemon.h HINTS ${SYSTEMD_ROOT_DIR}) include_directories(${SYSTEMD_INCLUDE_DIR}) + set_property( + SOURCE ${CMAKE_CURRENT_SOURCE_DIR}/journaldlogger.cpp + APPEND PROPERTY COMPILE_DEFINITIONS + SD_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; +}; + +} 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; 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