diff --git a/runtest-moduleapi b/runtest-moduleapi index 84cdb9bb8..9bf12677d 100755 --- a/runtest-moduleapi +++ b/runtest-moduleapi @@ -13,4 +13,4 @@ then fi make -C tests/modules && \ -$TCLSH tests/test_helper.tcl --single unit/moduleapi/commandfilter "${@}" +$TCLSH tests/test_helper.tcl --single unit/moduleapi/commandfilter --single unit/moduleapi/infotest "${@}" diff --git a/src/debug.c b/src/debug.c index 1f1157d4a..daf00b14c 100644 --- a/src/debug.c +++ b/src/debug.c @@ -1337,6 +1337,12 @@ void sigsegvHandler(int sig, siginfo_t *info, void *secret) { /* Log dump of processor registers */ logRegisters(uc); + /* Log Modules INFO */ + serverLogRaw(LL_WARNING|LL_RAW, "\n------ MODULES INFO OUTPUT ------\n"); + infostring = modulesCollectInfo(sdsempty(), NULL, 1, 0); + serverLogRaw(LL_WARNING|LL_RAW, infostring); + sdsfree(infostring); + #if defined(HAVE_PROC_MAPS) /* Test memory */ serverLogRaw(LL_WARNING|LL_RAW, "\n------ FAST MEMORY TEST ------\n"); diff --git a/src/module.c b/src/module.c index f4f753c00..0e19c11e2 100644 --- a/src/module.c +++ b/src/module.c @@ -40,6 +40,16 @@ * pointers that have an API the module can call with them) * -------------------------------------------------------------------------- */ +typedef struct RedisModuleInfoCtx { + struct RedisModule *module; + sds requested_section; + sds info; /* info string we collected so far */ + int sections; /* number of sections we collected so far */ + int in_section; /* indication if we're in an active section or not */ +} RedisModuleInfoCtx; + +typedef void (*RedisModuleInfoFunc)(RedisModuleInfoCtx *ctx, int for_crash_report); + /* This structure represents a module inside the system. */ struct RedisModule { void *handle; /* Module dlopen() handle. */ @@ -51,6 +61,7 @@ struct RedisModule { list *using; /* List of modules we use some APIs of. */ list *filters; /* List of filters the module has registered. */ int in_call; /* RM_Call() nesting level */ + RedisModuleInfoFunc info_cb; /* callback for module to add INFO fields. */ }; typedef struct RedisModule RedisModule; @@ -4686,6 +4697,118 @@ int RM_DictCompare(RedisModuleDictIter *di, const char *op, RedisModuleString *k return res ? REDISMODULE_OK : REDISMODULE_ERR; } + + + +/* -------------------------------------------------------------------------- + * Modules Info fields + * -------------------------------------------------------------------------- */ + +/* Used to start a new section, before adding any fields. the section name will + * be prefixed by "_" and must only include A-Z,a-z,0-9. + * When return value is REDISMODULE_ERR, the section should and will be skipped. */ +int RM_AddInfoSection(RedisModuleInfoCtx *ctx, char *name) { + sds full_name = sdscatprintf(sdsdup(ctx->module->name), "_%s", name); + + /* proceed only if: + * 1) no section was requested (emit all) + * 2) the module name was requested (emit all) + * 3) this specific section was requested. */ + if (ctx->requested_section) { + if (strcasecmp(ctx->requested_section, full_name) && + strcasecmp(ctx->requested_section, ctx->module->name)) { + sdsfree(full_name); + ctx->in_section = 0; + return REDISMODULE_ERR; + } + } + if (ctx->sections++) ctx->info = sdscat(ctx->info,"\r\n"); + ctx->info = sdscatprintf(ctx->info, "# %s\r\n", full_name); + ctx->in_section = 1; + sdsfree(full_name); + return REDISMODULE_OK; +} + +/* Used by RedisModuleInfoFunc to add info fields. + * Each field will be automatically prefixed by "_". + * Field names or values must not include \r\n of ":" */ +int RM_AddInfoFieldString(RedisModuleInfoCtx *ctx, char *field, RedisModuleString *value) { + if (!ctx->in_section) + return REDISMODULE_ERR; + ctx->info = sdscatprintf(ctx->info, + "%s_%s:%s\r\n", + ctx->module->name, + field, + (sds)value->ptr); + return REDISMODULE_OK; +} + +int RM_AddInfoFieldCString(RedisModuleInfoCtx *ctx, char *field, char *value) { + if (!ctx->in_section) + return REDISMODULE_ERR; + ctx->info = sdscatprintf(ctx->info, + "%s_%s:%s\r\n", + ctx->module->name, + field, + value); + return REDISMODULE_OK; +} + +int RM_AddInfoFieldDouble(RedisModuleInfoCtx *ctx, char *field, double value) { + if (!ctx->in_section) + return REDISMODULE_ERR; + ctx->info = sdscatprintf(ctx->info, + "%s_%s:%.17g\r\n", + ctx->module->name, + field, + value); + return REDISMODULE_OK; +} + +int RM_AddInfoFieldLongLong(RedisModuleInfoCtx *ctx, char *field, long long value) { + if (!ctx->in_section) + return REDISMODULE_ERR; + ctx->info = sdscatprintf(ctx->info, + "%s_%s:%lld\r\n", + ctx->module->name, + field, + value); + return REDISMODULE_OK; +} + +int RM_AddInfoFieldULongLong(RedisModuleInfoCtx *ctx, char *field, unsigned long long value) { + if (!ctx->in_section) + return REDISMODULE_ERR; + ctx->info = sdscatprintf(ctx->info, + "%s_%s:%llu\r\n", + ctx->module->name, + field, + value); + return REDISMODULE_OK; +} + +int RM_RegisterInfoFunc(RedisModuleCtx *ctx, RedisModuleInfoFunc cb) { + ctx->module->info_cb = cb; + return REDISMODULE_OK; +} + +sds modulesCollectInfo(sds info, sds section, int for_crash_report, int sections) { + dictIterator *di = dictGetIterator(modules); + dictEntry *de; + + while ((de = dictNext(di)) != NULL) { + struct RedisModule *module = dictGetVal(de); + if (!module->info_cb) + continue; + RedisModuleInfoCtx info_ctx = {module, section, info, sections, 0}; + module->info_cb(&info_ctx, for_crash_report); + info = info_ctx.info; + sections = info_ctx.sections; + } + dictReleaseIterator(di); + return info; +} + /* -------------------------------------------------------------------------- * Modules utility APIs * -------------------------------------------------------------------------- */ @@ -5490,4 +5613,11 @@ void moduleRegisterCoreAPI(void) { REGISTER_API(CommandFilterArgInsert); REGISTER_API(CommandFilterArgReplace); REGISTER_API(CommandFilterArgDelete); + REGISTER_API(RegisterInfoFunc); + REGISTER_API(AddInfoSection); + REGISTER_API(AddInfoFieldString); + REGISTER_API(AddInfoFieldCString); + REGISTER_API(AddInfoFieldDouble); + REGISTER_API(AddInfoFieldLongLong); + REGISTER_API(AddInfoFieldULongLong); } diff --git a/src/redismodule.h b/src/redismodule.h index b9c73957b..1feb14a68 100644 --- a/src/redismodule.h +++ b/src/redismodule.h @@ -160,6 +160,7 @@ typedef struct RedisModuleDict RedisModuleDict; typedef struct RedisModuleDictIter RedisModuleDictIter; typedef struct RedisModuleCommandFilterCtx RedisModuleCommandFilterCtx; typedef struct RedisModuleCommandFilter RedisModuleCommandFilter; +typedef struct RedisModuleInfoCtx RedisModuleInfoCtx; typedef int (*RedisModuleCmdFunc)(RedisModuleCtx *ctx, RedisModuleString **argv, int argc); typedef void (*RedisModuleDisconnectFunc)(RedisModuleCtx *ctx, RedisModuleBlockedClient *bc); @@ -173,6 +174,7 @@ typedef void (*RedisModuleTypeFreeFunc)(void *value); typedef void (*RedisModuleClusterMessageReceiver)(RedisModuleCtx *ctx, const char *sender_id, uint8_t type, const unsigned char *payload, uint32_t len); typedef void (*RedisModuleTimerProc)(RedisModuleCtx *ctx, void *data); typedef void (*RedisModuleCommandFilterFunc) (RedisModuleCommandFilterCtx *filter); +typedef void (*RedisModuleInfoFunc)(RedisModuleInfoCtx *ctx, int for_crash_report); #define REDISMODULE_TYPE_METHOD_VERSION 1 typedef struct RedisModuleTypeMethods { @@ -317,6 +319,13 @@ RedisModuleString *REDISMODULE_API_FUNC(RedisModule_DictNext)(RedisModuleCtx *ct RedisModuleString *REDISMODULE_API_FUNC(RedisModule_DictPrev)(RedisModuleCtx *ctx, RedisModuleDictIter *di, void **dataptr); int REDISMODULE_API_FUNC(RedisModule_DictCompareC)(RedisModuleDictIter *di, const char *op, void *key, size_t keylen); int REDISMODULE_API_FUNC(RedisModule_DictCompare)(RedisModuleDictIter *di, const char *op, RedisModuleString *key); +int REDISMODULE_API_FUNC(RedisModule_RegisterInfoFunc)(RedisModuleCtx *ctx, RedisModuleInfoFunc cb); +int REDISMODULE_API_FUNC(RedisModule_AddInfoSection)(RedisModuleInfoCtx *ctx, char *name); +int REDISMODULE_API_FUNC(RedisModule_AddInfoFieldString)(RedisModuleInfoCtx *ctx, char *field, RedisModuleString *value); +int REDISMODULE_API_FUNC(RedisModule_AddInfoFieldCString)(RedisModuleInfoCtx *ctx, char *field, char *value); +int REDISMODULE_API_FUNC(RedisModule_AddInfoFieldDouble)(RedisModuleInfoCtx *ctx, char *field, double value); +int REDISMODULE_API_FUNC(RedisModule_AddInfoFieldLongLong)(RedisModuleInfoCtx *ctx, char *field, long long value); +int REDISMODULE_API_FUNC(RedisModule_AddInfoFieldULongLong)(RedisModuleInfoCtx *ctx, char *field, unsigned long long value); /* Experimental APIs */ #ifdef REDISMODULE_EXPERIMENTAL_API @@ -490,6 +499,13 @@ static int RedisModule_Init(RedisModuleCtx *ctx, const char *name, int ver, int REDISMODULE_GET_API(DictPrev); REDISMODULE_GET_API(DictCompare); REDISMODULE_GET_API(DictCompareC); + REDISMODULE_GET_API(RegisterInfoFunc); + REDISMODULE_GET_API(AddInfoSection); + REDISMODULE_GET_API(AddInfoFieldString); + REDISMODULE_GET_API(AddInfoFieldCString); + REDISMODULE_GET_API(AddInfoFieldDouble); + REDISMODULE_GET_API(AddInfoFieldLongLong); + REDISMODULE_GET_API(AddInfoFieldULongLong); #ifdef REDISMODULE_EXPERIMENTAL_API REDISMODULE_GET_API(GetThreadSafeContext); diff --git a/src/server.c b/src/server.c index 4337b8f01..c671e375d 100644 --- a/src/server.c +++ b/src/server.c @@ -3809,12 +3809,15 @@ sds genRedisInfoString(char *section) { time_t uptime = server.unixtime-server.stat_starttime; int j; struct rusage self_ru, c_ru; - int allsections = 0, defsections = 0; + int allsections = 0, defsections = 0, everything = 0, modules = 0; int sections = 0; if (section == NULL) section = "default"; allsections = strcasecmp(section,"all") == 0; defsections = strcasecmp(section,"default") == 0; + everything = strcasecmp(section,"everything") == 0; + modules = strcasecmp(section,"modules") == 0; + if (everything) allsections = 1; getrusage(RUSAGE_SELF, &self_ru); getrusage(RUSAGE_CHILDREN, &c_ru); @@ -4357,6 +4360,17 @@ sds genRedisInfoString(char *section) { } } } + + /* Get info from modules. + * if user asked for "everything" or "modules", or a specific section + * that's not found yet. */ + if (everything || modules || + (!allsections && !defsections && sections==0)) { + info = modulesCollectInfo(info, + everything || modules ? NULL: section, + 0, /* not a crash report */ + sections); + } return info; } diff --git a/src/server.h b/src/server.h index b200a6696..ddddd84f0 100644 --- a/src/server.h +++ b/src/server.h @@ -1528,6 +1528,7 @@ void moduleAcquireGIL(void); void moduleReleaseGIL(void); void moduleNotifyKeyspaceEvent(int type, const char *event, robj *key, int dbid); void moduleCallCommandFilters(client *c); +sds modulesCollectInfo(sds info, sds section, int for_crash_report, int sections); /* Utils */ long long ustime(void); diff --git a/tests/modules/Makefile b/tests/modules/Makefile index 014d20afa..66bf6de35 100644 --- a/tests/modules/Makefile +++ b/tests/modules/Makefile @@ -13,12 +13,17 @@ endif .SUFFIXES: .c .so .xo .o -all: commandfilter.so +all: commandfilter.so infotest.so .c.xo: $(CC) -I../../src $(CFLAGS) $(SHOBJ_CFLAGS) -fPIC -c $< -o $@ commandfilter.xo: ../../src/redismodule.h +infotest.xo: ../../src/redismodule.h commandfilter.so: commandfilter.xo $(LD) -o $@ $< $(SHOBJ_LDFLAGS) $(LIBS) -lc + +infotest.so: infotest.xo + $(LD) -o $@ $< $(SHOBJ_LDFLAGS) $(LIBS) -lc + diff --git a/tests/modules/infotest.c b/tests/modules/infotest.c new file mode 100644 index 000000000..d53ea2126 --- /dev/null +++ b/tests/modules/infotest.c @@ -0,0 +1,32 @@ +#include "redismodule.h" + +#include + +void InfoFunc(RedisModuleInfoCtx *ctx, int for_crash_report) { + RedisModule_AddInfoSection(ctx, "Spanish"); + RedisModule_AddInfoFieldCString(ctx, "uno", "one"); + RedisModule_AddInfoFieldLongLong(ctx, "dos", 2); + + RedisModule_AddInfoSection(ctx, "Italian"); + RedisModule_AddInfoFieldLongLong(ctx, "due", 2); + RedisModule_AddInfoFieldDouble(ctx, "tre", 3.3); + + if (for_crash_report) { + RedisModule_AddInfoSection(ctx, "Klingon"); + RedisModule_AddInfoFieldCString(ctx, "one", "wa’"); + RedisModule_AddInfoFieldCString(ctx, "two", "cha’"); + RedisModule_AddInfoFieldCString(ctx, "three", "wej"); + } + +} + +int RedisModule_OnLoad(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { + REDISMODULE_NOT_USED(argv); + REDISMODULE_NOT_USED(argc); + if (RedisModule_Init(ctx,"infotest",1,REDISMODULE_APIVER_1) + == REDISMODULE_ERR) return REDISMODULE_ERR; + + if (RedisModule_RegisterInfoFunc(ctx, InfoFunc) == REDISMODULE_ERR) return REDISMODULE_ERR; + + return REDISMODULE_OK; +} diff --git a/tests/unit/moduleapi/infotest.tcl b/tests/unit/moduleapi/infotest.tcl new file mode 100644 index 000000000..143f9050a --- /dev/null +++ b/tests/unit/moduleapi/infotest.tcl @@ -0,0 +1,53 @@ +set testmodule [file normalize tests/modules/infotest.so] + +# Return value for INFO property +proc field {info property} { + if {[regexp "\r\n$property:(.*?)\r\n" $info _ value]} { + set _ $value + } +} + +start_server {tags {"modules"}} { + r module load $testmodule log-key 0 + + test {module info all} { + set info [r info all] + # info all does not contain modules + assert { ![string match "*Spanish*" $info] } + assert { [string match "*used_memory*" $info] } + } + + test {module info everything} { + set info [r info everything] + # info everything contains all default sections, but not ones for crash report + assert { [string match "*Spanish*" $info] } + assert { [string match "*Italian*" $info] } + assert { [string match "*used_memory*" $info] } + assert { ![string match "*Klingon*" $info] } + field $info infotest_dos + } {2} + + test {module info modules} { + set info [r info modules] + # info all does not contain modules + assert { [string match "*Spanish*" $info] } + assert { ![string match "*used_memory*" $info] } + } + + test {module info one module} { + set info [r info INFOTEST] + # info all does not contain modules + assert { [string match "*Spanish*" $info] } + assert { ![string match "*used_memory*" $info] } + } + + test {module info one section} { + set info [r info INFOTEST_SPANISH] + assert { ![string match "*used_memory*" $info] } + assert { ![string match "*Italian*" $info] } + field $info infotest_uno + } {one} + + # TODO: test crash report. + +}