driver/log.c (335 lines of code) (raw):
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
#include <stdlib.h>
#include <stdarg.h>
#include <stdio.h>
#include <time.h>
#include <assert.h>
#include "defs.h"
#include "log.h"
#ifdef _WIN32
#define FILE_PATH_SEPARATOR '\\'
#else /* _WIN32 */
#define FILE_PATH_SEPARATOR '/'
#endif /* _WIN32 */
#define LOG_LEVEL_SEPARATOR '?'
/*
* https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/strerror-s-strerror-s-wcserror-s-wcserror-s :
* "Your string message can be, at most, 94 characters long."
* Size of buffer to fetch errno-to-string message.
*/
#define LOG_ERRNO_BUF_SIZE 128
/* global file log */
esodbc_filelog_st *_gf_log = NULL;
#ifdef WITH_EXTENDED_BUFF_LOG
static char **log_buffs = NULL;
static size_t log_buffs_cnt = 0;
static esodbc_mutex_lt log_buffs_mux = ESODBC_MUX_SINIT;
#endif /* WITH_EXTENDED_BUFF_LOG */
BOOL log_init()
{
int cnt;
wchar_t *qmark; /* question mark position */
wstr_st str_level;
int log_level;
/* PID buffer */
wchar_t pid_buff[sizeof("4294967295")];
wstr_st pid = (wstr_st) {
pid_buff, sizeof(pid_buff)/sizeof(*pid_buff)
};
/* directory path */
wchar_t dpath_buff[MAX_PATH + 1];
wstr_st dpath = (wstr_st) {
dpath_buff, sizeof(dpath_buff)/sizeof(*dpath_buff)
};
/* full file path */
wchar_t fpath_buff[MAX_PATH + 1];
wstr_st fpath = (wstr_st) {
fpath_buff, sizeof(fpath_buff)/sizeof(*fpath_buff)
};
cnt = GetEnvironmentVariable(MK_WPTR(ESODBC_LOG_DIR_ENV_VAR),
dpath.str, (DWORD)dpath.cnt);
assert(0 <= cnt);
if (! cnt) { /* 0 means error */
/* env var wasn't defined OR error occured (which we can't log). */
return GetLastError() == ERROR_ENVVAR_NOT_FOUND;
} else if (dpath.cnt <= (size_t)cnt) {
/* path buffer too small */
assert(0);
return FALSE;
}
/* is there a log level specified? */
if ((qmark = wcschr(dpath.str, LOG_LEVEL_SEPARATOR))) {
*qmark = 0; /* end the path here */
dpath.cnt = qmark - dpath.str; /* adjust the length of path */
str_level.str = qmark + 1;
str_level.cnt = cnt - dpath.cnt - /* separator */1;
log_level = parse_log_level(&str_level);
} else {
dpath.cnt = cnt;
/* default log level, if not specified, is debug */
log_level = LOG_LEVEL_DBG;
}
pid.cnt = i64tot((int64_t)GetCurrentProcessId(), pid.str, /*w?*/TRUE);
if (pid.cnt <= 0) {
assert(0);
return FALSE;
}
if (! filelog_print_path(&fpath, &dpath, &pid)) {
return FALSE;
}
_gf_log = filelog_new(&fpath, log_level);
return _gf_log != NULL;
}
void log_cleanup()
{
# ifdef WITH_EXTENDED_BUFF_LOG
size_t i;
if (log_buffs) {
for (i = 0; i < log_buffs_cnt; i ++) {
free(log_buffs[i]);
}
free(log_buffs);
log_buffs = NULL;
log_buffs_cnt = 0;
ESODBC_MUX_DEL(&log_buffs_mux);
}
# endif /* WITH_EXTENDED_BUFF_LOG */
if (_gf_log) {
filelog_del(_gf_log);
_gf_log = NULL;
}
}
int parse_log_level(wstr_st *level)
{
if (level->cnt < 1) {
return LOG_LEVEL_DISABLED;
}
/* first letter will indicate the log level */
switch ((unsigned)level->str[0] | 0x20) { /* ~tolower(), ascii set only */
case 'e':
return LOG_LEVEL_ERR;
case 'w':
return LOG_LEVEL_WARN;
case 'i':
return LOG_LEVEL_INFO;
case 'd':
return LOG_LEVEL_DBG;
}
return LOG_LEVEL_DISABLED;
}
/*
* Fully qualified path name of the log file:
* <dir_path>\<E._LOG_FILE_PREFIX>_<datetime>_<ident><E._LOG_FILE_SUFFIX>
* Example:
* C:\Users\username\AppData\Local\Temp\esodbc_20181231235959_233.log
*/
BOOL filelog_print_path(wstr_st *dest, wstr_st *dir_path, wstr_st *ident)
{
wstr_st dir = *dir_path;
int cnt;
time_t now = time(NULL);
struct tm *then = localtime(&now); /* "single tm structure per thread" */
if (! then) {
assert(0); /* should not happen */
return FALSE;
}
/* strip trailing path separator */
for (; 0 < dir.cnt; dir.cnt --) {
if (dir.str[dir.cnt - 1] != MK_WPTR(FILE_PATH_SEPARATOR)) {
break;
}
}
if (dir.cnt <= 0) {
/* input was just '\' (or empty) */
return FALSE;
}
/* build the log full path name */
/* swprintf fails if formatted string would overrun the buffer, while
* _snwprintf doesn't (though also not portable) */
cnt = _snwprintf(dest->str, dest->cnt,
L"%.*s" "%c" "%s" "_%d%.2d%.2d%.2d%.2d%.2d_" "%.*s" "%s",
(int)dir.cnt, dir.str,
FILE_PATH_SEPARATOR,
MK_WPTR(ESODBC_LOG_FILE_PREFIX),
then->tm_year + 1900, then->tm_mon + 1, then->tm_mday,
then->tm_hour, then->tm_min, then->tm_sec,
(int)ident->cnt, ident->str,
MK_WPTR(ESODBC_LOG_FILE_SUFFIX));
if (cnt <= 0 || dest->cnt <= (size_t)cnt) {
/* fpath buffer is too small */
return FALSE;
} else {
dest->cnt = cnt;
}
return TRUE;
}
BOOL filelog_reset(esodbc_filelog_st *log)
{
if (ESODBC_LOG_MAX_RETRY < log->fails) {
/* disable logging alltogether on this logger */
log->level = LOG_LEVEL_DISABLED;
log->handle = INVALID_HANDLE_VALUE;
return FALSE;
}
if (log->handle != INVALID_HANDLE_VALUE) {
CloseHandle(log->handle);
}
log->handle = CreateFile(
log->path, /* file name ("path") */
GENERIC_WRITE, /* desired access */
FILE_SHARE_WRITE, /* share mode */
NULL, /* security attributes */
OPEN_ALWAYS, /* creation disposition */
FILE_ATTRIBUTE_NORMAL, /* flags & attributes */
NULL /* template */);
if (log->handle == INVALID_HANDLE_VALUE) {
/* log into general logger, if available */
ERRN("failed to open log file `" LWPD "`.", log->path);
log->fails ++;
return FALSE;
}
return TRUE;
}
esodbc_filelog_st *filelog_new(wstr_st *path, int level)
{
esodbc_filelog_st *log;
DWORD attrs;
size_t i;
assert (0 < path->cnt);
/* find the last separator in path */
for (i = path->cnt - 1; i && path->str[i] != MK_WPTR(FILE_PATH_SEPARATOR);
i --) {
;
}
assert(0 < i);
path->str[i] = L'\0';
attrs = GetFileAttributes(path->str);
path->str[i] = MK_WPTR(FILE_PATH_SEPARATOR);
/* minimum validation (dir could still not be writable for user); but
* acceptable tradeoff to (1) only creating files if needed; and (2) not
* creating and deleting a file for connections that might eventually not
* log one message */
if ((attrs == INVALID_FILE_ATTRIBUTES) ||
(attrs & FILE_ATTRIBUTE_DIRECTORY) == 0) {
/* log into general logger, if available */
ERR("invalid directory in log file path `" LWPDL "`.", LWSTR(path));
return NULL;
}
if (! (log = malloc(sizeof(*log) +
(path->cnt + /*\0*/1) * sizeof(*log->path)))) {
return NULL;
}
memset(log, 0, sizeof(*log));
log->path = (wchar_t *)((char *)log + sizeof(*log));
wcsncpy(log->path, path->str, path->cnt);
log->path[path->cnt] = L'\0';
log->level = level;
log->handle = INVALID_HANDLE_VALUE;
ESODBC_MUX_INIT(&log->mux);
if (LOG_LEVEL_INFO <= level) {
#ifndef NDEBUG
_LOG(log, LOG_LEVEL_INFO, /*werr*/0, "level: %d, file: " LWPDL ".",
level, LWSTR(path));
#endif /* NDEBUG */
_LOG(log, LOG_LEVEL_INFO, /*werr*/0, "driver version: %s.",
ESODBC_DRIVER_VER);
}
return log;
}
void filelog_del(esodbc_filelog_st *log)
{
if (! log) {
return;
}
if (log->handle != INVALID_HANDLE_VALUE) {
CloseHandle(log->handle);
}
ESODBC_MUX_DEL(&log->mux);
free(log);
}
static BOOL filelog_write(esodbc_filelog_st *log, char *buff, size_t cnt)
{
DWORD written;
/* write the buffer to file */
if (! WriteFile(
log->handle, /*handle*/
buff, /* buffer */
(DWORD)(cnt * sizeof(buff[0])), /*bytes to write */
&written /* bytes written */,
NULL /*overlap*/)) {
log->fails ++;
/* log into general logger, if available */
if (log != _gf_log) { /* avoid spin for general logger's first msg */
ERRN("failed writing into log file `" LWPD "`.", log->path);
}
if (filelog_reset(log)) {
/* reattempt the write, if reset is successfull */
if (filelog_write(log, buff, cnt)) {
log->fails = 0;
return TRUE;
}
}
return FALSE;
} else {
#ifndef NDEBUG
#ifdef _WIN32
//FlushFileBuffers(log->handle);
#endif /* _WIN32 */
#endif /* NDEBUG */
}
return TRUE;
}
static inline void filelog_log(esodbc_filelog_st *log,
int level, int werrno, const char *func, const char *srcfile, int lineno,
const char *fmt, va_list args)
{
time_t now = time(NULL);
int ret;
size_t pos;
# ifndef WITH_EXTENDED_BUFF_LOG
char buff[ESODBC_LOG_BUF_SIZE];
const size_t buff_sz = ESODBC_LOG_BUF_SIZE;
# else /* !WITH_EXTENDED_BUFF_LOG */
static thread_local char *buff = NULL;
const size_t buff_sz = ESODBC_EXT_LOG_BUF_SIZE;
char **r;
# endif /* !WITH_EXTENDED_BUFF_LOG */
char ebuff[LOG_ERRNO_BUF_SIZE];
const char *sfile, *next;
/* keep in sync with esodbc_log_levels */
static const char *level2str[] = { "ERROR", "WARN", "INFO", "DEBUG", };
assert(level < sizeof(level2str)/sizeof(level2str[0]));
/* if extended logging is needed (such as when needing an entire server
* answer), the (too large for the stack) log printing buffer needs to be
* allocate on heap and tracked to be freed on lib process detach */
# ifdef WITH_EXTENDED_BUFF_LOG
if (! buff) {
if (! (buff = malloc(buff_sz))) {
return;
}
ESODBC_MUX_LOCK(&log_buffs_mux);
if (! (r = realloc(log_buffs, (log_buffs_cnt + 1) * sizeof(char *)))) {
free(buff);
buff = NULL;
return;
}
log_buffs = r;
log_buffs[log_buffs_cnt ++] = buff;
ESODBC_MUX_UNLOCK(&log_buffs_mux);
}
# endif /* WITH_EXTENDED_BUFF_LOG */
if (ctime_s(buff, buff_sz, &now)) {
/* writing failed */
pos = 0;
} else {
pos = strnlen(buff, buff_sz) - /*\n*/1;
/*
* https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/ctime-s-ctime32-s-ctime64-s-wctime-s-wctime32-s-wctime64-s :
* """
* The return value string contains exactly 26 characters and has the
* form: Wed Jan 02 02:03:55 1980\n\0
* """
* Position on '\n'.
*/
assert(pos == 24);
}
/* drop path from source file name */
for (next = srcfile; next; next = strchr(sfile, FILE_PATH_SEPARATOR)) {
sfile = next + 1;
}
/* write the debugging prefix */
if ((ret = snprintf(buff + pos, buff_sz - pos, " - [%s] %s()@%s:%d ",
level2str[level], func, sfile, lineno)) < 0) {
return;
} else {
pos += ret;
}
/* write the error number details */
if (werrno) {
ret = snprintf(buff + pos, buff_sz - pos, "(%d:", errno);
if (ret < 0) {
return;
} else {
pos += ret;
}
if (strerror_s(ebuff, sizeof(ebuff), errno)) {
return;
}
ret = snprintf(buff + pos, buff_sz - pos, "%s) ", ebuff);
if (ret < 0) {
return;
} else {
pos += ret;
}
}
/* write user's message */
ret = vsnprintf(buff + pos, buff_sz - pos, fmt, args);
if (ret < 0) {
return;
} else {
pos += ret;
}
/* if overrunning, correct the pos, to be able to add a \n\0 */
if (buff_sz < pos + /*\n\0*/2) {
pos = buff_sz - 2;
}
ret = snprintf(buff + pos, buff_sz - pos, "\n");
if (0 <= ret) {
pos += ret;
}
assert(pos <= buff_sz);
ESODBC_MUX_LOCK(&log->mux);
filelog_write(log, buff, pos);
ESODBC_MUX_UNLOCK(&log->mux);
}
void _esodbc_log(esodbc_filelog_st *log, int lvl, int werrno,
const char *func, const char *srcfile, int lineno, const char *fmt, ...)
{
va_list args;
va_start(args, fmt);
filelog_log(log, lvl, werrno, func, srcfile, lineno, fmt, args);
va_end(args);
}
/* vim: set noet fenc=utf-8 ff=dos sts=0 sw=4 ts=4 tw=78 : */