]> git.ipfire.org Git - thirdparty/kea.git/commitdiff
[#4526] Added mark-continuation-lines
authorFrancis Dupont <fdupont@isc.org>
Thu, 25 Jun 2026 10:35:26 +0000 (12:35 +0200)
committerFrancis Dupont <fdupont@isc.org>
Sat, 4 Jul 2026 08:47:33 +0000 (10:47 +0200)
changelog_unreleased/4526-mark-continuation-lines-in-legal-log-files [new file with mode: 0644]
doc/sphinx/arm/hooks-legal-log.rst
src/hooks/dhcp/forensic_log/rotating_file.cc
src/hooks/dhcp/forensic_log/rotating_file.h
src/hooks/dhcp/forensic_log/tests/legal_log4_unittests.cc
src/hooks/dhcp/forensic_log/tests/legal_log6_unittests.cc
src/hooks/dhcp/forensic_log/tests/test_utils.h
src/lib/dhcpsrv/legal_log_mgr.cc
src/lib/dhcpsrv/legal_log_mgr.h

diff --git a/changelog_unreleased/4526-mark-continuation-lines-in-legal-log-files b/changelog_unreleased/4526-mark-continuation-lines-in-legal-log-files
new file mode 100644 (file)
index 0000000..b53e2e6
--- /dev/null
@@ -0,0 +1,9 @@
+[func]*                fdupont
+       Improved multiple line log records in forensic log files:
+       continuation lines, i.e. all lines before the last one of
+       the log record, get a hyphen instead a space after the
+       leading timestamp. This feature can be disabled e.g.
+       for backward compatibility by setting the new paramater
+       "mark-continuation-lines" to false in the hook library
+       configuration.
+       (Gitlab #4526)
index 1028e6acf1f088b814b7ee04097b9daa6f695cdd..dc2786b7fb0cff6502c3367069738d4d849abeec 100644 (file)
@@ -169,6 +169,13 @@ These executables must be stored in the ``"[kea-install-dir]/share/kea/scripts/"
 directory which can be overridden at startup by setting the environment variable
 ``KEA_HOOK_SCRIPTS_PATH`` to a different path.
 
+Since Kea 3.3.0 a new parameter was added to make always possible to
+distinguish a multiple line log record from multiple log records:
+
+-  ``mark-continuation-lines`` - when true (the default) continuation
+   lines (i.e. lines before the last one in the log record) get a hyphen
+   vs. a space after the timestamp.
+
 Custom formatting can be enabled for logging information that can be extracted
 either from the client's request packet or from the server's response packet.
 Use with caution as this might affect server performance.
index 6d6986d216650a7050a736a13b174d06771febbd..3870db7d86246d385b276f881e63c71a1a2dbf28 100644 (file)
@@ -16,6 +16,7 @@
 
 #include <errno.h>
 #include <iostream>
+#include <list>
 #include <set>
 #include <sstream>
 #include <time.h>
@@ -33,7 +34,8 @@ namespace isc {
 namespace legal_log {
 
 RotatingFile::RotatingFile(const DatabaseConnection::ParameterMap& parameters)
-    : LegalLogMgr(parameters), time_unit_(TimeUnit::Day), count_(1), timestamp_(0) {
+    : LegalLogMgr(parameters), time_unit_(TimeUnit::Day), count_(1),
+      timestamp_(0), mark_continuation_lines_(true) {
     apply(parameters);
 }
 
@@ -89,6 +91,11 @@ RotatingFile::apply(const DatabaseConnection::ParameterMap& parameters) {
     if (parameters.find("postrotate") != parameters.end()) {
         postrotate = parameters.at("postrotate");
     }
+    if (parameters.find("mark-continuation-lines") != parameters.end()) {
+        string mcl(parameters.at("mark-continuation-lines"));
+        // The parser sets "true" or "false" so do not check...
+        mark_continuation_lines_ = (mcl != "false");
+    }
     path_ = path;
     base_name_ = base;
     time_unit_ = unit;
@@ -391,8 +398,21 @@ RotatingFile::writelnInternal(const string& text) {
 
     string timestamp = getNowString();
     stringstream ss(text);
+    // Collect lines.
+    list<string> lines;
     for (string line; getline(ss, line, '\n');) {
-        file_ << timestamp << " " << line << endl;
+        lines.push_back(line);
+    }
+    while (!lines.empty()) {
+        string line = lines.front();
+        lines.pop_front();
+        file_ << timestamp;
+        if (mark_continuation_lines_ && !lines.empty()) {
+            file_ << "-";
+        } else {
+            file_ << " ";
+        }
+        file_ << line << endl;
     }
     int sav_error = errno;
     if (!file_.good()) {
index 13382147e990deb6df4b5819876bb6c7435abab9..9aba94b415a8b179c84ea35720de3bb718fb9b6b 100644 (file)
@@ -75,6 +75,7 @@ public:
     ///       - prerotate
     ///       - postrotate
     ///       - count
+    ///       - mark-continuation-lines
     ///
     /// @param parameters A data structure relating keywords and values
     ///        concerned with the manager configuration.
@@ -116,6 +117,10 @@ public:
     /// @b postrotate - An external executable or script called with the name of the file that
     /// was opened. Kea does not wait for the process to finish.
     ///
+    /// @b mark-continuation-lines - When true (default) mark continuation
+    /// lines so only the last line in a multiple line record gets a space
+    /// (vs hyphen) after the leading timestamp.
+    ///
     /// @param parameters The library parameters.
     void apply(const isc::db::DatabaseConnection::ParameterMap& parameters);
 
@@ -161,6 +166,12 @@ public:
     ///
     /// - @b EOL - the character(s) generated std::endl
     ///
+    /// When mark_continuation_lines_ is true (default) multiple lines give:
+    ///
+    ///     "<timestamp>-<text1><EOL>"
+    ///     "<timestamp>-<text2><EOL>"
+    ///     "<timestamp>SP<text3><EOL>"
+    ///
     /// @param addr Address or prefix (ignored).
     /// @param text String to append.
     ///
@@ -280,6 +291,10 @@ private:
     /// @brief Mutex to protect output.
     std::mutex mutex_;
 
+protected:
+    /// @brief The mark continuation lines flag.
+    bool mark_continuation_lines_;
+
 public:
     /// @brief Factory class method.
     ///
index ccad0223d87f49ac9c7fbba56dfecc5cd36bb27a..47002201c94bdbbd5b185ec2bc0fb562f98cb769 100644 (file)
@@ -34,6 +34,8 @@
 
 #include <gtest/gtest.h>
 
+#include <boost/pointer_cast.hpp>
+
 using namespace std;
 using namespace isc;
 using namespace isc::asiolink;
@@ -1696,6 +1698,12 @@ TEST_F(CalloutTestv4, customRequestLoggingFormatMultipleLines) {
 
     LegalLogMgrFactory::instance()->setRequestFormatExpression(format);
 
+    // Disable mark continuation lines.
+    TestableRotatingFilePtr trfp =
+        boost::dynamic_pointer_cast<TestableRotatingFile>(LegalLogMgrFactory::instance());
+    ASSERT_TRUE(trfp);
+    trfp->setMarkContinuationLines(false);
+
     int ret;
 
     // Make a lease and add it to the callout arguments.
@@ -1769,4 +1777,43 @@ TEST_F(CalloutTestv4, customLogRenderError) {
     checkFileLines(genName(today()), today_now_string, lines);
 }
 
+// Verifies that the custom format logs on a multiple line record.
+TEST_F(CalloutTestv4, customRequestLoggingFormatMultipleLineRecord) {
+    ASSERT_NO_THROW(LegalLogMgrFactory::instance().reset(new TestableRotatingFile(time_)));
+
+    // Make a callout handle
+    CalloutHandlePtr handle = getCalloutHandle(decline_);
+    handle->setCurrentLibrary(0);
+
+    std::string format = "ifelse(pkt4.msgtype == 4, 'first line' + 0x0a + 'second line', '')";
+
+    LegalLogMgrFactory::instance()->setRequestFormatExpression(format);
+
+    int ret;
+
+    // Make a lease and add it to the callout arguments.
+    Lease4Ptr lease4 = createLease4("192.2.1.100", 6735, hwaddr_, ClientIdPtr(), 1234);
+
+    // The callout should succeed and generate an entry for 192.2.1.100.
+    {
+        ScopedCalloutHandleState callout_handle_state(handle);
+        handle->setArgument("lease4", lease4);
+        handle->setArgument("query4", decline_);
+        ASSERT_NO_THROW(ret = lease4_decline(*handle));
+        EXPECT_EQ(0, ret);
+    }
+
+    // Close it to flush any unwritten data
+    LegalLogMgrFactory::instance()->close();
+
+    // Verify that the file content is correct.
+    std::vector<std::string>lines;
+    lines.push_back("first line");
+    lines.push_back("second line");
+
+    std::string today_now_string = LegalLogMgrFactory::instance()->getNowString();
+    // Use the continuation lines variant.
+    checkFileMultipleLines(genName(today()), today_now_string, lines);
+}
+
 } // end of anonymous namespace
index a76d280b0d5b97d5f3835036a0d254b4122bc9f1..d973bf82cf7c2e0ccae0b6ee66c7a8ade19a3167 100644 (file)
@@ -2385,6 +2385,12 @@ TEST_F(CalloutTestv6, customRequestLoggingFormatMultipleLines) {
 
     LegalLogMgrFactory::instance()->setRequestFormatExpression(format);
 
+    // Disable mark continuation lines.
+    TestableRotatingFilePtr trfp =
+        boost::dynamic_pointer_cast<TestableRotatingFile>(LegalLogMgrFactory::instance());
+    ASSERT_TRUE(trfp);
+    trfp->setMarkContinuationLines(false);
+
     int ret;
 
     // Make a lease and add it to the callout arguments.
@@ -2419,6 +2425,55 @@ TEST_F(CalloutTestv6, customRequestLoggingFormatMultipleLines) {
     checkFileLines(genName(today()), today_now_string, lines);
 }
 
+// Verifies that the custom format logs on a multiple line record.
+TEST_F(CalloutTestv6, customRequestLoggingFormatMultipleLineRecord) {
+    ASSERT_NO_THROW(LegalLogMgrFactory::instance().reset(new TestableRotatingFile(time_)));
+
+    CfgMgr::instance().setFamily(AF_INET6);
+
+    // Make a callout handle
+    CalloutHandlePtr handle = getCalloutHandle(decline_);
+    handle->setCurrentLibrary(0);
+
+    std::string format = "ifelse(pkt6.msgtype == 9, 'first line' + 0x0a + 'second line', '')";
+
+    LegalLogMgrFactory::instance()->setRequestFormatExpression(format);
+
+    int ret;
+
+    // Make a lease and add it to the callout arguments.
+    Lease6Ptr lease6 = createLease6(duid_, Lease::TYPE_NA, "2001:db8:1::", 128,
+                                    713, HWAddrPtr());
+
+    // The callout should succeed and generate an entry for 2001:db8:1::
+    {
+        ScopedCalloutHandleState callout_handle_state(handle);
+        handle->setArgument("lease6", lease6);
+        ASSERT_NO_THROW(ret = lease6_decline(*handle));
+        EXPECT_EQ(0, ret);
+    }
+
+    {
+        ScopedCalloutHandleState callout_handle_state(handle);
+        handle->setArgument("query6", decline_);
+        handle->setArgument("response6", response_);
+        ASSERT_NO_THROW(ret = pkt6_send(*handle));
+        EXPECT_EQ(0, ret);
+    }
+
+    // Close it to flush any unwritten data
+    LegalLogMgrFactory::instance()->close();
+
+    // Verify that the file content is correct.
+    std::vector<std::string>lines;
+    lines.push_back("first line");
+    lines.push_back("second line");
+
+    std::string today_now_string = LegalLogMgrFactory::instance()->getNowString();
+    // Use the continuation lines variant.
+    checkFileMultipleLines(genName(today()), today_now_string, lines);
+}
+
 TEST_F(CalloutTestv6, multipleAddressesAndPrefixesCustomLoggingFormatRequestOnly) {
     ASSERT_NO_THROW(LegalLogMgrFactory::instance().reset(new TestableRotatingFile(time_)));
 
index c384023f1e5402097c3872bb2a61bd94f4aab965..1964ba9bb7edecf522327ae79ef636b24cd6e99f 100644 (file)
@@ -120,6 +120,11 @@ public:
         file_list_.insert(getFileName());
     }
 
+    /// @brief Sets the mark continuation lines flag.
+    void setMarkContinuationLines(bool mark_continuation_lines) {
+        mark_continuation_lines_ = mark_continuation_lines;
+    }
+
     /// @brief Sets the override date value
     ///
     /// @param new value for the override date
@@ -329,6 +334,47 @@ public:
                                             << file_name;
     }
 
+    /// @brief Check a file's contents against a multiple-line record
+    ///
+    /// Passes if the given file's content matches. Fails otherwise.
+    ///
+    /// @param file_name name of the file to read
+    /// @param expected_lines a vector of the lines expected to be found
+    /// in the file (entries DO NOT include EOL) representing a multiple
+    /// line record so continuation lines until the last one.
+    void checkFileMultipleLines(const string& file_name,
+                                const string& now_string,
+                                const vector<string>& expected_lines) {
+        ifstream is;
+        is.open(file_name.c_str());
+        ASSERT_TRUE(is.good()) << "Could not open file: " << file_name;
+
+        unsigned i = 0;
+        while (!is.eof()) {
+            char buf[1024];
+
+            is.getline(buf, sizeof(buf));
+            if (is.gcount() > 0) {
+                ASSERT_TRUE(i <= expected_lines.size())
+                    << "Too many entries in file: " << file_name;
+                string cmp_line = now_string;
+                if (i + 1 == expected_lines.size()) {
+                    cmp_line += " ";
+                } else {
+                    cmp_line += "-";
+                }
+                cmp_line += expected_lines[i];
+                ASSERT_EQ(cmp_line, buf) << "line mismatch in: " << file_name
+                                         << " at line:" << i;
+
+                ++i;
+            }
+        }
+
+        ASSERT_EQ(i, expected_lines.size()) << "Not enough entries in file: "
+                                            << file_name;
+    }
+
     /// @brief Check that the file was not created.
     ///
     /// Passes if the given file does not exist. Fails otherwise.
index a412e6682a88852a111e0a1a35834f927b6818c4..249e302062feaa7221e771eaafedea2bc2e24ee2 100644 (file)
@@ -197,6 +197,15 @@ LegalLogMgr::parseFile(const ConstElementPtr& parameters, DatabaseConnection::Pa
             file_parameters[key] = boost::lexical_cast<string>(integer_value);
         }
     }
+
+    // bool
+    for (char const* const& key : { "mark-continuation-lines" }) {
+        ConstElementPtr const value(parameters->get(key));
+        if (value) {
+            file_parameters.emplace(key,
+                                    value->boolValue() ? "true" : "false");
+        }
+    }
     map = file_parameters;
 }
 
index 17314f574b7e161cd5e71d59d99efc828f65a41b..a912cba2a9d7b5c808d03d5a509476a2e7c59fd6 100644 (file)
@@ -93,6 +93,7 @@ public:
     ///       - prerotate
     ///       - postrotate
     ///       - count
+    ///       - mark-continuation-lines
     /// - syslog parameters:
     ///       - pattern
     ///       - facility
@@ -165,6 +166,7 @@ public:
     ///       - prerotate
     ///       - postrotate
     ///       - count
+    ///       - mark-continuation-lines
     ///
     /// @param parameters The library parameters.
     /// @param [out] map The parameter map.