mirror of
https://github.com/valkey-io/valkey.git
synced 2026-05-06 05:26:42 -04:00
6444717517
## Add Command Result Event Notifications for Modules ### Summary 1. Adds new server events `ValkeyModuleEvent_CommandResultSuccess` and `ValkeyModuleEvent_CommandResultFailure` for that can notify subscribed modules after command execution. This enables modules to implement audit logging, error monitoring, performance tracking, and observability without modifying core server code. 2. Adds new server event `ValkeyModuleEvent_CommandResultACLDenied` for commands rejected by ACL. Together with PR #2237 this covers auditing of authentication and authorisation. ### Motivation There is currently no module API to observe command outcomes after execution or to capture ACL denied commands. Modules that need audit logging or error monitoring have no mechanism to be notified when commands succeed or fail, what arguments were used, how long they took, or how many keys were modified. This feature fills that gap using the existing `ValkeyModule_SubscribeToServerEvent()` infrastructure. ### API #### Events | Event | Description | |---|---| | `ValkeyModuleEvent_CommandResultSuccess` | Fired after a command completes successfully | | `ValkeyModuleEvent_CommandResultFailure` | Fired after a command returns an error | | `ValkeyModuleEvent_CommandACLDenied` | Fired after a command is rejected by ACL | These are separate events (not sub-events), so modules can for example only subscribe to failures without incurring any callback overhead for successful commands. #### Event Data: `ValkeyModuleCommandResultInfo` The `data` pointer passed to the callback can be cast to `ValkeyModuleCommandResultInfo`: ```c typedef struct ValkeyModuleCommandResultInfo { uint64_t version; /* Version of this structure for ABI compat. */ const char *command_name; /* Full command name (e.g., "SET", "CLIENT|LIST"). */ long long duration_us; /* Execution duration in microseconds. */ long long dirty; /* Number of keys modified. */ uint64_t client_id; /* Client ID that executed the command. */ int is_module_client; /* 1 if command was from RM_Call, 0 otherwise. */ int argc; /* Number of command arguments. */ ValkeyModuleString **argv; /* Command arguments array (zero-copy, read-only). */ int acl_deny_reason; /* ACL_DENIED_CMD/KEY/CHANNEL/AUTH; 0 for non-ACL events */ const char *acl_object; /* Denied resource name (key/channel); NULL for CMD/AUTH */ } ValkeyModuleCommandResultInfoV1; ``` The struct is versioned (`VALKEYMODULE_COMMANDRESULTINFO_VERSION`) for forward-compatible API evolution. ### Usage Example ```c /* Callback receives events for whichever event(s) you subscribed to */ void OnCommandResult(ValkeyModuleCtx *ctx, ValkeyModuleEvent eid, uint64_t subevent, void *data) { VALKEYMODULE_NOT_USED(ctx); VALKEYMODULE_NOT_USED(subevent); ValkeyModuleCommandResultInfo *info = (ValkeyModuleCommandResultInfo *)data; if (info->version != VALKEYMODULE_COMMANDRESULTINFO_VERSION) return; int failed = (eid.id == VALKEYMODULE_EVENT_COMMAND_RESULT_FAILURE); /* Access fields directly */ printf("command=%s status=%s duration=%lldus dirty=%lld client=%llu\n", info->command_name, failed ? "FAIL" : "OK", info->duration_us, info->dirty, info->client_id); /* Access argv (read-only, zero-copy) */ for (int i = 0; i < info->argc; i++) { size_t len; const char *arg = ValkeyModule_StringPtrLen(info->argv[i], &len); printf(" argv[%d] = %.*s\n", i, (int)len, arg); } } /* Subscribe in ValkeyModule_OnLoad or at runtime */ /* Option A: command failures only (recommended for audit logging) */ ValkeyModule_SubscribeToServerEvent(ctx, ValkeyModuleEvent_CommandResultFailure, OnCommandResult); /* Option B: command successes only */ ValkeyModule_SubscribeToServerEvent(ctx, ValkeyModuleEvent_CommandResultSuccess, OnCommandResult); /* Option C: both command outcomes*/ ValkeyModule_SubscribeToServerEvent(ctx, ValkeyModuleEvent_CommandResultSuccess, OnCommandResult); ValkeyModule_SubscribeToServerEvent(ctx, ValkeyModuleEvent_CommandResultFailure, OnCommandResult); /* Subscribe to ACL Denied */ ValkeyModule_SubscribeToServerEvent(ctx, ValkeyModuleEvent_CommandResultACLDenied, onCommandResult); /* Unsubscribe pass NULL callback */ ValkeyModule_SubscribeToServerEvent(ctx, ValkeyModuleEvent_CommandResultFailure, NULL); ``` ### Design Decisions - **Separate events instead of sub-events**: Modules subscribing only to failures have zero overhead for successful commands (~2ns listener-list check vs ~30ns callback invocation per command). This is critical since success events fire on the hot path of every command. - **Stack-allocated info struct**: The `ValkeyModuleCommandResultInfoV1` is built on the stack ΓÇö no heap allocation per event. - **Zero-copy argv**: Arguments are passed directly from the client's argv array. Any integer-encoded arguments (from `tryObjectEncoding()` during command execution) are decoded to string-encoded objects before being passed to the callback, ensuring compatibility with `ValkeyModule_StringPtrLen()`. - **Early exit**: If no modules are subscribed to any server events, the event firing function returns immediately before building the info struct. - **Uses existing server event infrastructure**: Follows the `ValkeyModule_SubscribeToServerEvent()` pattern used by all other server events, rather than introducing a new callback mechanism. ### Files Changed | File | Change | |---|---| | `src/valkeymodule.h` | Event IDs, event constants, `ValkeyModuleCommandResultInfoV1` struct | | `src/module.c` | `moduleFireCommandResultEvent()`, event documentation, event version entries | | `src/module.h` | Function declaration | | `src/server.c` | Call `moduleFireCommandResultEvent()` from `call()` after command execution | | `src/server.c` | Call to `moduleFireCommandACLDeniedEvent` in `processCommand` after ACL rejection | | `tests/modules/commandresult.c` | Test module exercising the full API | | `tests/unit/moduleapi/commandresult.tcl` | Integration tests | --------- Signed-off-by: martinrvisser <mvisser@hotmail.com> Signed-off-by: martinrvisser <martinrvisser@users.noreply.github.com> Co-authored-by: Ricardo Dias <rjd15372@gmail.com>
92 lines
1.9 KiB
Makefile
92 lines
1.9 KiB
Makefile
|
|
# find the OS
|
|
uname_S := $(shell sh -c 'uname -s 2>/dev/null || echo not')
|
|
|
|
warning_cflags = -W -Wall -Wno-missing-field-initializers
|
|
ifeq ($(uname_S),Darwin)
|
|
SHOBJ_CFLAGS ?= $(warning_cflags) -dynamic -fno-common -g -ggdb -std=gnu11 -O2
|
|
SHOBJ_LDFLAGS ?= -bundle -undefined dynamic_lookup
|
|
else # Linux, others
|
|
SHOBJ_CFLAGS ?= $(warning_cflags) -fno-common -g -ggdb -std=gnu11 -O2
|
|
SHOBJ_LDFLAGS ?= -shared
|
|
endif
|
|
|
|
ifeq ($(uname_S),Linux)
|
|
LD = gcc
|
|
CC = gcc
|
|
endif
|
|
|
|
# OS X 11.x doesn't have /usr/lib/libSystem.dylib and needs an explicit setting.
|
|
ifeq ($(uname_S),Darwin)
|
|
ifeq ("$(wildcard /usr/lib/libSystem.dylib)","")
|
|
LIBS = -L /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib -lsystem
|
|
endif
|
|
endif
|
|
|
|
TEST_MODULES = \
|
|
commandfilter.so \
|
|
commandresult.so \
|
|
basics.so \
|
|
testrdb.so \
|
|
fork.so \
|
|
infotest.so \
|
|
propagate.so \
|
|
misc.so \
|
|
hooks.so \
|
|
blockonkeys.so \
|
|
blockonbackground.so \
|
|
scan.so \
|
|
datatype.so \
|
|
datatype2.so \
|
|
auth.so \
|
|
keyspace_events.so \
|
|
block_keyspace_notification.so \
|
|
blockedclient.so \
|
|
getkeys.so \
|
|
getchannels.so \
|
|
test_lazyfree.so \
|
|
timer.so \
|
|
defragtest.so \
|
|
keyspecs.so \
|
|
hash.so \
|
|
hash_stringref.so \
|
|
zset.so \
|
|
stream.so \
|
|
mallocsize.so \
|
|
aclcheck.so \
|
|
list.so \
|
|
subcommands.so \
|
|
reply.so \
|
|
cmdintrospection.so \
|
|
eventloop.so \
|
|
moduleconfigs.so \
|
|
moduleconfigstwo.so \
|
|
moduleparameter.so \
|
|
publish.so \
|
|
usercall.so \
|
|
postnotifications.so \
|
|
moduleauthtwo.so \
|
|
rdbloadsave.so \
|
|
crash.so \
|
|
cluster.so \
|
|
helloscripting.so \
|
|
unsupported_features.so
|
|
|
|
.PHONY: all
|
|
|
|
all: $(TEST_MODULES)
|
|
|
|
32bit:
|
|
$(MAKE) CFLAGS="-m32" LDFLAGS="-m32"
|
|
|
|
%.xo: %.c ../../src/valkeymodule.h
|
|
$(CC) -I../../src $(CFLAGS) $(SHOBJ_CFLAGS) -fPIC -c $< -o $@
|
|
|
|
%.so: %.xo
|
|
$(LD) -o $@ $^ $(SHOBJ_LDFLAGS) $(LDFLAGS) $(LIBS)
|
|
|
|
.PHONY: clean
|
|
|
|
clean:
|
|
rm -f $(TEST_MODULES) $(TEST_MODULES:.so=.xo)
|