You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@logging.apache.org by sw...@apache.org on 2023/04/25 02:53:17 UTC
[logging-log4cxx] branch master updated: Add support for using SQLPrepare and bound parameters to the ODBC appender (#205)
This is an automated email from the ASF dual-hosted git repository.
swebb2066 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 5235de3d Add support for using SQLPrepare and bound parameters to the ODBC appender (#205)
5235de3d is described below
commit 5235de3d4f39e22e47a113d94052b83bc495ec74
Author: Stephen Webb <st...@ieee.org>
AuthorDate: Tue Apr 25 12:53:12 2023 +1000
Add support for using SQLPrepare and bound parameters to the ODBC appender (#205)
* Document proposed configuration
* Output the timestamp with the extra precision supported by the 'datetime2' SQL type fields
* Move new fields of ODBCAppenderPriv after existing fields (for ABI compatibility report)
* Prevent faults when using a DSN intead of user and password on Linux
---------
Co-authored-by: Stephen Webb <sw...@gmail.com>
Co-authored-by: Robert Middleton <ro...@rm5248.com>
---
src/main/cpp/odbcappender.cpp | 303 +++++++++++++++++++--
src/main/include/log4cxx/db/odbcappender.h | 85 +++---
.../include/log4cxx/private/odbcappender_priv.h | 68 ++++-
.../input/xml/odbcAppenderDSN-Log4cxxTest.xml | 18 +-
4 files changed, 404 insertions(+), 70 deletions(-)
diff --git a/src/main/cpp/odbcappender.cpp b/src/main/cpp/odbcappender.cpp
index 513debf3..7a2ed5ab 100644
--- a/src/main/cpp/odbcappender.cpp
+++ b/src/main/cpp/odbcappender.cpp
@@ -21,7 +21,22 @@
#include <log4cxx/helpers/transcoder.h>
#include <log4cxx/patternlayout.h>
#include <apr_strings.h>
-#include <log4cxx/private/odbcappender_priv.h>
+#include <apr_time.h>
+#include <cmath> // std::pow
+
+#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>
#if !defined(LOG4CXX)
#define LOG4CXX 1
@@ -32,13 +47,22 @@
#include <windows.h>
#endif
#include <sqlext.h>
+#else
+ typedef void* SQLHSTMT;
#endif
+#include <log4cxx/private/odbcappender_priv.h>
+#if defined(min)
+ #undef min
+#endif
+#include <cstring>
+#include <algorithm>
using namespace log4cxx;
using namespace log4cxx::helpers;
using namespace log4cxx::db;
using namespace log4cxx::spi;
+using namespace log4cxx::pattern;
SQLException::SQLException(short fHandleType,
void* hInput, const char* prolog,
@@ -103,6 +127,31 @@ ODBCAppender::~ODBCAppender()
finalize();
}
+#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;
+}
+
void ODBCAppender::setOption(const LogString& option, const LogString& value)
{
if (StringHelper::equalsIgnoreCase(option, LOG4CXX_STR("BUFFERSIZE"), LOG4CXX_STR("buffersize")))
@@ -127,16 +176,44 @@ void ODBCAppender::setOption(const LogString& option, const LogString& value)
{
setUser(value);
}
+ else if (StringHelper::equalsIgnoreCase(option, LOG4CXX_STR("COLUMNMAPPING"), LOG4CXX_STR("columnmapping")))
+ {
+ _priv->mappedName.push_back(value);
+ }
else
{
AppenderSkeleton::setOption(option, value);
}
}
+//* Does ODBCAppender require a layout?
+
+bool ODBCAppender::requiresLayout() const
+{
+ return _priv->parameterValue.empty();
+}
+
void ODBCAppender::activateOptions(log4cxx::helpers::Pool&)
{
#if !LOG4CXX_HAVE_ODBC
LogLog::error(LOG4CXX_STR("Can not activate ODBCAppender unless compiled with ODBC support."));
+#else
+ 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
+ {
+ ODBCAppenderPriv::DataBinding paramData{ 0, 0, 0, 0, 0 };
+ std::vector<LogString> options;
+ if (LOG4CXX_STR("time") == pItem->first)
+ options.push_back(LOG4CXX_STR("yyyy-MM-dd HH:mm:ss.SSSSSS"));
+ paramData.converter = log4cxx::cast<LoggingEventPatternConverter>((pItem->second)(options));
+ _priv->parameterValue.push_back(paramData);
+ }
+ }
#endif
}
@@ -176,16 +253,21 @@ void ODBCAppender::execute(const LogString& sql, log4cxx::helpers::Pool& p)
if (ret < 0)
{
- throw SQLException( SQL_HANDLE_DBC, con, "Failed to allocate sql handle.", p);
+ throw SQLException( SQL_HANDLE_DBC, con, "Failed to allocate sql handle", p);
}
+#if LOG4CXX_LOGCHAR_IS_WCHAR
+ ret = SQLExecDirectW(stmt, (SQLWCHAR*)sql.c_str(), SQL_NTS);
+#elif LOG4CXX_LOGCHAR_IS_UTF8
+ ret = SQLExecDirectA(stmt, (SQLCHAR*)sql.c_str(), SQL_NTS);
+#else
SQLWCHAR* wsql;
encode(&wsql, sql, p);
ret = SQLExecDirectW(stmt, wsql, SQL_NTS);
-
+#endif
if (ret < 0)
{
- throw SQLException(SQL_HANDLE_STMT, stmt, "Failed to execute sql statement.", p);
+ throw SQLException(SQL_HANDLE_STMT, stmt, "Failed to execute sql statement", p);
}
}
catch (SQLException&)
@@ -211,10 +293,6 @@ void ODBCAppender::closeConnection(ODBCAppender::SQLHDBC /* con */)
{
}
-
-
-
-
ODBCAppender::SQLHDBC ODBCAppender::getConnection(log4cxx::helpers::Pool& p)
{
#if LOG4CXX_HAVE_ODBC
@@ -226,7 +304,7 @@ ODBCAppender::SQLHDBC ODBCAppender::getConnection(log4cxx::helpers::Pool& p)
if (ret < 0)
{
- SQLException ex(SQL_HANDLE_ENV, _priv->env, "Failed to allocate SQL handle.", p);
+ SQLException ex(SQL_HANDLE_ENV, _priv->env, "Failed to allocate SQL handle", p);
_priv->env = SQL_NULL_HENV;
throw ex;
}
@@ -235,7 +313,7 @@ ODBCAppender::SQLHDBC ODBCAppender::getConnection(log4cxx::helpers::Pool& p)
if (ret < 0)
{
- SQLException ex(SQL_HANDLE_ENV, _priv->env, "Failed to set odbc version.", p);
+ SQLException ex(SQL_HANDLE_ENV, _priv->env, "Failed to set odbc version", p);
SQLFreeHandle(SQL_HANDLE_ENV, _priv->env);
_priv->env = SQL_NULL_HENV;
throw ex;
@@ -248,16 +326,18 @@ ODBCAppender::SQLHDBC ODBCAppender::getConnection(log4cxx::helpers::Pool& p)
if (ret < 0)
{
- SQLException ex(SQL_HANDLE_DBC, _priv->connection, "Failed to allocate sql handle.", p);
+ SQLException ex(SQL_HANDLE_DBC, _priv->connection, "Failed to allocate sql handle", p);
_priv->connection = SQL_NULL_HDBC;
throw ex;
}
- SQLWCHAR* wURL, *wUser, *wPwd;
+ SQLWCHAR* wURL, *wUser = nullptr, *wPwd = nullptr;
encode(&wURL, _priv->databaseURL, p);
- encode(&wUser, _priv->databaseUser, p);
- encode(&wPwd, _priv->databasePassword, p);
+ if (!_priv->databaseUser.empty())
+ encode(&wUser, _priv->databaseUser, p);
+ if (!_priv->databasePassword.empty())
+ encode(&wPwd, _priv->databasePassword, p);
ret = SQLConnectW( _priv->connection,
wURL, SQL_NTS,
@@ -267,7 +347,7 @@ ODBCAppender::SQLHDBC ODBCAppender::getConnection(log4cxx::helpers::Pool& p)
if (ret < 0)
{
- SQLException ex(SQL_HANDLE_DBC, _priv->connection, "Failed to connect to database.", p);
+ SQLException ex(SQL_HANDLE_DBC, _priv->connection, "Failed to connect to database", p);
SQLFreeHandle(SQL_HANDLE_DBC, _priv->connection);
_priv->connection = SQL_NULL_HDBC;
throw ex;
@@ -316,14 +396,203 @@ void ODBCAppender::close()
_priv->closed = true;
}
+#if LOG4CXX_HAVE_ODBC
+void ODBCAppender::ODBCAppenderPriv::setPreparedStatement(SQLHDBC con, Pool& p)
+{
+ auto ret = SQLAllocHandle( SQL_HANDLE_STMT, con, &this->preparedStatement);
+ if (ret < 0)
+ {
+ throw SQLException( SQL_HANDLE_DBC, con, "Failed to allocate statement handle.", p);
+ }
+
+#if LOG4CXX_LOGCHAR_IS_WCHAR
+ ret = SQLPrepareW(this->preparedStatement, (SQLWCHAR*)this->sqlStatement.c_str(), SQL_NTS);
+#elif LOG4CXX_LOGCHAR_IS_UTF8
+ ret = SQLPrepareA(this->preparedStatement, (SQLCHAR*)this->sqlStatement.c_str(), SQL_NTS);
+#else
+ SQLWCHAR* wsql;
+ encode(&wsql, this->sqlStatement, p);
+ ret = SQLPrepareW(this->preparedStatement, wsql, SQL_NTS);
+#endif
+ if (ret < 0)
+ {
+ throw SQLException(SQL_HANDLE_STMT, this->preparedStatement, "Failed to prepare sql statement.", p);
+ }
+
+ int parameterNumber = 0;
+ for (auto& item : this->parameterValue)
+ {
+ ++parameterNumber;
+ SQLSMALLINT targetType;
+ SQLULEN targetMaxCharCount;
+ SQLSMALLINT decimalDigits;
+ SQLSMALLINT nullable;
+ auto ret = SQLDescribeParam
+ ( this->preparedStatement
+ , parameterNumber
+ , &targetType
+ , &targetMaxCharCount
+ , &decimalDigits
+ , &nullable
+ );
+ if (ret < 0)
+ {
+ throw SQLException(SQL_HANDLE_STMT, this->preparedStatement, "Failed to describe parameter", p);
+ }
+ if (SQL_CHAR == targetType || SQL_VARCHAR == targetType || SQL_LONGVARCHAR == targetType)
+ {
+ item.paramType = SQL_C_CHAR;
+ item.paramMaxCharCount = targetMaxCharCount;
+ item.paramValueSize = (SQLINTEGER)(item.paramMaxCharCount) * sizeof(char) + sizeof(char);
+ item.paramValue = (SQLPOINTER)p.palloc(item.paramValueSize + sizeof(char));
+ }
+ else if (SQL_WCHAR == targetType || SQL_WVARCHAR == targetType || SQL_WLONGVARCHAR == targetType)
+ {
+ item.paramType = SQL_C_WCHAR;
+ item.paramMaxCharCount = targetMaxCharCount;
+ item.paramValueSize = (SQLINTEGER)(targetMaxCharCount) * sizeof(wchar_t) + sizeof(wchar_t);
+ item.paramValue = (SQLPOINTER)p.palloc(item.paramValueSize + sizeof(wchar_t));
+ }
+ else if (SQL_TYPE_TIMESTAMP == targetType || SQL_TYPE_DATE == targetType || SQL_TYPE_TIME == targetType
+ || SQL_DATETIME == targetType)
+ {
+ item.paramType = SQL_C_TYPE_TIMESTAMP;
+ item.paramMaxCharCount = decimalDigits;
+ item.paramValueSize = sizeof(SQL_TIMESTAMP_STRUCT);
+ item.paramValue = (SQLPOINTER)p.palloc(item.paramValueSize);
+ }
+ else
+ {
+ if (SQL_INTEGER != targetType)
+ {
+ LogString msg(LOG4CXX_STR("Unexpected targetType ("));
+ helpers::StringHelper::toString(targetType, p, msg);
+ msg += LOG4CXX_STR(") at parameter ");
+ helpers::StringHelper::toString(parameterNumber, p, msg);
+ msg += LOG4CXX_STR(" while preparing SQL");
+ LogLog::warn(msg);
+ }
+ item.paramMaxCharCount = 30;
+#if LOG4CXX_LOGCHAR_IS_UTF8
+ item.paramType = SQL_C_CHAR;
+ item.paramValueSize = (SQLINTEGER)(item.paramMaxCharCount) * sizeof(char);
+ item.paramValue = (SQLPOINTER)p.palloc(item.paramValueSize + sizeof(char));
+#else
+ item.paramType = SQL_C_WCHAR;
+ item.paramValueSize = (SQLINTEGER)(item.paramMaxCharCount) * sizeof(wchar_t);
+ item.paramValue = (SQLPOINTER)p.palloc(item.paramValueSize + sizeof(wchar_t));
+#endif
+ }
+ item.strLen_or_Ind = SQL_NTS;
+ ret = SQLBindParameter
+ ( this->preparedStatement
+ , parameterNumber
+ , SQL_PARAM_INPUT
+ , item.paramType // ValueType
+ , targetType
+ , targetMaxCharCount
+ , decimalDigits
+ , item.paramValue
+ , item.paramValueSize
+ , &item.strLen_or_Ind
+ );
+ if (ret < 0)
+ {
+ throw SQLException(SQL_HANDLE_STMT, this->preparedStatement, "Failed to bind parameter", p);
+ }
+ }
+}
+
+void ODBCAppender::ODBCAppenderPriv::setParameterValues(const spi::LoggingEventPtr& event, Pool& p)
+{
+ for (auto& item : this->parameterValue)
+ {
+ if (!item.paramValue || item.paramValueSize <= 0)
+ ;
+ else if (SQL_C_WCHAR == item.paramType)
+ {
+ LogString sbuf;
+ item.converter->format(event, sbuf, p);
+#if LOG4CXX_LOGCHAR_IS_WCHAR_T
+ std::wstring& tmp = sbuf;
+#else
+ std::wstring tmp;
+ Transcoder::encode(sbuf, tmp);
+#endif
+ auto dst = (wchar_t*)item.paramValue;
+ auto charCount = std::min(size_t(item.paramMaxCharCount), tmp.size());
+ auto copySize = std::min(size_t(item.paramValueSize - 1), charCount * sizeof(wchar_t));
+ std::memcpy(dst, tmp.data(), copySize);
+ dst[copySize / sizeof(wchar_t)] = 0;
+ }
+ else if (SQL_C_CHAR == item.paramType)
+ {
+ LogString sbuf;
+ item.converter->format(event, sbuf, p);
+#if LOG4CXX_LOGCHAR_IS_UTF8
+ std::string& tmp = sbuf;
+#else
+ std::string tmp;
+ Transcoder::encode(sbuf, tmp);
+#endif
+ auto dst = (char*)item.paramValue;
+ auto sz = std::min(size_t(item.paramMaxCharCount), tmp.size());
+ auto copySize = std::min(size_t(item.paramValueSize - 1), sz * sizeof(char));
+ std::memcpy(dst, tmp.data(), copySize);
+ dst[copySize] = 0;
+ }
+ else if (SQL_C_TYPE_TIMESTAMP == item.paramType)
+ {
+ apr_time_exp_t exploded;
+ apr_status_t stat = this->timeZone->explode(&exploded, event->getTimeStamp());
+ if (stat == APR_SUCCESS)
+ {
+ auto dst = (SQL_TIMESTAMP_STRUCT*)item.paramValue;
+ dst->year = 1900 + exploded.tm_year;
+ dst->month = 1 + exploded.tm_mon;
+ dst->day = exploded.tm_mday;
+ dst->hour = exploded.tm_hour;
+ dst->minute = exploded.tm_min;
+ dst->second = exploded.tm_sec;
+ // Prevent '[ODBC SQL Server Driver]Datetime field overflow' by rounding to the target field precision
+ int roundingExponent = 6 - (int)item.paramMaxCharCount;
+ if (0 < roundingExponent)
+ {
+ int roundingDivisor = (int)std::pow(10, roundingExponent);
+ dst->fraction = 1000 * roundingDivisor * ((exploded.tm_usec + roundingDivisor / 2) / roundingDivisor);
+ }
+ else
+ dst->fraction = 1000 * exploded.tm_usec;
+ }
+ }
+ }
+}
+#endif
+
void ODBCAppender::flushBuffer(Pool& p)
{
for (auto& logEvent : _priv->buffer)
{
try
{
- auto sql = getLogStatement(logEvent, p);
- execute(sql, p);
+ if (!_priv->parameterValue.empty())
+ {
+#if LOG4CXX_HAVE_ODBC
+ if (0 == _priv->preparedStatement)
+ _priv->setPreparedStatement(getConnection(p), p);
+ _priv->setParameterValues(logEvent, p);
+ auto ret = SQLExecute(_priv->preparedStatement);
+ if (ret < 0)
+ {
+ throw SQLException(SQL_HANDLE_STMT, _priv->preparedStatement, "Failed to execute prepared statement", p);
+ }
+#endif
+ }
+ else
+ {
+ auto sql = getLogStatement(logEvent, p);
+ execute(sql, p);
+ }
}
catch (SQLException& e)
{
diff --git a/src/main/include/log4cxx/db/odbcappender.h b/src/main/include/log4cxx/db/odbcappender.h
index aa85aaf6..60db2ccb 100644
--- a/src/main/include/log4cxx/db/odbcappender.h
+++ b/src/main/include/log4cxx/db/odbcappender.h
@@ -47,15 +47,25 @@ class LOG4CXX_EXPORT SQLException : public log4cxx::helpers::Exception
/**
The ODBCAppender sends log events to a database.
-<p>Each append call adds to an <code>ArrayList</code> buffer. When
-the buffer is filled each log event is placed in a sql statement
-(which is configured in the <b>sql</b> element or the attached PatternLayout) and executed.
-
-The SQL insert statement pattern must be provided.
-The SQL statement can be specified in the Log4cxx configuration file
-either using the <b>sql</b> parameter element
-or by attaching a PatternLayout layout element.
-
+<p>Each append call adds the spi::LoggingEvent to a buffer.
+When the buffer is full, values are extracted from each spi::LoggingEvent
+and the sql insert statement executed.
+
+The SQL insert statement pattern must be provided
+either in the Log4cxx configuration file
+using the <b>sql</b> parameter element
+or programatically by calling the <code>setSql(String sql)</code> method.
+
+If no <b>ColumnMapping</b> element is provided in the configuration file
+the sql statement is assumed to be a PatternLayout layout.
+In this case all the conversion patterns in PatternLayout
+can be used inside of the statement. (see the test cases for examples)
+
+If the <b>sql</b> element is not provided
+and no <b>ColumnMapping</b> element is provided
+the attached a PatternLayout layout element
+is assumed to contain the sql statement.
+
The following <b>param</b> elements are optional:
- one of <b>DSN</b>, <b>URL</b>, <b>ConnectionString</b> -
The <b>serverName</b> parameter value in the <a href="https://learn.microsoft.com/en-us/sql/odbc/reference/syntax/sqlconnect-function">SQLConnect</a> call.
@@ -67,16 +77,22 @@ The following <b>param</b> elements are optional:
Delay executing the sql until this many logging events are available.
One by default, meaning an sql statement is executed
whenever a logging event is appended.
-
-<p>The <code>setSql(String sql)</code> sets the SQL statement to be
-used for logging -- this statement is sent to a
-<code>PatternLayout</code> (either created automaticly by the
-appender or added by the user). Therefore by default all the
-conversion patterns in <code>PatternLayout</code> can be used
-inside of the statement. (see the test cases for examples)
-
-<p>Overriding the {@link #getLogStatement} method allows more
-explicit control of the statement used for logging.
+- <b>ColumnMapping</b> -
+ One element for each "?" in the <b>sql</b> statement
+ in a sequence corresponding to the columns in the insert statement.
+ The following values are supported:
+ - logger
+ - level
+ - thread
+ - threadname
+ - time
+ - shortfilename
+ - fullfilename
+ - line
+ - class
+ - method
+ - message
+ - ndc
<p>For use as a base class:
@@ -92,18 +108,21 @@ you override getConnection make sure to implement
generated. Typically this would return the connection to the
pool it came from.
-<li>Override getLogStatement to
-produce specialized or dynamic statements. The default uses the
-sql option value.
-
</ul>
An example configuration that writes to the data source named "LoggingDSN" is:
~~~{.xml}
<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
-appender name="SqlAppender" class="ODBCAppender">
+<appender name="SqlAppender" class="ODBCAppender">
<param name="DSN" value="LoggingDSN"/>
- <param name="sql" value="INSERT INTO [SomeDatabaseName].[SomeUserName].[SomeTableName] ([Thread],[LogName],[LogTime],[LogLevel],[FileName],[FileLine],[Message]) VALUES ('%t', '%c','%d{dd MMM yyyy HH:mm:ss.SSS}','%p','%f','%L','%m{'}')" />
+ <param name="sql" value="INSERT INTO [SomeDatabaseName].[SomeUserName].[SomeTableName] ([Thread],[LogName],[LogTime],[LogLevel],[FileName],[FileLine],[Message]) VALUES (?,?,?,?,?,?,?)" />
+ <param name="ColumnMapping" value="thread"/>
+ <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>
<appender name="ASYNC" class="AsyncAppender">
<param name="BufferSize" value="1000"/>
@@ -150,21 +169,18 @@ class LOG4CXX_EXPORT ODBCAppender : public AppenderSkeleton
*/
void append(const spi::LoggingEventPtr& event, helpers::Pool&) override;
+ protected:
/**
- * By default getLogStatement sends the event to the required Layout object.
+ * Sends the event to the attached PatternLayout object.
* The layout will format the given pattern into a workable SQL string.
*
- * Overriding this provides direct access to the LoggingEvent
- * when constructing the logging statement.
- *
*/
- protected:
LogString getLogStatement(const spi::LoggingEventPtr& event,
helpers::Pool& p) const;
/**
*
- * Override this to provide an alertnate method of getting
+ * Override this to provide an alternate method of getting
* connections (such as caching). One method to fix this is to open
* connections at the start of flushBuffer() and close them at the
* end. I use a connection pool outside of ODBCAppender which is
@@ -207,12 +223,9 @@ class LOG4CXX_EXPORT ODBCAppender : public AppenderSkeleton
virtual void flushBuffer(log4cxx::helpers::Pool& p);
/**
- * ODBCAppender requires a layout.
+ * Does this appender require a layout?
* */
- bool requiresLayout() const override
- {
- return true;
- }
+ bool requiresLayout() const override;
/**
* Set pre-formated statement eg: insert into LogTable (msg) values ("%m")
diff --git a/src/main/include/log4cxx/private/odbcappender_priv.h b/src/main/include/log4cxx/private/odbcappender_priv.h
index 4a78ae55..de519582 100644
--- a/src/main/include/log4cxx/private/odbcappender_priv.h
+++ b/src/main/include/log4cxx/private/odbcappender_priv.h
@@ -19,9 +19,26 @@
#define LOG4CXX_ODBCAPPENDER_PRIV
#include <log4cxx/db/odbcappender.h>
+#include <log4cxx/pattern/loggingeventpatternconverter.h>
#include "appenderskeleton_priv.h"
-#include <list>
+#if !defined(LOG4CXX)
+ #define LOG4CXX 1
+#endif
+#include <log4cxx/private/log4cxx_private.h>
+#if LOG4CXX_HAVE_ODBC
+ #if defined(WIN32) || defined(_WIN32)
+ #include <windows.h>
+ #endif
+ #include <sqlext.h>
+#else
+ typedef void* SQLHSTMT;
+ typedef void* SQLPOINTER;
+ typedef uint64_t SQLULEN;
+ typedef int64_t SQLLEN;
+ typedef long SQLINTEGER;
+ typedef short SQLSMALLINT;
+#endif
namespace log4cxx
{
@@ -30,11 +47,14 @@ namespace db
struct ODBCAppender::ODBCAppenderPriv : public AppenderSkeleton::AppenderSkeletonPrivate
{
- ODBCAppenderPriv() :
- AppenderSkeletonPrivate(),
- connection(nullptr),
- env(nullptr),
- bufferSize(1) {}
+ ODBCAppenderPriv()
+ : AppenderSkeletonPrivate()
+ , connection(0)
+ , env(0)
+ , preparedStatement(0)
+ , bufferSize(1)
+ , timeZone(helpers::TimeZone::getDefault())
+ {}
/**
* URL of the DB for default connection handling
@@ -58,20 +78,13 @@ struct ODBCAppender::ODBCAppenderPriv : public AppenderSkeleton::AppenderSkeleto
* sub-class and overriding the <code>getConnection</code> and
* <code>closeConnection</code> methods.
*/
- log4cxx::db::ODBCAppender::SQLHDBC connection;
- log4cxx::db::ODBCAppender::SQLHENV env;
+ SQLHDBC connection;
+ SQLHENV env;
/**
* Stores the string given to the pattern layout for conversion into a SQL
- * statement, eg: insert into LogTable (Thread, File, Message) values
- * ("%t", "%F", "%m")
- *
- * Be careful of quotes in your messages!
- *
- * Also see PatternLayout.
*/
LogString sqlStatement;
-
/**
* size of LoggingEvent buffer before writing to the database.
* Default is 1.
@@ -82,6 +95,31 @@ struct ODBCAppender::ODBCAppenderPriv : public AppenderSkeleton::AppenderSkeleto
* ArrayList holding the buffer of Logging Events.
*/
std::vector<spi::LoggingEventPtr> buffer;
+
+ /** Provides timestamp components
+ */
+ helpers::TimeZonePtr timeZone;
+
+ /**
+ * The prepared statement handle and the bound column names, converters and buffers
+ */
+ SQLHSTMT preparedStatement;
+ struct DataBinding
+ {
+ using ConverterPtr = pattern::LoggingEventPatternConverterPtr;
+ ConverterPtr converter;
+ SQLSMALLINT paramType;
+ SQLULEN paramMaxCharCount;
+ SQLPOINTER paramValue;
+ SQLINTEGER paramValueSize;
+ SQLLEN strLen_or_Ind;
+ };
+ std::vector<LogString> mappedName;
+ std::vector<DataBinding> parameterValue;
+#if LOG4CXX_HAVE_ODBC
+ void setPreparedStatement(SQLHDBC con, helpers::Pool& p);
+ void setParameterValues(const spi::LoggingEventPtr& event, helpers::Pool& p);
+#endif
};
}
diff --git a/src/test/resources/input/xml/odbcAppenderDSN-Log4cxxTest.xml b/src/test/resources/input/xml/odbcAppenderDSN-Log4cxxTest.xml
index ff5bcbe2..8873ab40 100644
--- a/src/test/resources/input/xml/odbcAppenderDSN-Log4cxxTest.xml
+++ b/src/test/resources/input/xml/odbcAppenderDSN-Log4cxxTest.xml
@@ -25,16 +25,30 @@
<param name="ConversionPattern" value="%d %-5p %c{2} - %m%n"/>
</layout>
</appender>
-<appender name="SqlAppender" class="ODBCAppender">
+<appender name="DirectAppender" class="ODBCAppender">
<param name="DSN" value="Log4cxxTest"/>
<param name="sql" value="INSERT INTO [ApplicationLogs].[dbo].[UnitTestLog] ([Thread],[LogName],[LogTime],[LogLevel],[FileName],[FileLine],[Message]) VALUES ('%t', '%c','%d{dd MMM yyyy HH:mm:ss.SSS}','%p','%f','%L','%m{'}')" />
<!--param name="User" value="dbo"/-->
<!--param name="Password" value="myLog4cxxTestPassword"/-->
</appender>
+<appender name="PreparedAppender" class="ODBCAppender">
+ <param name="DSN" value="Log4cxxTest"/>
+ <param name="sql" value="INSERT INTO [ApplicationLogs].[dbo].[UnitTestLog] ([Thread],[LogName],[LogTime],[LogLevel],[FileName],[FileLine],[Message]) VALUES (?,?,?,?,?,?,?)" />
+ <param name="ColumnMapping" value="thread"/>
+ <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"/>
+ <!--param name="User" value="dbo"/-->
+ <!--param name="User" value="dbo"/-->
+ <!--param name="Password" value="myLog4cxxTestPassword"/-->
+</appender>
<appender name="ASYNC" class="AsyncAppender">
<param name="BufferSize" value="1000"/>
<param name="Blocking" value="false"/>
- <appender-ref ref="SqlAppender"/>
+ <appender-ref ref="PreparedAppender"/>
</appender>
<logger name="DB">
<level value="INFO" />