Describe the feature
glide-core ships a logger and exports it over the FFI as init, glide_log, and free_log_result. The Ruby client does not bind any of it. Every peer client does: python-sync has Logger in python/glide-shared/glide_shared/logger.py, and Node, Java, and Go each expose their own equivalent.
Ruby should get the same capability: a public module that configures the core logger's level and destination, and that lets an application emit its own messages into that logger.
Concretely, the pieces that are missing are a way to set the log level, a way to send logs to an hourly-rotated file instead of the console, a way to ask whether a level would be logged before building an expensive message, and a way to emit a message with an optional exception attached.
Use Case
Today the only way to see anything the GLIDE core logs from Ruby is to set RUST_LOG in the environment before the process starts. That has two problems.
The level cannot be changed from Ruby, so an application cannot raise the driver to :debug while reproducing a connection problem and drop it back afterwards. There is also no way to write the driver's logs to a file, which is what you want when the console is already carrying application output.
More importantly, an application has nowhere to put its own Valkey-related messages so that they interleave with the driver's. When a command times out, the driver's internal log line and the application's "retrying key foo" line end up in different places, in different formats, filtered by different rules. Reconstructing the order of events afterwards means correlating two logs by timestamp. Sharing one destination, one format, and one level filter removes that step.
The client itself needs this too. PubSubCallback#pubsub_callback in lib/valkey/pubsub_callback.rb reports incoming push messages with a bare puts to stdout, which is tolerable only because Pub/Sub is unfinished (#135) and its public API is disabled. Finishing Pub/Sub means that handler has to swallow exceptions raised by user callbacks, and there is currently nowhere to report them. Any driver code path that needs to report a non-fatal problem hits the same wall.
Proposed Solution
Add Valkey::Logger, a module wrapping the three FFI functions.
Valkey::Logger.init(level: nil, file_name: nil) # configures only if unconfigured
Valkey::Logger.set_logger_config(level: nil, file_name: nil) # always replaces
Valkey::Logger.log(level, identifier, message, error: nil)
Valkey::Logger.enabled?(level) # => Boolean
Valkey::Logger.logger_level # => Symbol
Valkey::Logger.configured? # => Boolean
Valkey::Logger::LEVELS # { error: 0, warn: 1, info: 2, debug: 3, trace: 4, off: 5 }
Valkey::Logger::DEFAULT_LEVEL # :warn
Plus Valkey::LoggerError < Valkey::BaseError, raised when the native logger cannot be configured.
Valkey::Logger.init(level: :info)
Valkey::Logger.log(:info, "MyApp", "connected to Valkey")
Valkey::Logger.init(level: :debug, file_name: "my_app.log") # hourly-rotated files under ./glide-logs
Valkey::Logger.log(:error, "MyApp", "write failed", error: caught_exception)
Valkey::Logger.set_logger_config(level: :off) # silence everything
The shape follows python-sync's Logger, including the split between init and set_logger_config and the handling of LogResult.log_error, adapted to Ruby idiom: level symbols rather than enum constants, keyword arguments, and an error: keyword that appends the exception and its backtrace.
Placement should be top-level, next to Valkey::OpenTelemetry. That is the repo's precedent for a user-facing observability module backed by native code.
Four details are worth deciding up front, because they are easy to get wrong:
Filtering belongs in Ruby. glide_log's own doc comment in ffi/src/lib.rs says the caller is responsible for filtering, so log should compare against the level the native logger reported and return before crossing the FFI boundary.
:off needs a special case. It is 5, numerically above every real level, so the plain level > logger_level comparison that python-sync uses lets everything through when the level is OFF and leans on the Rust side to drop it. An early return on :off makes the documented behavior hold in Ruby too.
logger_init and glide_log must be attached with blocking: true. Both write to stdout and to glide-core's rolling file appender. Ruby-FFI defaults to non-blocking, which holds the GVL for the whole native call, so a stalled pipe or a slow disk would freeze every thread in the VM.
A failed log must not raise. A message the native layer rejects, one that is not valid UTF-8 for instance, should be reported on stderr. Breaking the code path that emitted a log line is worse than losing the line. Configuration failures in init and set_logger_config should still raise Valkey::LoggerError.
Every LogResult needs freeing exactly once, including on raise paths, and a null pointer must never reach free_log_result.
Other Information
Out of scope for a first pass, in my view:
The driver's own connection and command paths do not have to emit through the new module straight away. Exposing the capability and instrumenting the driver are separable, and the second is larger.
Logger.error / .warn / .info shorthands are not worth adding. Kernel#warn is what the module itself would use to report a failed log, so shadowing it on the singleton is a trap, and no peer client offers them.
A block form for lazy message construction is unnecessary once enabled? exists.
One testing constraint to flag: the native logger is process-wide and reads GLIDE_LOG_DIR only at first initialization. A test that writes log files inside a shared suite process can leave a glide-logs/ directory in the repository, so file output is awkward to cover in the integration suites. Server-free unit tests are the right home, since logging never touches a server.
References:
Acknowledgements
Purely additive. No existing constant, method, or behavior changes.
Client version used
valkey-glide-rb 1.0.0
Environment details (OS name and version, Ruby version, etc.)
macOS 26.6.2 (arm64, Apple silicon), Ruby 3.4.9 (arm64-darwin25), valkey-glide-rb 1.0.0 built against glide-ffi from the pinned valkey-glide submodule. The gap is not platform-specific; no Ruby client on any platform binds the core logger.
Describe the feature
glide-core ships a logger and exports it over the FFI as
init,glide_log, andfree_log_result. The Ruby client does not bind any of it. Every peer client does: python-sync hasLoggerinpython/glide-shared/glide_shared/logger.py, and Node, Java, and Go each expose their own equivalent.Ruby should get the same capability: a public module that configures the core logger's level and destination, and that lets an application emit its own messages into that logger.
Concretely, the pieces that are missing are a way to set the log level, a way to send logs to an hourly-rotated file instead of the console, a way to ask whether a level would be logged before building an expensive message, and a way to emit a message with an optional exception attached.
Use Case
Today the only way to see anything the GLIDE core logs from Ruby is to set
RUST_LOGin the environment before the process starts. That has two problems.The level cannot be changed from Ruby, so an application cannot raise the driver to
:debugwhile reproducing a connection problem and drop it back afterwards. There is also no way to write the driver's logs to a file, which is what you want when the console is already carrying application output.More importantly, an application has nowhere to put its own Valkey-related messages so that they interleave with the driver's. When a command times out, the driver's internal log line and the application's "retrying key foo" line end up in different places, in different formats, filtered by different rules. Reconstructing the order of events afterwards means correlating two logs by timestamp. Sharing one destination, one format, and one level filter removes that step.
The client itself needs this too.
PubSubCallback#pubsub_callbackinlib/valkey/pubsub_callback.rbreports incoming push messages with a bareputsto stdout, which is tolerable only because Pub/Sub is unfinished (#135) and its public API is disabled. Finishing Pub/Sub means that handler has to swallow exceptions raised by user callbacks, and there is currently nowhere to report them. Any driver code path that needs to report a non-fatal problem hits the same wall.Proposed Solution
Add
Valkey::Logger, a module wrapping the three FFI functions.Plus
Valkey::LoggerError < Valkey::BaseError, raised when the native logger cannot be configured.The shape follows python-sync's
Logger, including the split betweeninitandset_logger_configand the handling ofLogResult.log_error, adapted to Ruby idiom: level symbols rather than enum constants, keyword arguments, and anerror:keyword that appends the exception and its backtrace.Placement should be top-level, next to
Valkey::OpenTelemetry. That is the repo's precedent for a user-facing observability module backed by native code.Four details are worth deciding up front, because they are easy to get wrong:
Filtering belongs in Ruby.
glide_log's own doc comment inffi/src/lib.rssays the caller is responsible for filtering, sologshould compare against the level the native logger reported and return before crossing the FFI boundary.:offneeds a special case. It is5, numerically above every real level, so the plainlevel > logger_levelcomparison that python-sync uses lets everything through when the level is OFF and leans on the Rust side to drop it. An early return on:offmakes the documented behavior hold in Ruby too.logger_initandglide_logmust be attached withblocking: true. Both write to stdout and to glide-core's rolling file appender. Ruby-FFI defaults to non-blocking, which holds the GVL for the whole native call, so a stalled pipe or a slow disk would freeze every thread in the VM.A failed
logmust not raise. A message the native layer rejects, one that is not valid UTF-8 for instance, should be reported on stderr. Breaking the code path that emitted a log line is worse than losing the line. Configuration failures ininitandset_logger_configshould still raiseValkey::LoggerError.Every
LogResultneeds freeing exactly once, including on raise paths, and a null pointer must never reachfree_log_result.Other Information
Out of scope for a first pass, in my view:
The driver's own connection and command paths do not have to emit through the new module straight away. Exposing the capability and instrumenting the driver are separable, and the second is larger.
Logger.error/.warn/.infoshorthands are not worth adding.Kernel#warnis what the module itself would use to report a failed log, so shadowing it on the singleton is a trap, and no peer client offers them.A block form for lazy message construction is unnecessary once
enabled?exists.One testing constraint to flag: the native logger is process-wide and reads
GLIDE_LOG_DIRonly at first initialization. A test that writes log files inside a shared suite process can leave aglide-logs/directory in the repository, so file output is awkward to cover in the integration suites. Server-free unit tests are the right home, since logging never touches a server.References:
python/glide-shared/glide_shared/logger.pyffi/src/lib.rs(glide_log,free_log_result, theLevelenum, theLogResultstruct)logger_core/src/lib.rsAcknowledgements
Purely additive. No existing constant, method, or behavior changes.
Client version used
valkey-glide-rb 1.0.0
Environment details (OS name and version, Ruby version, etc.)
macOS 26.6.2 (arm64, Apple silicon), Ruby 3.4.9 (arm64-darwin25), valkey-glide-rb 1.0.0 built against glide-ffi from the pinned
valkey-glidesubmodule. The gap is not platform-specific; no Ruby client on any platform binds the core logger.