blob: 2e757b0dfa7fffc8a98fd1e8f078b9c8833b36e6 [file] [log] [blame]
/*
* 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/odbcappender.h>
#include <log4cxx/helpers/loglog.h>
#include <log4cxx/helpers/optionconverter.h>
#include <log4cxx/helpers/stringhelper.h>
#include <log4cxx/helpers/transcoder.h>
#include <log4cxx/patternlayout.h>
#include <log4cxx/pattern/mdcpatternconverter.h>
#include <apr_strings.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
#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;
#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,
log4cxx::helpers::Pool& p)
: Exception(formatMessage(fHandleType, hInput, prolog, p))
{
}
SQLException::SQLException(const char* msg)
: Exception(msg)
{
}
SQLException::SQLException(const SQLException& src)
: Exception(src)
{
}
const char* SQLException::formatMessage(short fHandleType,
void* hInput, const char* prolog, log4cxx::helpers::Pool& p)
{
std::string strReturn(prolog);
strReturn.append(" - ");
#if LOG4CXX_HAVE_ODBC
SQLCHAR SqlState[6];
SQLCHAR Msg[SQL_MAX_MESSAGE_LENGTH];
SQLINTEGER NativeError;
SQLSMALLINT i;
SQLSMALLINT MsgLen;
SQLRETURN rc2;
// Get the status records.
i = 1;
while ((rc2 = SQLGetDiagRecA(fHandleType, hInput, i, SqlState, &NativeError,
Msg, sizeof(Msg), &MsgLen)) != SQL_NO_DATA)
{
strReturn.append((char*) Msg);
i++;
}
#else
strReturn.append("log4cxx built without ODBC support");
#endif
return apr_pstrdup((apr_pool_t*) p.getAPRPool(), strReturn.c_str());
}
IMPLEMENT_LOG4CXX_OBJECT(ODBCAppender)
#define _priv static_cast<ODBCAppenderPriv*>(m_priv.get())
ODBCAppender::ODBCAppender()
: AppenderSkeleton (std::make_unique<ODBCAppenderPriv>())
{
}
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("mdc", MDCPatternConverter);
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")))
{
setBufferSize((size_t)OptionConverter::toInt(value, 1));
}
else if (StringHelper::equalsIgnoreCase(option, LOG4CXX_STR("PASSWORD"), LOG4CXX_STR("password")))
{
setPassword(value);
}
else if (StringHelper::equalsIgnoreCase(option, LOG4CXX_STR("SQL"), LOG4CXX_STR("sql")))
{
setSql(value);
}
else if (StringHelper::equalsIgnoreCase(option, LOG4CXX_STR("URL"), LOG4CXX_STR("url"))
|| StringHelper::equalsIgnoreCase(option, LOG4CXX_STR("DSN"), LOG4CXX_STR("dsn"))
|| StringHelper::equalsIgnoreCase(option, LOG4CXX_STR("CONNECTIONSTRING"), LOG4CXX_STR("connectionstring")) )
{
setURL(value);
}
else if (StringHelper::equalsIgnoreCase(option, LOG4CXX_STR("USER"), LOG4CXX_STR("user")))
{
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 false;
}
void ODBCAppender::activateOptions(log4cxx::helpers::Pool&)
{
#if !LOG4CXX_HAVE_ODBC
LogLog::error(LOG4CXX_STR("Can not activate ODBCAppender unless compiled with ODBC support."));
#else
if (_priv->mappedName.empty())
{
LogLog::error(LOG4CXX_STR("ODBCAppender column mappings not defined, logging events will not be inserted"));
}
auto specs = getFormatSpecifiers();
for (auto& name : _priv->mappedName)
{
auto lowerName = StringHelper::toLowerCase(name);
auto pItem = specs.find(lowerName);
if (specs.end() == pItem)
{
if (lowerName.size() < 5
|| lowerName.substr(0, 4) != LOG4CXX_STR("mdc{"))
LogLog::error(name + LOG4CXX_STR(" is not a supported ColumnMapping value"));
else // A single MDC entry
{
auto index = lowerName.find(0x7D /* '}' */, 4);
auto len = (lowerName.npos == index ? lowerName.size() : index) - 4;
ODBCAppenderPriv::DataBinding paramData{ 0, 0, 0, 0, 0 };
paramData.converter = std::make_shared<MDCPatternConverter>(lowerName.substr(4, len));
_priv->parameterValue.push_back(paramData);
}
}
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
}
void ODBCAppender::append(const spi::LoggingEventPtr& event, log4cxx::helpers::Pool& p)
{
#if LOG4CXX_HAVE_ODBC
_priv->buffer.push_back(event);
if (_priv->buffer.size() >= _priv->bufferSize)
{
flushBuffer(p);
}
#endif
}
LogString ODBCAppender::getLogStatement(const spi::LoggingEventPtr& event, log4cxx::helpers::Pool& p) const
{
return LogString();
}
void ODBCAppender::execute(const LogString& sql, log4cxx::helpers::Pool& p)
{
}
/* The default behavior holds a single connection open until the appender
is closed (typically when garbage collected).*/
void ODBCAppender::closeConnection(ODBCAppender::SQLHDBC /* con */)
{
}
ODBCAppender::SQLHDBC ODBCAppender::getConnection(log4cxx::helpers::Pool& p)
{
#if LOG4CXX_HAVE_ODBC
SQLRETURN ret;
if (_priv->env == SQL_NULL_HENV)
{
ret = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &_priv->env);
if (ret < 0)
{
SQLException ex(SQL_HANDLE_ENV, _priv->env, "Failed to allocate SQL handle", p);
_priv->env = SQL_NULL_HENV;
throw ex;
}
ret = SQLSetEnvAttr(_priv->env, SQL_ATTR_ODBC_VERSION, (SQLPOINTER) SQL_OV_ODBC3, SQL_IS_INTEGER);
if (ret < 0)
{
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;
}
}
if (_priv->connection == SQL_NULL_HDBC)
{
ret = SQLAllocHandle(SQL_HANDLE_DBC, _priv->env, &_priv->connection);
if (ret < 0)
{
SQLException ex(SQL_HANDLE_DBC, _priv->connection, "Failed to allocate sql handle", p);
_priv->connection = SQL_NULL_HDBC;
throw ex;
}
#if LOG4CXX_LOGCHAR_IS_WCHAR
SQLWCHAR *wUser = nullptr, *wPwd = nullptr;
if (!_priv->databaseUser.empty())
wUser = (SQLWCHAR*)_priv->databaseUser.c_str();
if (!_priv->databasePassword.empty())
wPwd = (SQLWCHAR*)_priv->databasePassword.c_str();
ret = SQLConnectW(_priv->connection
, (SQLWCHAR*)_priv->databaseURL.c_str(), SQL_NTS
, wUser, SQL_NTS
, wPwd, SQL_NTS
);
#elif LOG4CXX_LOGCHAR_IS_UTF8
SQLCHAR *wUser = nullptr, *wPwd = nullptr;
if (!_priv->databaseUser.empty())
wUser = (SQLCHAR*)_priv->databaseUser.c_str();
if (!_priv->databasePassword.empty())
wPwd = (SQLCHAR*)_priv->databasePassword.c_str();
ret = SQLConnectA(_priv->connection
, (SQLCHAR*)_priv->databaseURL.c_str(), SQL_NTS
, wUser, SQL_NTS
, wPwd, SQL_NTS
);
#else
SQLWCHAR* wURL, *wUser = nullptr, *wPwd = nullptr;
encode(&wURL, _priv->databaseURL, 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
, wUser, SQL_NTS
, wPwd, SQL_NTS
);
#endif
if (ret < 0)
{
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;
}
}
return _priv->connection;
#else
return 0;
#endif
}
void ODBCAppender::close()
{
if (_priv->closed)
{
return;
}
Pool p;
try
{
flushBuffer(p);
}
catch (SQLException& e)
{
_priv->errorHandler->error(LOG4CXX_STR("Error closing connection"),
e, ErrorCode::GENERIC_FAILURE);
}
#if LOG4CXX_HAVE_ODBC
if (_priv->connection != SQL_NULL_HDBC)
{
SQLDisconnect(_priv->connection);
SQLFreeHandle(SQL_HANDLE_DBC, _priv->connection);
}
if (_priv->env != SQL_NULL_HENV)
{
SQLFreeHandle(SQL_HANDLE_ENV, _priv->env);
}
#endif
_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 = (0 <= decimalDigits) ? decimalDigits : 6;
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)
{
if (_priv->parameterValue.empty())
_priv->errorHandler->error(LOG4CXX_STR("ODBCAppender column mappings not defined"));
#if LOG4CXX_HAVE_ODBC
else try
{
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);
}
}
catch (SQLException& e)
{
_priv->errorHandler->error(LOG4CXX_STR("Failed to execute sql"), e,
ErrorCode::FLUSH_FAILURE);
}
#endif
}
// clear the buffer of reported events
_priv->buffer.clear();
}
void ODBCAppender::setSql(const LogString& s)
{
_priv->sqlStatement = s;
}
#if LOG4CXX_WCHAR_T_API || LOG4CXX_LOGCHAR_IS_WCHAR_T || defined(WIN32) || defined(_WIN32)
void ODBCAppender::encode(wchar_t** dest, const LogString& src, Pool& p)
{
*dest = Transcoder::wencode(src, p);
}
#endif
void ODBCAppender::encode(unsigned short** dest,
const LogString& src, Pool& p)
{
// worst case double number of characters from UTF-8 or wchar_t
*dest = (unsigned short*)
p.palloc((src.size() + 1) * 2 * sizeof(unsigned short));
unsigned short* current = *dest;
for (LogString::const_iterator i = src.begin();
i != src.end();)
{
unsigned int sv = Transcoder::decode(src, i);
if (sv < 0x10000)
{
*current++ = (unsigned short) sv;
}
else
{
unsigned char u = (unsigned char) (sv >> 16);
unsigned char w = (unsigned char) (u - 1);
unsigned short hs = (0xD800 + ((w & 0xF) << 6) + ((sv & 0xFFFF) >> 10));
unsigned short ls = (0xDC00 + (sv & 0x3FF));
*current++ = (unsigned short) hs;
*current++ = (unsigned short) ls;
}
}
*current = 0;
}
const LogString& ODBCAppender::getSql() const
{
return _priv->sqlStatement;
}
void ODBCAppender::setUser(const LogString& user)
{
_priv->databaseUser = user;
}
void ODBCAppender::setURL(const LogString& url)
{
_priv->databaseURL = url;
}
void ODBCAppender::setPassword(const LogString& password)
{
_priv->databasePassword = password;
}
void ODBCAppender::setBufferSize(size_t newBufferSize)
{
_priv->bufferSize = newBufferSize;
}
const LogString& ODBCAppender::getUser() const
{
return _priv->databaseUser;
}
const LogString& ODBCAppender::getURL() const
{
return _priv->databaseURL;
}
const LogString& ODBCAppender::getPassword() const
{
return _priv->databasePassword;
}
size_t ODBCAppender::getBufferSize() const
{
return _priv->bufferSize;
}