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