#define PERL_EXT_XS_LOG 1
#include <EXTERN.h>
#include <perl.h>
#include <XSUB.h>
#include <fcntl.h>
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
#include <sys/file.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
#include "logger.h"
char*
get_default_file_path() {
char *path;
SV *sv = get_sv( "XS::Logger::PATH_FILE", 0 );
if ( sv && SvPOK(sv) )
path = SvPV_nolen( sv );
else
path = (char *) DEFAULT_LOG_FILE; /* fallback to default path */
return path;
}
char*
_file_path_for_logger(MyLogger *self) {
if ( strlen(self->filepath) )
return self->filepath;
else
return get_default_file_path();
}
/* c internal functions */
void
do_log(MyLogger *mylogger, logLevel level, const char *fmt, int num_args, ...) {
FILE *fhandle = NULL;
char *path = NULL;
SV *sv = NULL;
/* Get current time */
time_t t = time(NULL);
struct tm lt = {0};
char buf[32];
bool has_logger_object = true;
bool hold_lock = false;
pid_t pid;
bool quiet = false; /* do not display messages on stderr when quiet mode enabled */
localtime_r(&t, <);
if ( level == LOG_DISABLE ) /* to move earlier */
return;
buf[strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", <)] = '\0';
pid = getpid();
/* Note: *mylogger can be a NULL pointer => would fall back to a GV string or a constant from .c to get the filename */
if ( mylogger ) { /* we got a mylogger pointer */
path = _file_path_for_logger( mylogger );
if ( mylogger->pid && mylogger->pid != pid ) {
if (mylogger->fhandle) fclose(mylogger->fhandle);
mylogger->fhandle = NULL;
}
if ( ! mylogger->fhandle ) {
if ( (fhandle = fopen( path, "a" )) == NULL ) /* open in append mode */
croak("Failed to open file \"%s\"", path);
mylogger->fhandle = fhandle; /* save the fhandle for future reuse */
mylogger->pid = pid; /* store the pid which open the file */
ACQUIRE_LOCK_ONCE(fhandle); /* get a lock before moving to the end */
fseek(fhandle, 0, SEEK_END);
}
fhandle = mylogger->fhandle;
quiet = mylogger->quiet;
} else {
path = get_default_file_path();
has_logger_object = false;
if ( (fhandle = fopen( path, "a" )) == NULL ) /* open in append mode */
croak("Failed to open file \"%s\"", path);
ACQUIRE_LOCK_ONCE(fhandle); /* get a lock before moving to the end */
fseek(fhandle, 0, SEEK_END);
}
if ( fhandle ) {
va_list args;
int abs_gmtoff = lt.tm_gmtoff >= 0 ? lt.tm_gmtoff : -1 * lt.tm_gmtoff;
if (num_args) va_start(args, num_args);
ACQUIRE_LOCK_ONCE(fhandle);
/* write the message */
/* header: [timestamp tz] pid LEVEL */
if ( mylogger && mylogger->use_color ) {
M_FPRINTF( fhandle, "[%s %s%02d%02d] %s%-5s%s",
buf,
lt.tm_gmtoff >= 0 ? "+" : "-",
(int) abs_gmtoff / 3600,
( abs_gmtoff % 3600) / 60,
LEVEL_COLORS[level], LOG_LEVEL_NAMES[level], END_COLOR
);
} else {
M_FPRINTF( fhandle, "[%s %s%02d%02d] %-5s",
buf,
lt.tm_gmtoff >= 0 ? "+" : "-",
(int) abs_gmtoff / 3600, ( abs_gmtoff % 3600) / 60,
LOG_LEVEL_NAMES[level]
);
}
{
SV *const dollar_0 = get_sv("0",GV_ADDWARN); /* $0 - application name */
char *str_dollar_0;
if ( !SvPOK(dollar_0) ) { /* probably a better helper to simply get the PV at all cost */
if ( SvIOK(dollar_0) )
SvUPGRADE(dollar_0, SVt_PVIV);
else
croak("dollar_0 is not a string?!");
}
str_dollar_0 = SvPV_nolen( dollar_0 );
M_FPRINTF( fhandle, " %u [%s] ", (unsigned int) pid, str_dollar_0 ); /* print the source */
/* with the pid ? */
/* fprintf( fhandle, " [%u %s] ", (unsigned int) pid, str_dollar_0 ); */
}
{
int len = 0;
//PerlIO_printf( PerlIO_stderr(), "# num_args %d\n", num_args );
if ( fmt && (len=strlen(fmt)) ) {
if (num_args == 0) /* no need to use sprintf when not needed */
M_FPUTS( fmt, fhandle )
else
M_VFPRINTF( fhandle, fmt, args )
}
// only add "\n" if missing from fmt
if ( !len || fmt[len-1] != '\n')
M_FPUTS( "\n", fhandle );
}
if (has_logger_object) fflush(fhandle); /* otherwise we are going to close the ffhandle just after */
if (num_args) va_end(args);
}
RELEASE_LOCK(fhandle); /* only release if acquired before */
if ( !has_logger_object ) fclose( fhandle );
return;
}
/* function exposed to the module */
/* maybe a bad idea to use a prefix */
MODULE = XS__Logger PACKAGE = XS::Logger PREFIX = xlog_
TYPEMAP: <<HERE
MyLogger* T_PTROBJ
XS::Logger T_PTROBJ
HERE
XS::Logger
xlog_new(class, ...)
char* class;
PREINIT:
MyLogger* mylogger;
HV* opts = NULL;
SV **svp;
CODE:
{
Newxz( mylogger, 1, MyLogger );
RETVAL = mylogger;
if( items > 1 ) { /* could also probably use va_start, va_list, ... */
SV *extra = (SV*) ST(1);
if ( SvROK(extra) && SvTYPE(SvRV(extra)) == SVt_PVHV )
opts = (HV*) SvRV( extra );
}
/* default (non zero) values */
mylogger->use_color = true; /* maybe use a GV from the stash to set the default value */
if ( opts ) {
if ( (svp = hv_fetchs(opts, "color", FALSE)) ) {
if (!SvIOK(*svp)) croak("invalid color option value: should be a boolean 1/0");
mylogger->use_color = (bool) SvIV(*svp);
}
if ( (svp = hv_fetchs(opts, "level", FALSE)) ) {
if (!SvIOK(*svp)) croak("invalid log level: should be one integer");
mylogger->level = (logLevel) SvIV(*svp);
}
if ( (svp = hv_fetchs(opts, "quiet", FALSE)) ) {
if (!SvIOK(*svp)) croak("invalid quiet value: should be one integer 0 or 1");
mylogger->quiet = (logLevel) SvIV(*svp);
}
if ( (svp = hv_fetchs(opts, "logfile", FALSE)) || (svp = hv_fetchs(opts, "path", FALSE)) ) {
STRLEN len;
char *src;
if (!SvPOK(*svp)) croak("invalid logfile path: must be a string");
src = SvPV(*svp, len);
if (len >= sizeof(mylogger->filepath))
croak("file path too long max=256!");
strcpy(mylogger->filepath, src); /* do a copy to the object */
}
}
}
OUTPUT:
RETVAL
void
xlog_loggers(...)
ALIAS:
XS::Logger::info = 1
XS::Logger::warn = 2
XS::Logger::error = 3
XS::Logger::die = 4
XS::Logger::panic = 5
XS::Logger::fatal = 6
XS::Logger::debug = 7
PREINIT:
SV *ret;
SV* self; /* optional */
CODE:
{
logLevel level = LOG_DISABLE;
bool dolog = true;
MyLogger* mylogger = NULL; /* can be null when not called on an object */
int args_start_at = 0;
bool should_die = false;
const char *fmt;
MultiValue targs[10] = {0}; /* no need to malloc limited to 10 */
switch (ix) {
case 1: /* info */
level = LOG_INFO;
break;
case 2: /* warn */
level = LOG_WARN;
break;
case 3: /* error */
level = LOG_ERROR;
break;
case 4: /* die */
level = LOG_ERROR;
should_die = true;
break;
case 5: /* panic */
case 6: /* fatal */
level = LOG_FATAL;
should_die = true;
break;
case 7:
level = LOG_DEBUG;
break;
default:
level = LOG_DISABLE;
}
/* check if called as function or method call */
if ( items && SvROK(ST(0)) && SvOBJECT(SvRV(ST(0))) ) { /* check if self is an object */
self = ST(0);
args_start_at = 1;
mylogger = INT2PTR(MyLogger*, SvIV(SvRV(self)));
/* check the caller level */
if ( level < mylogger->level )
dolog = false;
}
if (dolog) {
SV **list;
if ( items < (1 + args_start_at) ) {
fmt = EMPTY_STR;
do_log( mylogger, level, fmt, 0 ); /* do a simple call */
} else if ( items <= ( 11 + args_start_at ) ) { /* set a cap on the maximum of item we can use: 10 arguments + 1 format + 1 for self */
IV i;
I32 nitems = items - args_start_at; /* for self */
//Newx(list, nitems, SV*);
for ( i = args_start_at ; i < items ; ++i ) {
SV *sv = ST(i);
if ( !SvOK(sv) )
croak( "Invalid element item %i - not an SV.", (int) i );
else {
/* do a switch on the type */
if ( i == args_start_at ) { /* the first entry shoulkd be the format */
if ( !SvPOK(sv) ) { /* maybe upgrade to a PV */
if ( SvIOK(sv) )
SvUPGRADE(sv, SVt_PVIV);
else
croak("First argument must be a string.");
}
fmt = SvPV_nolen( sv );
} else {
int ix = i - 1 - args_start_at;
if ( SvIOK(sv) ) { /* SvTYPE(sv) == SVt_IV */
targs[ix].ival = SvIV(sv);
} else if ( SvNOK(sv) ) { // not working for now
//PerlIO_printf( PerlIO_stderr(), "# SV SV %f\n", 1.345 );
//PerlIO_printf( PerlIO_stderr(), "# SV SV %f\n", SvNV(sv) );
targs[ix].fval = SvNV(sv);
} else {
targs[ix].sval = SvPV_nolen(sv);
}
}
}
}
/* not really necessary but probaby better for performance */
switch ( nitems ) {
case 1:
do_log( mylogger, level, fmt, nitems,
targs[0]
);
break;
case 2:
do_log( mylogger, level, fmt, nitems,
targs[0], targs[1]
);
break;
case 3:
do_log( mylogger, level, fmt, nitems,
targs[0], targs[1], targs[2]
);
break;
case 4:
do_log( mylogger, level, fmt, nitems,
targs[0], targs[1], targs[2], targs[3]
);
break;
case 5:
do_log( mylogger, level, fmt, nitems,
targs[0], targs[1], targs[2], targs[3], targs[4]
);
break;
case 6:
do_log( mylogger, level, fmt, nitems,
targs[0], targs[1], targs[2], targs[3], targs[4],
targs[5]
);
break;
case 7:
do_log( mylogger, level, fmt, nitems,
targs[0], targs[1], targs[2], targs[3], targs[4],
targs[5], targs[6]
);
break;
case 8:
do_log( mylogger, level, fmt, nitems,
targs[0], targs[1], targs[2], targs[3], targs[4],
targs[5], targs[6], targs[7]
);
break;
case 9:
do_log( mylogger, level, fmt, nitems,
targs[0], targs[1], targs[2], targs[3], targs[4],
targs[5], targs[6], targs[7], targs[8]
);
break;
case 10:
do_log( mylogger, level, fmt, nitems,
targs[0], targs[1], targs[2], targs[3], targs[4],
targs[5], targs[6], targs[7], targs[8], targs[9]
);
break;
default:
do_log( mylogger, level, fmt, nitems,
targs[0], targs[1], targs[2], targs[3], targs[4],
targs[5], targs[6], targs[7], targs[8], targs[9]
);
}
} else {
croak("Too many args to the caller (max=10).");
}
} /* end of dolog */
if ( should_die ) /* maybe fatal needs to exit */ {
/* FIXME: right now only using the fmt without the args */
/* exit level [panic] [pid=6904] (This is a message) */
croak( "exit level [%s] [pid=%d] (%s)\n", LOG_LEVEL_NAMES_lc[level], getpid(),
fmt
);
}
/* no need to return anything there */
XSRETURN_EMPTY;
}
SV*
xlog_getters(self)
XS::Logger self;
ALIAS:
XS::Logger::get_pid = 1
XS::Logger::use_color = 2
XS::Logger::get_level = 3
XS::Logger::get_quiet = 4
XS::Logger::get_file_path = 5
XS::Logger::logfile = 5
PREINIT:
MyLogger* mylogger;
char *fp;
CODE:
{ /* some getters: mainly used for test for now to access internals */
switch (ix) {
case 1:
RETVAL = newSViv( self->pid );
break;
case 2:
RETVAL = newSViv( self->use_color );
break;
case 3:
RETVAL = newSViv( (int) self->level );
break;
case 4:
RETVAL = newSViv( (int) self->quiet );
break;
case 5:
fp = _file_path_for_logger( self );
RETVAL = newSVpv( fp, strlen(fp) );
break;
default:
XSRETURN_EMPTY;
}
}
OUTPUT:
RETVAL
void
xlog_setters(self, value)
XS::Logger self;
SV* value;
ALIAS:
XS::Logger::set_level = 1
XS::Logger::set_quiet = 2
PREINIT:
MyLogger* mylogger;
CODE:
{ /* improve protection on self/logger here */
switch (ix) {
case 1:
if ( !SvIOK(value) ) croak("invalid level: must be interger.");
self->level = SvIV(value);
break;
case 2:
if ( !SvIOK(value) ) croak("invalid quiet value: must be interger.");
self->quiet = SvIV(value);
break;
default:
croak("undefined setter");
}
XSRETURN_EMPTY;
}
void xlog_DESTROY(self)
XS::Logger self;
PREINIT:
I32* temp;
PPCODE:
{
temp = PL_markstack_ptr++;
if ( self ) {
/* close the file fhandle on destroy if exists */
if ( self->fhandle )
fclose( self->fhandle );
/* free the logger... maybe more to clear from struct */
Safefree(self);
}
if (PL_markstack_ptr != temp) {
/* truly void, because dXSARGS not invoked */
PL_markstack_ptr = temp;
XSRETURN_EMPTY;
/* return empty stack */
} /* must have used dXSARGS; list context implied */
return; /* assume stack size is correct */
}
BOOT:
{
HV *stash;
SV *sv;
stash = gv_stashpvn("XS::Logger", 10, TRUE);
newCONSTSUB(stash, "_loaded", newSViv(1) );
newCONSTSUB(stash, "DEBUG_LOG_LEVEL", newSViv( LOG_DEBUG ) );
newCONSTSUB(stash, "INFO_LOG_LEVEL", newSViv( LOG_INFO ) );
newCONSTSUB(stash, "WARN_LOG_LEVEL", newSViv( LOG_WARN ) );
newCONSTSUB(stash, "ERROR_LOG_LEVEL", newSViv( LOG_ERROR ) );
newCONSTSUB(stash, "FATAL_LOG_LEVEL", newSViv( LOG_FATAL ) );
newCONSTSUB(stash, "DISABLE_LOG_LEVEL", newSViv( LOG_DISABLE ) );
sv = get_sv("XS::Logger::PATH_FILE", GV_ADD|GV_ADDMULTI);
if ( ! SvPOK(sv) ) { /* preserve any value set before loading the module */
SvREFCNT_inc(sv);
sv_setpv(sv, DEFAULT_LOG_FILE);
}
}