test-valkey-glide-rb 0.0.1.pre.rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +69 -0
  3. data/.rubocop_todo.yml +22 -0
  4. data/AGENTS.md +224 -0
  5. data/CLAUDE.md +130 -0
  6. data/CONTRIBUTING.md +115 -0
  7. data/DEVELOPER.md +390 -0
  8. data/README.md +268 -0
  9. data/Rakefile +99 -0
  10. data/examples/README.md +55 -0
  11. data/examples/cluster.rb +24 -0
  12. data/examples/opentelemetry.rb +50 -0
  13. data/examples/pipelining.rb +29 -0
  14. data/examples/standalone.rb +22 -0
  15. data/examples/statistics.rb +24 -0
  16. data/lib/valkey/bindings.rb +319 -0
  17. data/lib/valkey/commands/bitmap_commands.rb +86 -0
  18. data/lib/valkey/commands/cluster_commands.rb +259 -0
  19. data/lib/valkey/commands/connection_commands.rb +318 -0
  20. data/lib/valkey/commands/function_commands.rb +255 -0
  21. data/lib/valkey/commands/generic_commands.rb +525 -0
  22. data/lib/valkey/commands/geo_commands.rb +87 -0
  23. data/lib/valkey/commands/hash_commands.rb +592 -0
  24. data/lib/valkey/commands/hyper_log_log_commands.rb +51 -0
  25. data/lib/valkey/commands/json_commands.rb +389 -0
  26. data/lib/valkey/commands/list_commands.rb +348 -0
  27. data/lib/valkey/commands/module_commands.rb +125 -0
  28. data/lib/valkey/commands/pubsub_commands.rb +237 -0
  29. data/lib/valkey/commands/scripting_commands.rb +287 -0
  30. data/lib/valkey/commands/server_commands.rb +961 -0
  31. data/lib/valkey/commands/set_commands.rb +220 -0
  32. data/lib/valkey/commands/sorted_set_commands.rb +971 -0
  33. data/lib/valkey/commands/stream_commands.rb +636 -0
  34. data/lib/valkey/commands/string_commands.rb +371 -0
  35. data/lib/valkey/commands/transaction_commands.rb +175 -0
  36. data/lib/valkey/commands/vector_search_commands.rb +271 -0
  37. data/lib/valkey/commands.rb +68 -0
  38. data/lib/valkey/errors.rb +41 -0
  39. data/lib/valkey/native/aarch64-unknown-linux-gnu/libglide_ffi.so +0 -0
  40. data/lib/valkey/native/x86_64-unknown-linux-gnu/libglide_ffi.so +0 -0
  41. data/lib/valkey/opentelemetry.rb +207 -0
  42. data/lib/valkey/pipeline.rb +20 -0
  43. data/lib/valkey/pubsub_callback.rb +10 -0
  44. data/lib/valkey/request_error_type.rb +10 -0
  45. data/lib/valkey/request_type.rb +436 -0
  46. data/lib/valkey/response_type.rb +20 -0
  47. data/lib/valkey/utils.rb +267 -0
  48. data/lib/valkey/version.rb +5 -0
  49. data/lib/valkey.rb +617 -0
  50. metadata +107 -0
data/Rakefile ADDED
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Only load bundler gem tasks when not testing installed gem
4
+ # bundler/gem_tasks can interfere with load path when testing installed gems
5
+ require 'bundler/gem_tasks' unless ENV["TEST_INSTALLED_GEM"]
6
+ require 'rake/testtask'
7
+
8
+ # =============================================================================
9
+ # Native Library Build Tasks
10
+ # =============================================================================
11
+
12
+ def native_lib_ext
13
+ case RbConfig::CONFIG['host_os']
14
+ when /darwin/
15
+ 'dylib'
16
+ else
17
+ 'so'
18
+ end
19
+ end
20
+
21
+ namespace :native do
22
+ desc "Initialize the valkey-glide submodule"
23
+ task :submodule do
24
+ puts "Initializing valkey-glide submodule..."
25
+ sh "git submodule update --init --recursive"
26
+ end
27
+
28
+ desc "Build the native FFI library (release mode)"
29
+ task build: :submodule do
30
+ puts "Building native FFI library..."
31
+ Dir.chdir("valkey-glide/ffi") do
32
+ sh "cargo build --release"
33
+ end
34
+ puts "Native library built successfully!"
35
+ puts "Location: valkey-glide/ffi/target/release/libglide_ffi.#{native_lib_ext}"
36
+ end
37
+
38
+ desc "Build the native FFI library (debug mode)"
39
+ task build_debug: :submodule do
40
+ puts "Building native FFI library (debug)..."
41
+ Dir.chdir("valkey-glide/ffi") do
42
+ sh "cargo build"
43
+ end
44
+ puts "Native library built successfully!"
45
+ puts "Location: valkey-glide/ffi/target/debug/libglide_ffi.#{native_lib_ext}"
46
+ end
47
+
48
+ desc "Clean native build artifacts"
49
+ task :clean do
50
+ puts "Cleaning native build artifacts..."
51
+ if Dir.exist?("valkey-glide/ffi")
52
+ Dir.chdir("valkey-glide/ffi") do
53
+ sh "cargo clean"
54
+ end
55
+ end
56
+ puts "Clean complete!"
57
+ end
58
+
59
+ desc "Copy built library to lib/valkey for gem packaging"
60
+ task package: :build do
61
+ require 'fileutils'
62
+ src = "valkey-glide/ffi/target/release/libglide_ffi.#{native_lib_ext}"
63
+ dest = "lib/valkey/libglide_ffi.#{native_lib_ext}"
64
+
65
+ if File.exist?(src)
66
+ FileUtils.cp(src, dest)
67
+ puts "Copied #{src} to #{dest}"
68
+ else
69
+ abort "Native library not found at #{src}. Run 'rake native:build' first."
70
+ end
71
+ end
72
+ end
73
+
74
+ desc "Build the native FFI library"
75
+ task native: "native:build"
76
+
77
+ # =============================================================================
78
+ # Test Tasks
79
+ # =============================================================================
80
+
81
+ namespace :test do
82
+ groups = %i[valkey cluster]
83
+ groups.each do |group|
84
+ Rake::TestTask.new(group) do |t|
85
+ t.libs << "test"
86
+ # Only add local lib to load path when not testing installed gem
87
+ t.libs << "lib" unless ENV["TEST_INSTALLED_GEM"]
88
+ t.test_files = FileList["test/#{group}/**/*_test.rb"]
89
+ t.options = '-v' if ENV['CI'] || ENV['VERBOSE']
90
+ end
91
+ end
92
+
93
+ lost_tests = Dir["test/**/*_test.rb"] - groups.map { |g| Dir["test/#{g}/**/*_test.rb"] }.flatten
94
+ abort "The following test files are in no group:\n#{lost_tests.join("\n")}" unless lost_tests.empty?
95
+ end
96
+
97
+ task test: ["test:valkey"]
98
+
99
+ task default: :test
@@ -0,0 +1,55 @@
1
+ # Examples
2
+
3
+ Runnable examples for Valkey GLIDE Ruby. Requires a Valkey or Redis OSS server unless noted.
4
+
5
+ ## Prerequisites
6
+
7
+ From the repository root:
8
+
9
+ ```bash
10
+ bin/setup
11
+ ```
12
+
13
+ Run examples with the gem loaded from `lib/`:
14
+
15
+ ```bash
16
+ bundle exec ruby examples/standalone.rb
17
+ ```
18
+
19
+ Or:
20
+
21
+ ```bash
22
+ RUBYOPT="-I$(pwd)/lib" ruby examples/standalone.rb
23
+ ```
24
+
25
+ ## Examples
26
+
27
+ | File | Description | Server |
28
+ |------|-------------|--------|
29
+ | [standalone.rb](./standalone.rb) | Basic connect, SET, GET | Standalone `:6379` |
30
+ | [cluster.rb](./cluster.rb) | Cluster connect, SET, GET | Cluster `:7000`–`:7005` |
31
+ | [pipelining.rb](./pipelining.rb) | Non-atomic pipeline | Standalone `:6379` |
32
+ | [opentelemetry.rb](./opentelemetry.rb) | OTel file exporter + traced commands | Standalone `:6379` |
33
+ | [statistics.rb](./statistics.rb) | Client statistics | Standalone `:6379` |
34
+
35
+ ## Environment variables
36
+
37
+ | Variable | Default | Purpose |
38
+ |----------|---------|---------|
39
+ | `VALKEY_HOST` | `127.0.0.1` | Server host |
40
+ | `VALKEY_PORT` | `6379` | Standalone port |
41
+ | `VALKEY_CLUSTER_PORT` | `7000` | First cluster node port |
42
+
43
+ ## Standalone with Docker
44
+
45
+ ```bash
46
+ docker run -d --name valkey -p 6379:6379 valkey/valkey:8
47
+ bundle exec ruby examples/standalone.rb
48
+ ```
49
+
50
+ ## Cluster with Docker
51
+
52
+ ```bash
53
+ docker run -d -p 7000-7005:7000-7005 grokzen/redis-cluster:7.0.15
54
+ bundle exec ruby examples/cluster.rb
55
+ ```
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Cluster mode example.
4
+ #
5
+ # Run: bundle exec ruby examples/cluster.rb
6
+ # Requires a 6-node cluster on 127.0.0.1:7000-7005 (see examples/README.md).
7
+
8
+ require "valkey"
9
+
10
+ base_port = Integer(ENV.fetch("VALKEY_CLUSTER_PORT", 7000))
11
+ host = ENV.fetch("VALKEY_HOST", "127.0.0.1")
12
+
13
+ nodes = 6.times.map { |i| { host: host, port: base_port + i } }
14
+
15
+ client = Valkey.new(nodes: nodes, cluster_mode: true)
16
+
17
+ puts "PING: #{client.ping}"
18
+ puts "SET: #{client.set('cluster_key', 'cluster_value')}"
19
+ puts "GET: #{client.get('cluster_key')}"
20
+
21
+ client.del("cluster_key")
22
+ client.close
23
+
24
+ puts "Done."
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ # OpenTelemetry example using file exporters (no collector required).
4
+ #
5
+ # Run: bundle exec ruby examples/opentelemetry.rb
6
+ # Traces: /tmp/valkey_ruby_example_traces.json
7
+ # Metrics: /tmp/valkey_ruby_example_metrics.json
8
+
9
+ require "valkey"
10
+ require "fileutils"
11
+
12
+ TRACES_FILE = "/tmp/valkey_ruby_example_traces.json"
13
+ METRICS_FILE = "/tmp/valkey_ruby_example_metrics.json"
14
+
15
+ FileUtils.rm_f(TRACES_FILE)
16
+ FileUtils.rm_f(METRICS_FILE)
17
+
18
+ Valkey::OpenTelemetry.init(
19
+ traces: {
20
+ endpoint: "file://#{TRACES_FILE}",
21
+ sample_percentage: 100
22
+ },
23
+ metrics: {
24
+ endpoint: "file://#{METRICS_FILE}"
25
+ },
26
+ flush_interval_ms: 1000
27
+ )
28
+
29
+ host = ENV.fetch("VALKEY_HOST", "127.0.0.1")
30
+ port = Integer(ENV.fetch("VALKEY_PORT", 6379))
31
+
32
+ client = Valkey.new(host: host, port: port)
33
+
34
+ client.set("otel_key", "otel_value")
35
+ client.get("otel_key")
36
+ client.pipelined do |pipe|
37
+ pipe.set("otel_pipe_1", "a")
38
+ pipe.get("otel_pipe_1")
39
+ end
40
+
41
+ client.del("otel_key", "otel_pipe_1")
42
+ client.close
43
+
44
+ # Allow exporter flush
45
+ sleep 2
46
+
47
+ puts "OpenTelemetry initialized: #{Valkey::OpenTelemetry.initialized?}"
48
+ puts "Traces file: #{TRACES_FILE} (#{File.size?(TRACES_FILE) || 0} bytes)"
49
+ puts "Metrics file: #{METRICS_FILE} (#{File.size?(METRICS_FILE) || 0} bytes)"
50
+ puts "Done."
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Pipeline example — batch commands in one round trip.
4
+ #
5
+ # Run: bundle exec ruby examples/pipelining.rb
6
+
7
+ require "valkey"
8
+
9
+ host = ENV.fetch("VALKEY_HOST", "127.0.0.1")
10
+ port = Integer(ENV.fetch("VALKEY_PORT", 6379))
11
+
12
+ client = Valkey.new(host: host, port: port)
13
+
14
+ client.del("pipe_counter")
15
+
16
+ results = client.pipelined do |pipe|
17
+ pipe.set("pipe_key", "pipe_value")
18
+ pipe.get("pipe_key")
19
+ pipe.incr("pipe_counter")
20
+ pipe.get("pipe_counter")
21
+ end
22
+
23
+ puts "Pipeline results:"
24
+ results.each_with_index { |r, i| puts " [#{i}] #{r.inspect}" }
25
+
26
+ client.del("pipe_key", "pipe_counter")
27
+ client.close
28
+
29
+ puts "Done."
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Basic standalone Valkey example.
4
+ #
5
+ # Run: bundle exec ruby examples/standalone.rb
6
+ # Requires Valkey/Redis on VALKEY_HOST:VALKEY_PORT (default 127.0.0.1:6379).
7
+
8
+ require "valkey"
9
+
10
+ host = ENV.fetch("VALKEY_HOST", "127.0.0.1")
11
+ port = Integer(ENV.fetch("VALKEY_PORT", 6379))
12
+
13
+ client = Valkey.new(host: host, port: port)
14
+
15
+ puts "PING: #{client.ping}"
16
+ puts "SET: #{client.set('hello', 'world')}"
17
+ puts "GET: #{client.get('hello')}"
18
+
19
+ client.del("hello")
20
+ client.close
21
+
22
+ puts "Done."
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Client statistics example.
4
+ #
5
+ # Run: bundle exec ruby examples/statistics.rb
6
+
7
+ require "valkey"
8
+
9
+ host = ENV.fetch("VALKEY_HOST", "127.0.0.1")
10
+ port = Integer(ENV.fetch("VALKEY_PORT", 6379))
11
+
12
+ client = Valkey.new(host: host, port: port)
13
+ client.set("stats_demo", "1")
14
+ client.get("stats_demo")
15
+
16
+ stats = client.get_statistics
17
+
18
+ puts "Client statistics:"
19
+ stats.each { |k, v| puts " #{k}: #{v}" }
20
+
21
+ client.del("stats_demo")
22
+ client.close
23
+
24
+ puts "Done."
@@ -0,0 +1,319 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Valkey
4
+ module Bindings
5
+ extend FFI::Library
6
+
7
+ # Determine platform-specific library extension and directory name
8
+ def self.platform_info
9
+ os = if FFI::Platform.mac?
10
+ "darwin"
11
+ elsif FFI::Platform.windows?
12
+ "windows"
13
+ else
14
+ "linux"
15
+ end
16
+
17
+ # Detect architecture
18
+ arch = case RbConfig::CONFIG["host_cpu"]
19
+ when /x86_64|amd64/i
20
+ "x86_64"
21
+ when /aarch64|arm64/i
22
+ "aarch64"
23
+ when /i[3-6]86/i
24
+ "x86"
25
+ else
26
+ RbConfig::CONFIG["host_cpu"]
27
+ end
28
+
29
+ lib_ext = case os
30
+ when "darwin"
31
+ "dylib"
32
+ when "windows"
33
+ "dll"
34
+ else
35
+ "so"
36
+ end
37
+
38
+ # Platform directory name matches Rust target triple convention
39
+ platform_dir = case os
40
+ when "darwin"
41
+ "#{arch}-apple-darwin"
42
+ when "linux"
43
+ "#{arch}-unknown-linux-gnu"
44
+ when "windows"
45
+ "#{arch}-pc-windows-msvc"
46
+ end
47
+
48
+ { os: os, arch: arch, lib_ext: lib_ext, platform_dir: platform_dir }
49
+ end
50
+
51
+ platform = platform_info
52
+ lib_ext = platform[:lib_ext]
53
+ platform_dir = platform[:platform_dir]
54
+
55
+ # Look for the native library in the following locations (in order):
56
+ # 1. valkey-glide submodule build output (development/source builds)
57
+ # 2. Platform-specific bundled library in lib/valkey/native/{platform}/ (for gem distribution)
58
+ # 3. Legacy bundled library in lib/valkey (fallback for old gem structure)
59
+ lib_paths = [
60
+ # Submodule build output (release) - from lib/valkey/ go up 2 levels to repo root
61
+ File.expand_path("../../valkey-glide/ffi/target/release/libglide_ffi.#{lib_ext}", __dir__),
62
+ # Submodule build output (debug)
63
+ File.expand_path("../../valkey-glide/ffi/target/debug/libglide_ffi.#{lib_ext}", __dir__),
64
+ # Platform-specific bundled library (for gem distribution)
65
+ File.expand_path("./native/#{platform_dir}/libglide_ffi.#{lib_ext}", __dir__),
66
+ # Legacy bundled library (fallback)
67
+ File.expand_path("./libglide_ffi.#{lib_ext}", __dir__)
68
+ ]
69
+
70
+ lib_path = lib_paths.find { |path| File.exist?(path) }
71
+
72
+ unless lib_path
73
+ raise LoadError, <<~ERROR
74
+ Could not find libglide_ffi native library for platform: #{platform_dir}
75
+
76
+ Searched in:
77
+ #{lib_paths.map { |p| " - #{p}" }.join("\n")}
78
+
79
+ To build from source:
80
+ 1. Initialize the submodule: git submodule update --init --recursive
81
+ 2. Build the FFI library: cd valkey-glide/ffi && cargo build --release
82
+
83
+ Detected platform: OS=#{platform[:os]}, Arch=#{platform[:arch]}
84
+ ERROR
85
+ end
86
+
87
+ ffi_lib lib_path
88
+
89
+ class ClientType < FFI::Struct
90
+ layout(
91
+ :tag, :uint # 0 = AsyncClient, 1 = SyncClient
92
+ )
93
+ end
94
+
95
+ class ConnectionResponse < FFI::Struct
96
+ layout(
97
+ :conn_ptr, :pointer, # *const c_void
98
+ :connection_error_message, :string # *const c_char (null-terminated C string)
99
+ )
100
+ end
101
+
102
+ class CommandError < FFI::Struct
103
+ layout(
104
+ :command_error_message, :string,
105
+ :command_error_type, :int # Assuming RequestErrorType is repr(C) enum
106
+ )
107
+ end
108
+
109
+ class BatchOptionsInfo < FFI::Struct
110
+ layout(
111
+ :retry_server_error, :bool,
112
+ :retry_connection_error, :bool,
113
+ :has_timeout, :bool,
114
+ :timeout, :uint, # Assuming u32 is represented as uint in C
115
+ :route_info, :pointer # *const RouteInfo
116
+ )
117
+ end
118
+
119
+ class CmdInfo < FFI::Struct
120
+ layout(
121
+ :request_type, :int, # Assuming RequestType is repr(C) enum
122
+ :args, :pointer, # *const *const u8 (pointer to array of pointers to args)
123
+ :arg_count, :ulong, # usize (number of arguments)
124
+ :args_len, :pointer # *const usize (pointer to array of argument lengths)
125
+ )
126
+ end
127
+
128
+ class ScriptHashBuffer < FFI::Struct
129
+ layout(
130
+ :ptr, :pointer, # *mut u8 (pointer to the script hash)
131
+ :len, :ulong, # usize (length of the script hash)
132
+ :capacity, :ulong # usize (capacity of the buffer)
133
+ )
134
+ end
135
+
136
+ class BatchInfo < FFI::Struct
137
+ layout(
138
+ :cmd_count, :ulong, # usize
139
+ :cmds, :pointer, # *const *const CmdInfo
140
+ :is_atomic, :bool # bool
141
+ )
142
+ end
143
+
144
+ class CommandResponse < FFI::Struct
145
+ layout(
146
+ :response_type, :int, # Assuming ResponseType is repr(C) enum
147
+ :int_value, :int64,
148
+ :float_value, :double,
149
+ :bool_value, :bool,
150
+ :string_value, :pointer, # points to C string
151
+ :string_value_len, :long,
152
+ :array_value, :pointer, # points to CommandResponse array
153
+ :array_value_len, :long,
154
+ :map_key, :pointer, # CommandResponse*
155
+ :map_value, :pointer, # CommandResponse*
156
+ :sets_value, :pointer, # CommandResponse*
157
+ :sets_value_len, :long,
158
+ :arena_ptr, :pointer # *mut c_void - arena allocator pointer
159
+ )
160
+ end
161
+
162
+ callback :success_callback, %i[ulong pointer], :void
163
+ callback :failure_callback, %i[ulong string int], :void
164
+
165
+ class AsyncClientData < FFI::Struct
166
+ layout(
167
+ :success_callback, :success_callback,
168
+ :failure_callback, :failure_callback
169
+ )
170
+ end
171
+
172
+ class ClientData < FFI::Union
173
+ layout(
174
+ :async_client, AsyncClientData
175
+ )
176
+ end
177
+
178
+ class CommandResult < FFI::Struct
179
+ layout(
180
+ :response, CommandResponse.by_ref,
181
+ :command_error, CommandError.by_ref,
182
+ :arena, :pointer # *mut ResponseArena
183
+ )
184
+ end
185
+
186
+ callback :pubsub_callback, [
187
+ :ulong, # client_ptr
188
+ :int, # kind (PushKind enum)
189
+ :pointer, :long, # message + length
190
+ :pointer, :long, # channel + length
191
+ :pointer, :long # pattern + length
192
+ ], :void
193
+
194
+ attach_function :create_client, [
195
+ :pointer, # *const u8 (connection_request_bytes)
196
+ :ulong, # usize (connection_request_len)
197
+ ClientType.by_ref, # *const ClientType
198
+ :pubsub_callback # callback
199
+ ], :pointer # *const ConnectionResponse
200
+
201
+ attach_function :create_client_from_uri, [
202
+ :string, # *const c_char (uri_str)
203
+ :string, # *const c_char (extra_options_json)
204
+ ClientType.by_ref, # *const ClientType
205
+ :pubsub_callback # callback
206
+ ], :pointer # *const ConnectionResponse
207
+
208
+ attach_function :free_connection_response, [
209
+ :pointer # *mut ConnectionResponse
210
+ ], :void
211
+
212
+ attach_function :close_client, [
213
+ :pointer # client_adapter_ptr
214
+ ], :void
215
+
216
+ attach_function :command, [
217
+ :pointer, # client_adapter_ptr
218
+ :ulong, # request_id
219
+ :int, # command_type
220
+ :ulong, # arg_count
221
+ :pointer, # args (pointer to usize[])
222
+ :pointer, # args_len (pointer to c_ulong[])
223
+ :pointer, # route_bytes
224
+ :ulong, # route_bytes_len
225
+ :ulong # span_ptr (u64)
226
+ ], :pointer, blocking: true # returns *mut CommandResult, releases GVL during I/O
227
+
228
+ attach_function :batch, [
229
+ :pointer, # client_ptr
230
+ :ulong, # callback_index
231
+ BatchInfo.by_ref, # *const BatchInfo
232
+ :bool, # raise_on_error
233
+ :pointer, # *const BatchOptionsInfo
234
+ :ulong # span_ptr (u64)
235
+ ], :pointer, blocking: true # returns *mut CommandResult, releases GVL during I/O
236
+
237
+ attach_function :store_script, [
238
+ :pointer, # *const u8 (script_bytes)
239
+ :ulong # usize (script_len)
240
+ ], :pointer # returns *mut ScriptHashBuffer
241
+
242
+ attach_function :invoke_script, [
243
+ :pointer, # client_ptr
244
+ :ulong, # request_id
245
+ :pointer, # hash (pointer to C string)
246
+ :ulong, # keys_count (number of keys)
247
+ :pointer, # keys (pointer to usize[])
248
+ :pointer, # keys_len (pointer to c_ulong[])
249
+ :ulong, # args_count (number of args)
250
+ :pointer, # args (pointer to usize[])
251
+ :pointer, # args_len (pointer to c_ulong[])
252
+ :pointer, # route_bytes (pointer to u8)
253
+ :ulong, # route_bytes_len (usize)
254
+ :uint64 # span_ptr (OpenTelemetry span pointer)
255
+ ], :pointer, blocking: true # returns *mut CommandResult, releases GVL during I/O
256
+
257
+ # OpenTelemetry structures
258
+ class OpenTelemetryTracesConfig < FFI::Struct
259
+ layout(
260
+ :endpoint, :pointer, # const char* (trace collector endpoint)
261
+ :has_sample_percentage, :bool, # whether sample_percentage is set
262
+ :sample_percentage, :uint32 # sampling percentage (0-100)
263
+ )
264
+ end
265
+
266
+ class OpenTelemetryMetricsConfig < FFI::Struct
267
+ layout(
268
+ :endpoint, :pointer # const char* (metrics collector endpoint)
269
+ )
270
+ end
271
+
272
+ class OpenTelemetryConfig < FFI::Struct
273
+ layout(
274
+ :traces, :pointer, # OpenTelemetryTracesConfig*
275
+ :metrics, :pointer, # OpenTelemetryMetricsConfig*
276
+ :has_flush_interval_ms, :bool, # whether flush_interval_ms is set
277
+ :flush_interval_ms, :int64 # flush interval in milliseconds
278
+ )
279
+ end
280
+
281
+ # Statistics structure
282
+ class Statistics < FFI::Struct
283
+ layout(
284
+ :total_connections, :ulong,
285
+ :total_clients, :ulong,
286
+ :total_values_compressed, :ulong,
287
+ :total_values_decompressed, :ulong,
288
+ :total_original_bytes, :ulong,
289
+ :total_bytes_compressed, :ulong,
290
+ :total_bytes_decompressed, :ulong,
291
+ :compression_skipped_count, :ulong,
292
+ :subscription_out_of_sync_count, :ulong,
293
+ :subscription_last_sync_timestamp, :ulong
294
+ )
295
+ end
296
+
297
+ # OpenTelemetry functions
298
+ attach_function :init_open_telemetry, [
299
+ OpenTelemetryConfig.by_ref # OpenTelemetry configuration
300
+ ], :pointer # returns error string or NULL on success
301
+
302
+ attach_function :free_c_string, [
303
+ :pointer # C string to free
304
+ ], :void
305
+
306
+ attach_function :create_otel_span, [
307
+ :int # request_type (RequestType enum value)
308
+ ], :uint64 # returns span pointer (u64) or 0 on failure
309
+
310
+ attach_function :create_batch_otel_span, [], :uint64 # returns span pointer (u64) or 0 on failure
311
+
312
+ attach_function :drop_otel_span, [
313
+ :uint64 # span_ptr to close
314
+ ], :void
315
+
316
+ # Statistics function
317
+ attach_function :get_statistics, [], Statistics.by_value # returns statistics by value
318
+ end
319
+ end