This is an automated email from the ASF dual-hosted git repository.

rmiddleton pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/logging-log4cxx.git


The following commit(s) were added to refs/heads/master by this push:
     new e90e0700 Add a new generic DB appender class that can use ODBC to 
insert data (#206)
e90e0700 is described below

commit e90e07003e9e8082b3521c3fd71837cd1a569553
Author: Robert Middleton <[email protected]>
AuthorDate: Mon May 1 18:05:24 2023 -0400

    Add a new generic DB appender class that can use ODBC to insert data (#206)
    
    Add a new generic DB appender class that can uses APR to provide database 
support to insert data.
---
 CMakeLists.txt                           |   1 +
 src/main/cpp/CMakeLists.txt              |   1 +
 src/main/cpp/aprinitializer.cpp          |   3 +
 src/main/cpp/dbappender.cpp              | 241 +++++++++++++++++++++++++++++++
 src/main/include/log4cxx/db/dbappender.h | 173 ++++++++++++++++++++++
 5 files changed, 419 insertions(+)

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 7d533701..61c04791 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -325,6 +325,7 @@ endif(BUILD_TESTING)
 message(STATUS "Available appenders:")
 message(STATUS "  Async Appender .................. : ON")
 message(STATUS "  ODBC Appender ................... : ${HAS_ODBC}")
+message(STATUS "  DB Appender ..................... : ON")
 message(STATUS "  SMTP Appender ................... : ${HAS_LIBESMTP}")
 message(STATUS "  XMLSocketAppender ............... : 
${LOG4CXX_NETWORKING_SUPPORT}")
 message(STATUS "  SocketHubAppender ............... : 
${LOG4CXX_NETWORKING_SUPPORT}")
diff --git a/src/main/cpp/CMakeLists.txt b/src/main/cpp/CMakeLists.txt
index 55c10381..91fc608d 100644
--- a/src/main/cpp/CMakeLists.txt
+++ b/src/main/cpp/CMakeLists.txt
@@ -86,6 +86,7 @@ target_sources(log4cxx
   date.cpp
   dateformat.cpp
   datepatternconverter.cpp
+  dbappender.cpp
   defaultconfigurator.cpp
   defaultloggerfactory.cpp
   defaultrepositoryselector.cpp
diff --git a/src/main/cpp/aprinitializer.cpp b/src/main/cpp/aprinitializer.cpp
index 9eec3041..5a4c597e 100644
--- a/src/main/cpp/aprinitializer.cpp
+++ b/src/main/cpp/aprinitializer.cpp
@@ -25,6 +25,7 @@
 #include <log4cxx/helpers/threadspecificdata.h>
 #include <apr_thread_mutex.h>
 #include <apr_thread_proc.h>
+#include <apr_dbd.h>
 #include <log4cxx/helpers/filewatchdog.h>
 #include <log4cxx/helpers/date.h>
 
@@ -82,6 +83,8 @@ APRInitializer::APRInitializer() :
        assert(stat == APR_SUCCESS);
        assert(stat == APR_SUCCESS);
 #endif
+    apr_status_t stat2 = apr_dbd_init(m_priv->p);
+    assert(stat2 == APR_SUCCESS);
 }
 
 APRInitializer::~APRInitializer()
diff --git a/src/main/cpp/dbappender.cpp b/src/main/cpp/dbappender.cpp
new file mode 100644
index 00000000..7d31ff5d
--- /dev/null
+++ b/src/main/cpp/dbappender.cpp
@@ -0,0 +1,241 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#include <log4cxx/db/dbappender.h>
+#include <log4cxx/appenderskeleton.h>
+#include <log4cxx/helpers/stringhelper.h>
+#include <log4cxx/helpers/pool.h>
+#include <log4cxx/helpers/loglog.h>
+#include <log4cxx/patternlayout.h>
+#include <log4cxx/helpers/transcoder.h>
+#include <log4cxx/pattern/loggerpatternconverter.h>
+#include <log4cxx/pattern/classnamepatternconverter.h>
+#include <log4cxx/pattern/datepatternconverter.h>
+#include <log4cxx/pattern/filelocationpatternconverter.h>
+#include <log4cxx/pattern/fulllocationpatternconverter.h>
+#include <log4cxx/pattern/shortfilelocationpatternconverter.h>
+#include <log4cxx/pattern/linelocationpatternconverter.h>
+#include <log4cxx/pattern/messagepatternconverter.h>
+#include <log4cxx/pattern/methodlocationpatternconverter.h>
+#include <log4cxx/pattern/levelpatternconverter.h>
+#include <log4cxx/pattern/threadpatternconverter.h>
+#include <log4cxx/pattern/threadusernamepatternconverter.h>
+#include <log4cxx/pattern/ndcpatternconverter.h>
+#include <log4cxx/private/appenderskeleton_priv.h>
+#include <apr_dbd.h>
+
+using namespace log4cxx;
+using namespace log4cxx::helpers;
+using namespace log4cxx::db;
+using namespace log4cxx::spi;
+using namespace log4cxx::pattern;
+
+IMPLEMENT_LOG4CXX_OBJECT(DBAppender)
+
+#define _priv static_cast<DBAppenderPriv*>(m_priv.get())
+
+struct DBAppender::DBAppenderPriv : public 
AppenderSkeleton::AppenderSkeletonPrivate
+{
+    DBAppenderPriv() :
+        AppenderSkeletonPrivate()
+        {}
+
+    apr_dbd_driver_t* m_driver = nullptr;
+    apr_dbd_t* m_databaseHandle = nullptr;
+    apr_dbd_prepared_t* preparedStmt = nullptr;
+    std::vector<LogString> mappedName;
+    std::string driverName;
+    std::string driverParams;
+    std::string databaseName;
+    std::string sqlStatement;
+    Pool m_pool;
+    std::vector<pattern::LoggingEventPatternConverterPtr> converters;
+};
+
+#define RULES_PUT(spec, cls) \
+    specs.insert(PatternMap::value_type(LogString(LOG4CXX_STR(spec)), cls 
::newInstance))
+
+static PatternMap getFormatSpecifiers()
+{
+    PatternMap specs;
+    if (specs.empty())
+    {
+        RULES_PUT("logger", LoggerPatternConverter);
+        RULES_PUT("class", ClassNamePatternConverter);
+        RULES_PUT("time", DatePatternConverter);
+        RULES_PUT("shortfilename", ShortFileLocationPatternConverter);
+        RULES_PUT("fullfilename", FileLocationPatternConverter);
+        RULES_PUT("location", FullLocationPatternConverter);
+        RULES_PUT("line", LineLocationPatternConverter);
+        RULES_PUT("message", MessagePatternConverter);
+        RULES_PUT("method", MethodLocationPatternConverter);
+        RULES_PUT("level", LevelPatternConverter);
+        RULES_PUT("thread", ThreadPatternConverter);
+        RULES_PUT("threadname", ThreadUsernamePatternConverter);
+        RULES_PUT("ndc", NDCPatternConverter);
+    }
+    return specs;
+}
+
+DBAppender::DBAppender()
+    : AppenderSkeleton (std::make_unique<DBAppenderPriv>())
+{
+}
+
+DBAppender::~DBAppender()
+{
+    close();
+}
+
+void DBAppender::close(){
+    if(_priv->m_driver && _priv->m_databaseHandle){
+        apr_dbd_close(_priv->m_driver, _priv->m_databaseHandle);
+    }
+    _priv->m_driver = nullptr;
+    _priv->m_databaseHandle = nullptr;
+}
+
+void DBAppender::setOption(const LogString& option, const LogString& value){
+    if (StringHelper::equalsIgnoreCase(option, LOG4CXX_STR("COLUMNMAPPING"), 
LOG4CXX_STR("columnmapping")))
+    {
+        _priv->mappedName.push_back(value);
+    }
+    else if (StringHelper::equalsIgnoreCase(option, LOG4CXX_STR("DRIVERNAME"), 
LOG4CXX_STR("drivername")))
+    {
+        Transcoder::encodeUTF8(value, _priv->driverName);
+    }
+    else if (StringHelper::equalsIgnoreCase(option, 
LOG4CXX_STR("DRIVERPARAMS"), LOG4CXX_STR("driverparams")))
+    {
+        Transcoder::encodeUTF8(value, _priv->driverParams);
+    }
+    else if (StringHelper::equalsIgnoreCase(option, 
LOG4CXX_STR("DATABASENAME"), LOG4CXX_STR("databasename")))
+    {
+        Transcoder::encodeUTF8(value, _priv->databaseName);
+    }
+    else if (StringHelper::equalsIgnoreCase(option, LOG4CXX_STR("SQL"), 
LOG4CXX_STR("sql")))
+    {
+        Transcoder::encodeUTF8(value, _priv->sqlStatement);
+    }
+    else
+    {
+        AppenderSkeleton::setOption(option, value);
+    }
+}
+
+void DBAppender::activateOptions(helpers::Pool& p){
+    apr_status_t stat = apr_dbd_get_driver(_priv->m_pool.getAPRPool(),
+                                           _priv->driverName.c_str(),
+                                           const_cast<const 
apr_dbd_driver_t**>(&_priv->m_driver));
+
+    if(stat != APR_SUCCESS){
+        LogString errMsg = LOG4CXX_STR("Unable to get driver named ");
+        LOG4CXX_DECODE_CHAR(driverName, _priv->driverName);
+        errMsg.append(driverName);
+        LogLog::error(errMsg);
+        _priv->errorHandler->error(errMsg);
+        return;
+    }
+
+    stat = apr_dbd_open(_priv->m_driver,
+                        _priv->m_pool.getAPRPool(),
+                        _priv->driverParams.c_str(),
+                        &_priv->m_databaseHandle);
+    if(stat != APR_SUCCESS){
+        LogLog::error(LOG4CXX_STR("Unable to open database"));
+        _priv->errorHandler->error(LOG4CXX_STR("Unable to open database"));
+        return;
+    }
+
+    if(!_priv->databaseName.empty()){
+        apr_dbd_set_dbname(_priv->m_driver,
+                           _priv->m_pool.getAPRPool(),
+                           _priv->m_databaseHandle,
+                           _priv->databaseName.c_str());
+    }
+
+    stat = apr_dbd_prepare(_priv->m_driver,
+                           _priv->m_pool.getAPRPool(),
+                           _priv->m_databaseHandle,
+                           _priv->sqlStatement.c_str(),
+                           "log_insert",
+                           &_priv->preparedStmt);
+    if(stat != APR_SUCCESS){
+        LogString error = LOG4CXX_STR("Unable to prepare statement: ");
+        std::string dbdErr(apr_dbd_error(_priv->m_driver, 
_priv->m_databaseHandle, stat));
+        LOG4CXX_DECODE_CHAR(dbdErrLS, dbdErr);
+        error.append(dbdErrLS);
+        LogLog::error(error);
+        _priv->errorHandler->error(error);
+        return;
+    }
+
+    auto specs = getFormatSpecifiers();
+    for (auto& name : _priv->mappedName)
+    {
+        auto pItem = specs.find(StringHelper::toLowerCase(name));
+        if (specs.end() == pItem)
+            LogLog::error(name + LOG4CXX_STR(" is not a supported 
ColumnMapping value"));
+        else
+        {
+            std::vector<LogString> options;
+            if (LOG4CXX_STR("time") == pItem->first)
+                options.push_back(LOG4CXX_STR("yyyy-MM-ddTHH:mm:ss.SSS"));
+            pattern::LoggingEventPatternConverterPtr converter = 
log4cxx::cast<LoggingEventPatternConverter>((pItem->second)(options));
+            _priv->converters.push_back(converter);
+        }
+    }
+}
+
+void DBAppender::append(const spi::LoggingEventPtr& event, helpers::Pool& p){
+       std::vector<std::string> ls_args;
+    std::vector<const char*> args;
+    int stat;
+    int num_rows;
+
+    if(_priv->m_driver == nullptr ||
+            _priv->m_databaseHandle == nullptr ||
+            _priv->preparedStmt == nullptr){
+        _priv->errorHandler->error(LOG4CXX_STR("DBAppender not initialized 
properly: logging not available"));
+        return;
+    }
+
+    for(auto& converter : _priv->converters){
+        LogString str_data;
+        converter->format(event, str_data, p);
+               LOG4CXX_ENCODE_CHAR(new_str_data, str_data);
+               ls_args.push_back(new_str_data);
+    }
+
+       for(std::string& str : ls_args){
+        args.push_back(str.data());
+    }
+    args.push_back(nullptr);
+
+    stat = apr_dbd_pquery(_priv->m_driver,
+                          _priv->m_pool.getAPRPool(),
+                          _priv->m_databaseHandle,
+                          &num_rows,
+                          _priv->preparedStmt,
+                          args.size(),
+                          args.data());
+    if(stat != APR_SUCCESS){
+        LogString error = LOG4CXX_STR("Unable to insert: ");
+               LOG4CXX_DECODE_CHAR(local_error, apr_dbd_error(_priv->m_driver, 
_priv->m_databaseHandle, stat));
+               error.append(local_error);
+        LogLog::error(error);
+        _priv->errorHandler->error(error);
+    }
+}
diff --git a/src/main/include/log4cxx/db/dbappender.h 
b/src/main/include/log4cxx/db/dbappender.h
new file mode 100644
index 00000000..5dc37f4a
--- /dev/null
+++ b/src/main/include/log4cxx/db/dbappender.h
@@ -0,0 +1,173 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef LOG4CXX_DB_DB_APPENDER_H
+#define LOG4CXX_DB_DB_APPENDER_H
+
+#include <log4cxx/log4cxx.h>
+
+#include <log4cxx/helpers/exception.h>
+#include <log4cxx/appenderskeleton.h>
+#include <log4cxx/spi/loggingevent.h>
+#include <list>
+#include <memory>
+
+namespace log4cxx
+{
+namespace db
+{
+
+/**
+ * The DBAppender lets you log messages to a database.  This utilizes the 
APR's database support in order
+ * to provide a single way of logging to multiple databases, not just ODBC as 
the ODBCAppender does.
+ *
+ * The following SQL script is an example of how you may make a table that 
stores log messages:
+ *
+ * ~~~{.sql}
+ * CREATE TABLE logs (
+ *  logger VARCHAR(200),
+ *  time DATETIME,
+ *  level CHAR(5),
+ *  file VARCHAR(200),
+ *  line_number INT,
+ *  message VARCHAR(1000)
+ * );
+ * ~~~
+ *
+ * Once you have defined the table, you must define the order in which the 
columns are formatted when they are inserted
+ * along with the insert statement.
+ *
+ * Using APR, the following insert statement can be used to insert log 
statements: <code>INSERT INTO logs (logger, time, level, file, line_number, 
message) VALUES (%s, %pDa, %s, %s, %d, %s)</code>
+ * The values to insert must be formatted appropriately and in the correct 
order.  In order to do this, the parameter <code>ColumnMapping</code> must be 
specified as many times
+ * as there are columns to insert.
+ *
+ * The following table shows the conversion specifiers.  These are effectively 
the same as the conversion patterns provided by PatternLayout but with more 
descriptive names:
+ * |Conversion Specifier|Effect|
+ * |---|---|
+ * |logger|The name of the logger(e.g. com.foo.bar)|
+ * |class|The class that the log message was in|
+ * |time|The time of the log message|
+ * |shortfilename|The short filename where the log message is from|
+ * |fullfilename|The full filename where the log mesasge is from|
+ * |location|The location of the log message|
+ * |line|The line where the log message appears|
+ * |message|The log message|
+ * |method|The method where the message was logged|
+ * |level|The level of the log message|
+ * |thread|The thread where this message was logged|
+ * |threadname|The name of the thread that logged the message|
+ * |ndc|The NDC(nested diagnostic context) of the log|
+ *
+ * Other parameters that are important:
+ * |Parameter name|Usage|
+ * |---|---|
+ * |DriverName|The name of the database driver to load(ex: odbc, sqlite3)|
+ * |DriverParams|A string of parameters to pass to the driver.  This is 
database-specific|
+ * |DatabaseName|The name of the database to use when connecting to the server|
+ *
+ * The following code shows how you might connect to an ODBC data source and 
insert the log messages:
+ * ~~~{.xml}
+ * <appender name="SqlDBAppender" class="DBAppender">
+ *   <param name="drivername" value="odbc"/>
+ *   <param name="DriverParams" value="DATASOURCE=MariaDB-server"/>
+ *   <param name="DatabaseName" value="example_database"/>
+ *   <param name="sql" value="INSERT INTO logs (logger, time, level, file, 
line_number, message) VALUES (%s, %pDa, %s, %s, %d, %s)" />
+ *   <param name="ColumnMapping" value="logger"/>
+ *   <param name="ColumnMapping" value="time"/>
+ *   <param name="ColumnMapping" value="level"/>
+ *   <param name="ColumnMapping" value="shortfilename"/>
+ *   <param name="ColumnMapping" value="line"/>
+ *   <param name="ColumnMapping" value="message"/>
+ * </appender>
+ * ~~~
+ *
+ * A similar configuration can be used for SQLite:
+ *  * ~~~{.xml}
+ * <appender name="SqlDBAppender" class="DBAppender">
+ *   <param name="drivername" value="sqlite3"/>
+ *   <param name="DriverParams" value="/path/to/database.db"/>
+ *   <param name="sql" value="INSERT INTO logs (logger, time, level, file, 
line_number, message) VALUES (%s, %pDa, %s, %s, %d, %s)" />
+ *   <param name="ColumnMapping" value="logger"/>
+ *   <param name="ColumnMapping" value="time"/>
+ *   <param name="ColumnMapping" value="level"/>
+ *   <param name="ColumnMapping" value="shortfilename"/>
+ *   <param name="ColumnMapping" value="line"/>
+ *   <param name="ColumnMapping" value="message"/>
+ * </appender>
+ * ~~~
+ */
+class LOG4CXX_EXPORT DBAppender : public AppenderSkeleton
+{
+        public:
+                DECLARE_LOG4CXX_OBJECT(DBAppender)
+                BEGIN_LOG4CXX_CAST_MAP()
+                LOG4CXX_CAST_ENTRY(DBAppender)
+                LOG4CXX_CAST_ENTRY_CHAIN(AppenderSkeleton)
+                END_LOG4CXX_CAST_MAP()
+
+                DBAppender();
+                virtual ~DBAppender();
+
+                /**
+                Set options
+                */
+                void setOption(const LogString& option, const LogString& 
value) override;
+
+                /**
+                Activate the specified options.
+                */
+                void activateOptions(helpers::Pool& p) override;
+
+                /**
+                * Adds the event to the buffer.  When full the buffer is 
flushed.
+                */
+                void append(const spi::LoggingEventPtr& event, helpers::Pool&) 
override;
+
+                void close() override;
+
+                /**
+                * DBAppender does not require a layout.
+                * */
+                bool requiresLayout() const override
+                {
+                        return false;
+                }
+
+                /**
+                * Set pre-formated statement eg: insert into LogTable (msg) 
values (?)
+                */
+                void setSql(const LogString& s);
+
+                /**
+                * Returns pre-formated statement eg: insert into LogTable 
(msg) values (?)
+                */
+                const LogString& getSql() const;
+
+        private:
+                DBAppender(const DBAppender&);
+                DBAppender& operator=(const DBAppender&);
+
+        protected:
+                struct DBAppenderPriv;
+}; // class DBAppender
+
+LOG4CXX_PTR_DEF(DBAppender);
+
+} // namespace db
+} // namespace log4cxx
+
+#endif // LOG4CXX_DB_DB_APPENDER_H

Reply via email to