Merge pull request #8710 from Icinga/feature/windows-event-log

Add support for Windows Event Log and write early log messages to it
This commit is contained in:
Noah Hilverling 2021-06-24 09:19:50 +02:00 committed by GitHub
commit 8af66ce44c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 296 additions and 16 deletions

View File

@ -1865,4 +1865,21 @@ Facility Constants:
FacilityUucp | LOG\_UUCP | The UUCP system.
### WindowsEventLogLogger <a id="objecttype-windowseventloglogger"></a>
Specifies Icinga 2 logging to the Windows Event Log.
This configuration object is available as `windowseventlog` [logging feature](14-features.md#logging).
Example:
```
object WindowsEventLogLogger "windowseventlog" {
severity = "information"
}
```
Configuration Attributes:
Name | Type | Description
--------------------------|-----------------------|----------------------------------
severity | String | **Optional.** The minimum severity for this log. Can be "debug", "notice", "information", "warning" or "critical". Defaults to "information".

View File

@ -11,11 +11,12 @@ Icinga 2 supports three different types of logging:
You can enable additional loggers using the `icinga2 feature enable`
and `icinga2 feature disable` commands to configure loggers:
Feature | Description
---------|------------
debuglog | Debug log (path: `/var/log/icinga2/debug.log`, severity: `debug` or higher)
mainlog | Main log (path: `/var/log/icinga2/icinga2.log`, severity: `information` or higher)
syslog | Syslog (severity: `warning` or higher)
Feature | Description
----------------|------------
debuglog | Debug log (path: `/var/log/icinga2/debug.log`, severity: `debug` 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)
By default file the `mainlog` feature is enabled. When running Icinga 2
on a terminal log messages with severity `information` or higher are

View File

@ -39,6 +39,8 @@ 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)
else()
install_if_not_exists(icinga2/features-available/windowseventlog.conf ${ICINGA2_CONFIGDIR}/features-available)
endif()
install_if_not_exists(icinga2/scripts/mail-host-notification.sh ${ICINGA2_CONFIGDIR}/scripts)
install_if_not_exists(icinga2/scripts/mail-service-notification.sh ${ICINGA2_CONFIGDIR}/scripts)
@ -54,7 +56,7 @@ if(NOT WIN32)
install(FILES bash_completion.d/icinga2 DESTINATION ${BASHCOMPLETION_DIR})
else()
install_if_not_exists(icinga2/features-enabled/mainlog.conf ${ICINGA2_CONFIGDIR}/features-enabled)
install_if_not_exists(icinga2/features-enabled/windowseventlog.conf ${ICINGA2_CONFIGDIR}/features-enabled)
endif()
if(${CMAKE_SYSTEM_NAME} MATCHES "(Linux|Solaris|SunOS)")

View File

@ -0,0 +1,8 @@
/**
* The WindowsEventLogLogger type writes log information to the Windows Event Log.
*/
object WindowsEventLogLogger "windowseventlog" {
severity = "information"
}

View File

@ -1 +0,0 @@
include "../features-available/mainlog.conf"

View File

@ -0,0 +1 @@
include "../features-available/windowseventlog.conf"

View File

@ -213,10 +213,11 @@ static int UpgradeNSIS(void)
static int InstallIcinga(void)
{
std::string installDir = GetIcingaInstallPath();
std::string skelDir = installDir + "\\share\\skel";
std::string dataDir = GetIcingaDataPath();
if (!PathExists(dataDir)) {
std::string sourceDir = installDir + "\\share\\skel" + std::string(1, '\0');
std::string sourceDir = skelDir + std::string(1, '\0');
std::string destinationDir = dataDir + std::string(1, '\0');
SHFILEOPSTRUCT fop;
@ -243,6 +244,32 @@ static int InstallIcinga(void)
MkDirP(dataDir + "/var/spool/icinga2/tmp");
}
// Upgrade from versions older than 2.13 by making the windowseventlog feature available,
// enable it by default and disable the old mainlog feature.
if (!PathExists(dataDir + "/etc/icinga2/features-available/windowseventlog.conf")) {
// Disable the old mainlog feature as it is replaced by windowseventlog by default.
std::string mainlogEnabledFile = dataDir + "/etc/icinga2/features-enabled/mainlog.conf";
if (PathExists(mainlogEnabledFile)) {
if (DeleteFileA(mainlogEnabledFile.c_str()) == 0) {
throw std::runtime_error("deleting '" + mainlogEnabledFile + "' failed");
}
}
// Install the new windowseventlog feature. As features-available/windowseventlog.conf is used as a marker file,
// copy it as the last step, so that this is run again should the upgrade be interrupted.
for (const std::string& d : {"features-enabled", "features-available"}) {
std::string sourceFile = skelDir + "/etc/icinga2/" + d + "/windowseventlog.conf";
std::string destinationFile = dataDir + "/etc/icinga2/" + d + "/windowseventlog.conf";
if (CopyFileA(sourceFile.c_str(), destinationFile.c_str(), false) == 0) {
throw std::runtime_error("copying '" + sourceFile + "' to '" + destinationFile + "' failed");
}
}
}
// TODO: In Icinga 2.14, rename features-available/mainlog.conf to mainlog.conf.deprecated
// so that it's no longer listed as an available feature.
ExecuteCommand("icacls", "\"" + dataDir + "\" /grant *S-1-5-20:(oi)(ci)m");
ExecuteCommand("icacls", "\"" + dataDir + "\\etc\" /inheritance:r /grant:r *S-1-5-20:(oi)(ci)m *S-1-5-32-544:(oi)(ci)f");

View File

@ -20,6 +20,20 @@
<Custom Action="XtraUninstall" Before="RemoveExistingProducts">$CM_CP_sbin.icinga2_installer.exe=2 AND NOT SUPPRESS_XTRA</Custom>
</InstallExecuteSequence>
<!--
Write the path to eventprovider.dll to the registry so that the Event Viewer is able to find
the message definitions and properly displays our log messages.
See also: https://docs.microsoft.com/en-us/windows/win32/eventlog/reporting-an-event
-->
<FeatureRef Id="ProductFeature" IgnoreParent="yes">
<Component Id="EventProviderRegistryEntry" Guid="*" Directory="INSTALL_ROOT">
<RegistryKey Root="HKLM" Key="SYSTEM\CurrentControlSet\Services\EventLog\Application\Icinga 2" Action="createAndRemoveOnUninstall">
<RegistryValue Name="EventMessageFile" Type="string" Value="[#CM_FP_sbin.eventprovider.dll]" />
</RegistryKey>
</Component>
</FeatureRef>
<Property Id="WIXUI_EXITDIALOGOPTIONALCHECKBOXTEXT" Value="Run Icinga 2 setup wizard" />
<Property Id="WixShellExecTarget" Value="[#CM_FP_sbin.Icinga2SetupAgent.exe]" />

View File

@ -85,6 +85,33 @@ set(base_SOURCES
workqueue.cpp workqueue.hpp
)
if(WIN32)
mkclass_target(windowseventloglogger.ti windowseventloglogger-ti.cpp windowseventloglogger-ti.hpp)
list(APPEND base_SOURCES windowseventloglogger.cpp windowseventloglogger.hpp windowseventloglogger-ti.hpp)
# Generate a DLL containing message definitions for the Windows Event Viewer.
# See also: https://docs.microsoft.com/en-us/windows/win32/eventlog/reporting-an-event
add_custom_command(
OUTPUT windowseventloglogger-provider.rc windowseventloglogger-provider.h
COMMAND mc ARGS -U ${CMAKE_CURRENT_SOURCE_DIR}/windowseventloglogger-provider.mc
DEPENDS windowseventloglogger-provider.mc
)
list(APPEND base_SOURCES windowseventloglogger-provider.h)
add_custom_command(
OUTPUT windowseventloglogger-provider.res
COMMAND rc ARGS windowseventloglogger-provider.rc
DEPENDS windowseventloglogger-provider.rc
)
add_library(eventprovider MODULE windowseventloglogger-provider.res windowseventloglogger-provider.rc)
set_target_properties(eventprovider PROPERTIES LINKER_LANGUAGE CXX)
target_link_libraries(eventprovider PRIVATE -noentry)
install(TARGETS eventprovider LIBRARY DESTINATION ${CMAKE_INSTALL_SBINDIR})
endif()
set_property(SOURCE ${CMAKE_CURRENT_SOURCE_DIR}/application-version.cpp PROPERTY EXCLUDE_UNITY_BUILD TRUE)
if(ICINGA2_UNITY_BUILD)

View File

@ -9,6 +9,9 @@
#include "base/objectlock.hpp"
#include "base/context.hpp"
#include "base/scriptglobal.hpp"
#ifdef _WIN32
#include "base/windowseventloglogger.hpp"
#endif /* _WIN32 */
#include <iostream>
#include <utility>
@ -29,6 +32,7 @@ REGISTER_TYPE(Logger);
std::set<Logger::Ptr> Logger::m_Loggers;
std::mutex Logger::m_Mutex;
bool Logger::m_ConsoleLogEnabled = true;
std::atomic<bool> Logger::m_EarlyLoggingEnabled (true);
bool Logger::m_TimestampEnabled = true;
LogSeverity Logger::m_ConsoleLogSeverity = LogInformation;
@ -157,6 +161,14 @@ LogSeverity Logger::GetConsoleLogSeverity()
return m_ConsoleLogSeverity;
}
void Logger::DisableEarlyLogging() {
m_EarlyLoggingEnabled = false;
}
bool Logger::IsEarlyLoggingEnabled() {
return m_EarlyLoggingEnabled;
}
void Logger::DisableTimestamp()
{
m_TimestampEnabled = false;
@ -242,6 +254,12 @@ Log::~Log()
* then cout will not flush lines automatically. */
std::cout << std::flush;
}
#ifdef _WIN32
if (Logger::IsEarlyLoggingEnabled() && entry.Severity >= Logger::GetConsoleLogSeverity()) {
WindowsEventLogLogger::WriteToWindowsEventLog(entry);
}
#endif /* _WIN32 */
}
Log& Log::operator<<(const char *val)

View File

@ -67,6 +67,8 @@ public:
static void DisableConsoleLog();
static void EnableConsoleLog();
static bool IsConsoleLogEnabled();
static void DisableEarlyLogging();
static bool IsEarlyLoggingEnabled();
static void DisableTimestamp();
static void EnableTimestamp();
static bool IsTimestampEnabled();
@ -84,6 +86,7 @@ private:
static std::mutex m_Mutex;
static std::set<Logger::Ptr> m_Loggers;
static bool m_ConsoleLogEnabled;
static std::atomic<bool> m_EarlyLoggingEnabled;
static bool m_TimestampEnabled;
static LogSeverity m_ConsoleLogSeverity;
};

View File

@ -0,0 +1,5 @@
MessageId=0x1
SymbolicName=MSG_PLAIN_LOG_ENTRY
Language=English
%1
.

View File

@ -0,0 +1,83 @@
/* Icinga 2 | (c) 2021 Icinga GmbH | GPLv2+ */
#ifdef _WIN32
#include "base/windowseventloglogger.hpp"
#include "base/windowseventloglogger-ti.cpp"
#include "base/windowseventloglogger-provider.h"
#include "base/configtype.hpp"
#include "base/statsfunction.hpp"
#include <windows.h>
using namespace icinga;
REGISTER_TYPE(WindowsEventLogLogger);
REGISTER_STATSFUNCTION(WindowsEventLogLogger, &WindowsEventLogLogger::StatsFunc);
INITIALIZE_ONCE(&WindowsEventLogLogger::StaticInitialize);
static HANDLE l_EventLog = nullptr;
void WindowsEventLogLogger::StaticInitialize()
{
l_EventLog = RegisterEventSourceA(nullptr, "Icinga 2");
}
void WindowsEventLogLogger::StatsFunc(const Dictionary::Ptr& status, const Array::Ptr&)
{
DictionaryData nodes;
for (const WindowsEventLogLogger::Ptr& logger : ConfigType::GetObjectsByType<WindowsEventLogLogger>()) {
nodes.emplace_back(logger->GetName(), 1);
}
status->Set("windowseventloglogger", new Dictionary(std::move(nodes)));
}
/**
* Processes a log entry and outputs it to the Windows Event Log.
*
* This function implements the interface expected by the Logger base class and passes
* the log entry to WindowsEventLogLogger::WriteToWindowsEventLog().
*
* @param entry The log entry.
*/
void WindowsEventLogLogger::ProcessLogEntry(const LogEntry& entry) {
WindowsEventLogLogger::WriteToWindowsEventLog(entry);
}
/**
* Writes a LogEntry object to the Windows Event Log.
*
* @param entry The log entry.
*/
void WindowsEventLogLogger::WriteToWindowsEventLog(const LogEntry& entry)
{
if (l_EventLog != nullptr) {
std::string message = Logger::SeverityToString(entry.Severity) + "/" + entry.Facility + ": " + entry.Message;
std::array<const char *, 1> strings{
message.c_str()
};
WORD eventType;
switch (entry.Severity) {
case LogCritical:
eventType = EVENTLOG_ERROR_TYPE;
break;
case LogWarning:
eventType = EVENTLOG_WARNING_TYPE;
break;
default:
eventType = EVENTLOG_INFORMATION_TYPE;
}
ReportEventA(l_EventLog, eventType, 0, MSG_PLAIN_LOG_ENTRY, NULL, strings.size(), 0, strings.data(), NULL);
}
}
void WindowsEventLogLogger::Flush()
{
/* Nothing to do here. */
}
#endif /* _WIN32 */

View File

@ -0,0 +1,37 @@
/* Icinga 2 | (c) 2021 Icinga GmbH | GPLv2+ */
#ifndef WINDOWSEVENTLOGLOGGER_H
#define WINDOWSEVENTLOGLOGGER_H
#ifdef _WIN32
#include "base/i2-base.hpp"
#include "base/windowseventloglogger-ti.hpp"
namespace icinga
{
/**
* A logger that logs to the Windows Event Log.
*
* @ingroup base
*/
class WindowsEventLogLogger final : public ObjectImpl<WindowsEventLogLogger>
{
public:
DECLARE_OBJECT(WindowsEventLogLogger);
DECLARE_OBJECTNAME(WindowsEventLogLogger);
static void StaticInitialize();
static void StatsFunc(const Dictionary::Ptr& status, const Array::Ptr& perfdata);
static void WriteToWindowsEventLog(const LogEntry& entry);
protected:
void ProcessLogEntry(const LogEntry& entry) override;
void Flush() override;
};
}
#endif /* _WIN32 */
#endif /* WINDOWSEVENTLOGLOGGER_H */

View File

@ -0,0 +1,15 @@
/* Icinga 2 | (c) 2021 Icinga GmbH | GPLv2+ */
#include "base/logger.hpp"
library base;
namespace icinga
{
class WindowsEventLogLogger : Logger
{
activation_priority -100;
};
}

View File

@ -276,7 +276,7 @@ int RunWorker(const std::vector<std::string>& configs, bool closeConsoleLog = fa
}
// activate config only after daemonization: it starts threads and that is not compatible with fork()
if (!ConfigItem::ActivateItems(newItems, false, false, true)) {
if (!ConfigItem::ActivateItems(newItems, false, true, true)) {
Log(LogCritical, "cli", "Error activating configuration.");
return EXIT_FAILURE;
}

View File

@ -624,8 +624,18 @@ bool ConfigItem::CommitItems(const ActivationContext::Ptr& context, WorkQueue& u
return true;
}
/**
* ActivateItems activates new config items.
*
* @param newItems Vector of items to be activated
* @param runtimeCreated Whether the objects were created by a runtime object
* @param mainConfigActivation Whether this is the call for activating the main configuration during startup
* @param withModAttrs Whether this call shall read the modified attributes file
* @param cookie Cookie for preventing message loops
* @return Whether the config activation was successful (in case of errors, exceptions are thrown)
*/
bool ConfigItem::ActivateItems(const std::vector<ConfigItem::Ptr>& newItems, bool runtimeCreated,
bool silent, bool withModAttrs, const Value& cookie)
bool mainConfigActivation, bool withModAttrs, const Value& cookie)
{
static std::mutex mtx;
std::unique_lock<std::mutex> lock(mtx);
@ -663,7 +673,7 @@ bool ConfigItem::ActivateItems(const std::vector<ConfigItem::Ptr>& newItems, boo
object->PreActivate();
}
if (!silent)
if (mainConfigActivation)
Log(LogInformation, "ConfigItem", "Triggering Start signal for config items");
/* Activate objects in priority order. */
@ -675,6 +685,14 @@ bool ConfigItem::ActivateItems(const std::vector<ConfigItem::Ptr>& newItems, boo
return false;
});
/* Find the last logger type to be activated. */
Type::Ptr lastLoggerType = nullptr;
for (const Type::Ptr& type : types) {
if (Logger::TypeInstance->IsAssignableFrom(type)) {
lastLoggerType = type;
}
}
for (const Type::Ptr& type : types) {
for (const ConfigItem::Ptr& item : newItems) {
if (!item->m_Object)
@ -695,9 +713,14 @@ bool ConfigItem::ActivateItems(const std::vector<ConfigItem::Ptr>& newItems, boo
object->Activate(runtimeCreated, cookie);
}
if (mainConfigActivation && type == lastLoggerType) {
/* Disable early logging configuration once the last logger type was activated. */
Logger::DisableEarlyLogging();
}
}
if (!silent)
if (mainConfigActivation)
Log(LogInformation, "ConfigItem", "Activated all objects.");
return true;
@ -720,7 +743,7 @@ bool ConfigItem::RunWithActivationContext(const Function::Ptr& function)
if (!CommitItems(scope.GetContext(), upq, newItems, true))
return false;
if (!ActivateItems(newItems, false, true))
if (!ActivateItems(newItems, false, false))
return false;
return true;

View File

@ -54,7 +54,7 @@ public:
static bool CommitItems(const ActivationContext::Ptr& context, WorkQueue& upq, std::vector<ConfigItem::Ptr>& newItems, bool silent = false);
static bool ActivateItems(const std::vector<ConfigItem::Ptr>& newItems, bool runtimeCreated = false,
bool silent = false, bool withModAttrs = false, const Value& cookie = Empty);
bool mainConfigActivation = false, bool withModAttrs = false, const Value& cookie = Empty);
static bool RunWithActivationContext(const Function::Ptr& function);

View File

@ -238,7 +238,7 @@ bool ConfigObjectUtility::CreateObject(const Type::Ptr& type, const String& full
* uq, items, runtimeCreated, silent, withModAttrs, cookie
* IMPORTANT: Forward the cookie aka origin in order to prevent sync loops in the same zone!
*/
if (!ConfigItem::ActivateItems(newItems, true, true, false, cookie)) {
if (!ConfigItem::ActivateItems(newItems, true, false, false, cookie)) {
if (errors) {
Log(LogNotice, "ConfigObjectUtility")
<< "Failed to activate config object '" << fullName << "'. Aborting and removing config path '" << path << "'.";

View File

@ -60,7 +60,7 @@ syn keyword icinga2ObjType IcingaApplication IdoMysqlConnection IdoPgsqlConnec
syn keyword icinga2ObjType InfluxdbWriter LivestatusListener Notification NotificationCommand
syn keyword icinga2ObjType NotificationComponent OpenTsdbWriter PerfdataWriter
syn keyword icinga2ObjType ScheduledDowntime Service ServiceGroup SyslogLogger
syn keyword icinga2ObjType TimePeriod User UserGroup Zone
syn keyword icinga2ObjType TimePeriod User UserGroup WindowsEventLogLogger Zone
" Object/Template marker (simplified)
syn match icinga2ObjDef "\(object\|template\)[ \t]\+.*"