diff --git a/config.m4 b/config.m4
index cda06f43..10a86bfd 100644
--- a/config.m4
+++ b/config.m4
@@ -86,7 +86,7 @@ if test "$PHP_VALKEY_GLIDE" != "no"; then
esac
PHP_NEW_EXTENSION(valkey_glide,
- valkey_glide.c valkey_glide_cluster.c cluster_scan_cursor.c command_response.c logger.c valkey_glide_commands.c valkey_glide_commands_2.c valkey_glide_commands_3.c valkey_glide_core_commands.c valkey_glide_core_common.c valkey_glide_expire_commands.c valkey_glide_geo_commands.c valkey_glide_geo_common.c valkey_glide_hash_common.c valkey_glide_list_common.c valkey_glide_s_common.c valkey_glide_str_commands.c valkey_glide_x_commands.c valkey_glide_x_common.c valkey_glide_z.c valkey_glide_z_common.c valkey_z_php_methods.c src/command_request.pb-c.c src/connection_request.pb-c.c src/response.pb-c.c src/client_constructor_mock.c,
+ valkey_glide.c valkey_glide_cluster.c cluster_scan_cursor.c command_response.c logger.c valkey_glide_commands.c valkey_glide_commands_2.c valkey_glide_commands_3.c valkey_glide_core_commands.c valkey_glide_core_common.c valkey_glide_expire_commands.c valkey_glide_geo_commands.c valkey_glide_geo_common.c valkey_glide_hash_common.c valkey_glide_list_common.c valkey_glide_s_common.c valkey_glide_str_commands.c valkey_glide_x_commands.c valkey_glide_x_common.c valkey_glide_z.c valkey_glide_z_common.c valkey_z_php_methods.c valkey_glide_otel.c valkey_glide_otel_commands.c src/command_request.pb-c.c src/connection_request.pb-c.c src/response.pb-c.c src/client_constructor_mock.c,
$ext_shared,, $VALKEY_GLIDE_SHARED_LIBADD)
dnl Add FFI library only for macOS (keep Mac working as before)
diff --git a/examples/otel_example.php b/examples/otel_example.php
new file mode 100644
index 00000000..63818aed
--- /dev/null
+++ b/examples/otel_example.php
@@ -0,0 +1,76 @@
+ [
+ 'endpoint' => 'grpc://localhost:4317', // OTEL collector endpoint
+ 'sample_percentage' => 10 // Sample 10% of requests (default is 1%)
+ ],
+ 'metrics' => [
+ 'endpoint' => 'grpc://localhost:4317' // OTEL collector endpoint
+ ],
+ 'flush_interval_ms' => 5000 // Flush every 5 seconds (default)
+ ];
+
+ // Create ValkeyGlide client with OTEL configuration
+ $client = new ValkeyGlide(
+ addresses: [
+ ['host' => 'localhost', 'port' => 6379]
+ ],
+ use_tls: false,
+ credentials: null,
+ read_from: ValkeyGlide::READ_FROM_PRIMARY,
+ request_timeout: null,
+ reconnect_strategy: null,
+ database_id: 0,
+ client_name: 'otel-example-client',
+ client_az: null,
+ advanced_config: [
+ 'connection_timeout' => 5000,
+ 'otel' => $otelConfig // Add OTEL configuration
+ ]
+ );
+
+ echo "ValkeyGlide client created with OpenTelemetry support" . PHP_EOL;
+ echo "- Sample percentage: 10% (higher than default 1% for demo)" . PHP_EOL;
+ echo "- Flush interval: 5000ms (default)" . PHP_EOL;
+ echo "- Traces endpoint: grpc://localhost:4317" . PHP_EOL;
+ echo "- Metrics endpoint: grpc://localhost:4317" . PHP_EOL . PHP_EOL;
+
+ // Perform some operations that will be traced
+ $client->set('otel:test:key1', 'value1');
+ echo "SET operation completed" . PHP_EOL;
+
+ $value = $client->get('otel:test:key1');
+ echo "GET operation completed: $value" . PHP_EOL;
+
+ $client->set('otel:test:key2', 'value2');
+ $client->set('otel:test:key3', 'value3');
+
+ // Batch operations will also be traced
+ $results = $client->mget(['otel:test:key1', 'otel:test:key2', 'otel:test:key3']);
+ echo "MGET operation completed: " . json_encode($results) . PHP_EOL;
+
+ // Cleanup
+ $client->del(['otel:test:key1', 'otel:test:key2', 'otel:test:key3']);
+ echo "Cleanup completed" . PHP_EOL;
+
+ $client->close();
+ echo "Client closed" . PHP_EOL;
+
+} catch (Exception $e) {
+ echo "Error: " . $e->getMessage() . PHP_EOL;
+ exit(1);
+}
+
+echo PHP_EOL . "OpenTelemetry example completed successfully!" . PHP_EOL;
+echo "Check your OTEL collector for traces and metrics." . PHP_EOL;
+echo PHP_EOL . "Note: OTEL can only be initialized once per process." . PHP_EOL;
+echo "If you need to change configuration, restart the process." . PHP_EOL;
+?>
diff --git a/package.xml b/package.xml
index bf796ad6..6e2597de 100644
--- a/package.xml
+++ b/package.xml
@@ -85,6 +85,10 @@ Requirements:
+
+
+
+
diff --git a/tests/ValkeyGlideClusterFeaturesTest.php b/tests/ValkeyGlideClusterFeaturesTest.php
index 6ee2e6ce..a28cb9e2 100644
--- a/tests/ValkeyGlideClusterFeaturesTest.php
+++ b/tests/ValkeyGlideClusterFeaturesTest.php
@@ -755,4 +755,48 @@ public function testClusterClientCreateDeleteLoop()
echo "WARNING: Significant memory growth detected: " . round($memoryGrowth / 1024 / 1024, 2) . " MB\n";
}
}
+
+ public function testOtelClusterConfiguration()
+ {
+ // Test that cluster client works with OpenTelemetry configuration
+ $otelConfig = [
+ 'traces' => [
+ 'endpoint' => 'file:///tmp/valkey-cluster-traces.json',
+ 'sample_percentage' => 10
+ ],
+ 'metrics' => [
+ 'endpoint' => 'file:///tmp/valkey-cluster-metrics.json'
+ ]
+ ];
+
+ // Create cluster client with OpenTelemetry configuration
+ $client = new ValkeyGlideCluster(
+ addresses: [
+ ['host' => 'localhost', 'port' => 7001],
+ ['host' => 'localhost', 'port' => 7002],
+ ['host' => 'localhost', 'port' => 7003]
+ ],
+ use_tls: false,
+ credentials: null,
+ read_from: null,
+ request_timeout: null,
+ reconnect_strategy: null,
+ client_name: 'otel-cluster-test',
+ periodic_checks: null,
+ client_az: null,
+ advanced_config: [
+ 'otel' => $otelConfig
+ ]
+ );
+
+ // Verify cluster client works with OpenTelemetry configured
+ $client->set('otel:cluster:test', 'value');
+ $value = $client->get('otel:cluster:test');
+ $this->assertEquals('value', $value, "GET should return the set value with OpenTelemetry");
+
+ $deleteResult = $client->del('otel:cluster:test');
+ $this->assertGreaterThan(0, $deleteResult, "DEL should delete the key with OpenTelemetry");
+
+ $client->close();
+ }
}
diff --git a/tests/ValkeyGlideFeaturesTest.php b/tests/ValkeyGlideFeaturesTest.php
index 7a6142bd..50adbcb4 100644
--- a/tests/ValkeyGlideFeaturesTest.php
+++ b/tests/ValkeyGlideFeaturesTest.php
@@ -895,4 +895,80 @@ public function testClientCreateDeleteLoop()
echo "WARNING: Significant memory growth detected: " . round($memoryGrowth / 1024 / 1024, 2) . " MB\n";
}
}
+
+ public function testOtelConfiguration()
+ {
+ // Test that client works with OpenTelemetry configuration
+ $otelConfig = [
+ 'traces' => [
+ 'endpoint' => 'file:///tmp/valkey_glide_traces.json',
+ 'sample_percentage' => 10
+ ],
+ 'metrics' => [
+ 'endpoint' => 'file:///tmp/valkey_glide_metrics.json'
+ ]
+ ];
+
+ try {
+ // Create client with OpenTelemetry configuration
+ $client = new ValkeyGlide(
+ addresses: [
+ ['host' => 'localhost', 'port' => 6379]
+ ],
+ use_tls: false,
+ credentials: null,
+ read_from: null,
+ request_timeout: null,
+ reconnect_strategy: null,
+ client_name: 'otel-test-client',
+ periodic_checks: null,
+ advanced_config: [
+ 'otel' => $otelConfig
+ ]
+ );
+
+ // Verify client works with OpenTelemetry configured
+ $client->set('otel:test', 'value');
+ $value = $client->get('otel:test');
+ $this->assertEquals('value', $value, "GET should return the set value with OpenTelemetry");
+
+ $deleteResult = $client->del('otel:test');
+ $this->assertGreaterThan(0, $deleteResult, "DEL should delete the key with OpenTelemetry");
+
+ $client->close();
+ } catch (Exception $e) {
+ // OpenTelemetry configuration should not cause client construction to fail
+ $this->fail("OpenTelemetry configuration caused client construction to fail: " . $e->getMessage());
+ }
+ }
+
+ public function testOtelWithoutConfiguration()
+ {
+ // Test that client works normally without OpenTelemetry configuration
+ try {
+ $client = new ValkeyGlide(
+ addresses: [
+ ['host' => 'localhost', 'port' => 6379]
+ ],
+ use_tls: false,
+ credentials: null,
+ read_from: null,
+ request_timeout: null,
+ reconnect_strategy: null,
+ client_name: 'no-otel-test'
+ );
+
+ // Operations should work normally without OpenTelemetry
+ $client->set('no:otel:test', 'value');
+ $value = $client->get('no:otel:test');
+ $this->assertEquals('value', $value, "GET should return the set value");
+
+ $deleteResult = $client->del('no:otel:test');
+ $this->assertGreaterThan(0, $deleteResult, "DEL should delete the key");
+
+ $client->close();
+ } catch (Exception $e) {
+ $this->fail("Client without OpenTelemetry should work normally: " . $e->getMessage());
+ }
+ }
}
diff --git a/valgrind.supp b/valgrind.supp
index 4c335e86..3403a8e1 100644
--- a/valgrind.supp
+++ b/valgrind.supp
@@ -326,6 +326,30 @@
fun:*aws_lc_rs*rand*fill*
}
+# OpenTelemetry SDK resource detector allocations
+{
+ opentelemetry_resource_from_detectors
+ Memcheck:Leak
+ match-leak-kinds: possible
+ fun:malloc
+ fun:*hashbrown*raw*RawTableInner*fallible_with_capacity*
+ fun:*hashbrown*raw*RawTable*reserve_rehash*
+ fun:*hashbrown*map*HashMap*insert*
+ fun:*opentelemetry_sdk*resource*Resource*from_detectors*
+}
+
+# Thread-local storage for OpenTelemetry threads
+{
+ opentelemetry_thread_local_storage
+ Memcheck:Leak
+ match-leak-kinds: possible
+ fun:calloc
+ fun:calloc
+ fun:allocate_dtv
+ fun:_dl_allocate_tls
+ fun:allocate_stack
+}
+
# Rust raw_vec finish_grow with corrupted stack
{
rust_raw_vec_finish_grow_corrupted
diff --git a/valkey_glide.c b/valkey_glide.c
index 57becb23..0016c81f 100644
--- a/valkey_glide.c
+++ b/valkey_glide.c
@@ -14,6 +14,7 @@
#include "valkey_glide_cluster_arginfo.h" // Include generated arginfo header
#include "valkey_glide_commands_common.h"
#include "valkey_glide_hash_common.h"
+#include "valkey_glide_otel.h" // Include OTEL support
/* Enum support includes - must be BEFORE arginfo includes */
#if PHP_VERSION_ID >= 80100
@@ -389,6 +390,16 @@ void valkey_glide_build_client_config_base(valkey_glide_php_common_constructor_p
} else {
config->advanced_config->tls_config = NULL;
}
+
+ /* Check for OTEL config */
+ zval* otel_config_val = zend_hash_str_find(advanced_ht, "otel", sizeof("otel") - 1);
+ if (otel_config_val && Z_TYPE_P(otel_config_val) == IS_ARRAY) {
+ VALKEY_LOG_DEBUG("otel_config", "Processing OTEL configuration from advanced_config");
+ if (!valkey_glide_otel_init(otel_config_val)) {
+ VALKEY_LOG_WARN("otel_config",
+ "Failed to initialize OTEL, continuing without tracing");
+ }
+ }
} else {
config->advanced_config = NULL;
}
@@ -453,7 +464,6 @@ PHP_MINIT_FUNCTION(valkey_glide) {
return SUCCESS;
}
-
zend_module_entry valkey_glide_module_entry = {STANDARD_MODULE_HEADER,
"valkey_glide",
ext_functions,
diff --git a/valkey_glide.stub.php b/valkey_glide.stub.php
index c2a294c1..42c4290f 100644
--- a/valkey_glide.stub.php
+++ b/valkey_glide.stub.php
@@ -316,7 +316,10 @@ class ValkeyGlide
* @param string|null $client_name Client name identifier.
* @param string|null $client_az Client availability zone.
* @param array|null $advanced_config Advanced configuration ['connection_timeout' => 5000,
- * 'tls_config' => ['use_insecure_tls' => false]].
+ * 'tls_config' => ['use_insecure_tls' => false],
+ * 'otel' => ['traces' => ['endpoint' => 'grpc://localhost:4317', 'sample_percentage' => 1],
+ * 'metrics' => ['endpoint' => 'grpc://localhost:4317'],
+ * 'flush_interval_ms' => 5000]].
* connection_timeout is in milliseconds.
* @param bool|null $lazy_connect Whether to use lazy connection.
*/
diff --git a/valkey_glide_cluster.stub.php b/valkey_glide_cluster.stub.php
index c59787ad..9812bf7f 100644
--- a/valkey_glide_cluster.stub.php
+++ b/valkey_glide_cluster.stub.php
@@ -197,6 +197,9 @@ class ValkeyGlideCluster
* - 'tls_config' => ['use_insecure_tls' => false]
* - 'refresh_topology_from_initial_nodes' => false (default: false)
* When true, topology updates use only initial nodes instead of internal cluster view.
+ * - 'otel' => ['traces' => ['endpoint' => 'grpc://localhost:4317', 'sample_percentage' => 1],
+ * 'metrics' => ['endpoint' => 'grpc://localhost:4317'],
+ * 'flush_interval_ms' => 5000]
* @param bool|null $lazy_connect Whether to use lazy connection.
* @param int|null $database_id Index of the logical database to connect to. Must be non-negative
* and within the range supported by the server configuration.
diff --git a/valkey_glide_core_common.c b/valkey_glide_core_common.c
index 3b8bc211..8e4ea26f 100644
--- a/valkey_glide_core_common.c
+++ b/valkey_glide_core_common.c
@@ -21,6 +21,7 @@
#include
#include "logger.h"
+#include "valkey_glide_otel.h"
#include "valkey_glide_z_common.h"
/* ====================================================================
@@ -50,11 +51,15 @@ int execute_core_command(valkey_glide_object* valkey_glide,
return 0;
}
+ /* Create OTEL span for tracing */
+ uint64_t span_ptr = valkey_glide_create_span(args->cmd_type);
+
/* Log command execution entry */
VALKEY_LOG_DEBUG_FMT("command_execution",
- "Entering command execution - Command type: %d, Batch mode: %s",
+ "Entering command execution - Command type: %d, Batch mode: %s, Span: %lu",
args->cmd_type,
- valkey_glide->is_in_batch_mode ? "yes" : "no");
+ valkey_glide->is_in_batch_mode ? "yes" : "no",
+ (unsigned long) span_ptr);
uintptr_t* cmd_args = NULL;
unsigned long* cmd_args_len = NULL;
@@ -73,6 +78,7 @@ int execute_core_command(valkey_glide_object* valkey_glide,
if (arg_count < 0) {
VALKEY_LOG_ERROR("execute_core_command", "Failed to prepare command arguments");
+ valkey_glide_drop_span(span_ptr);
efree(result_ptr);
return 0;
}
@@ -94,6 +100,7 @@ int execute_core_command(valkey_glide_object* valkey_glide,
processor);
free_core_args(cmd_args, cmd_args_len, allocated_strings, allocated_count);
+ valkey_glide_drop_span(span_ptr);
if (res == 0) {
VALKEY_LOG_WARN_FMT("batch_execution",
"Failed to buffer command for batch - command type: %d",
@@ -110,16 +117,17 @@ int execute_core_command(valkey_glide_object* valkey_glide,
if (args->has_route && args->route_param) {
/* Cluster mode with routing */
VALKEY_LOG_DEBUG("command_execution", "Using cluster routing");
- result = execute_command_with_route(args->glide_client,
- args->cmd_type,
- arg_count,
- cmd_args,
- cmd_args_len,
- args->route_param);
+ result = execute_command_with_route_and_span(args->glide_client,
+ args->cmd_type,
+ arg_count,
+ cmd_args,
+ cmd_args_len,
+ args->route_param,
+ span_ptr);
} else {
/* Non-cluster mode or no routing */
- result =
- execute_command(args->glide_client, args->cmd_type, arg_count, cmd_args, cmd_args_len);
+ result = execute_command_with_span(
+ args->glide_client, args->cmd_type, arg_count, cmd_args, cmd_args_len, span_ptr);
}
debug_print_command_result(result);
@@ -146,6 +154,7 @@ int execute_core_command(valkey_glide_object* valkey_glide,
/* Cleanup */
free_core_args(cmd_args, cmd_args_len, allocated_strings, allocated_count);
+ valkey_glide_drop_span(span_ptr);
return res;
}
diff --git a/valkey_glide_core_common.h b/valkey_glide_core_common.h
index f45db8cd..e26b65a7 100644
--- a/valkey_glide_core_common.h
+++ b/valkey_glide_core_common.h
@@ -19,6 +19,7 @@
#include "command_response.h"
#include "valkey_glide_commands_common.h"
+#include "valkey_glide_otel_commands.h"
/* ====================================================================
* CORE COMMAND ARGUMENT STRUCTURES
diff --git a/valkey_glide_otel.c b/valkey_glide_otel.c
new file mode 100644
index 00000000..217ddf7a
--- /dev/null
+++ b/valkey_glide_otel.c
@@ -0,0 +1,286 @@
+/*
+ +----------------------------------------------------------------------+
+ +----------------------------------------------------------------------+
+ | Copyright (c) 2023-2025 The PHP Group |
+ +----------------------------------------------------------------------+
+ | This source file is subject to version 3.01 of the PHP license, |
+ | that is bundled with this package in the file LICENSE, and is |
+ | available through the world-wide-web at the following url: |
+ | http://www.php.net/license/3_01.txt |
+ | If you did not receive a copy of the PHP license and are unable to |
+ | obtain it through the world-wide-web, please send a note to |
+ | license@php.net so we can mail you a copy immediately. |
+ +----------------------------------------------------------------------+
+*/
+
+#include "valkey_glide_otel.h"
+
+#include
+#include
+
+#include "logger.h"
+
+/* Global OTEL configuration */
+valkey_glide_otel_config_t g_otel_config = {0};
+static bool g_otel_initialized = false;
+
+/**
+ * Initialize OpenTelemetry with the given configuration
+ * Can only be initialized once per process
+ */
+int valkey_glide_otel_init(zval* config_array) {
+ if (g_otel_initialized) {
+ VALKEY_LOG_WARN("otel_init",
+ "OpenTelemetry already initialized, ignoring subsequent calls");
+ return 1; /* Success - already initialized */
+ }
+
+ if (!config_array || Z_TYPE_P(config_array) != IS_ARRAY) {
+ VALKEY_LOG_DEBUG("otel_init", "No OTEL configuration provided");
+ return 1; /* Success - OTEL is optional */
+ }
+
+ /* Parse configuration */
+ if (!parse_otel_config_array(config_array, &g_otel_config)) {
+ VALKEY_LOG_ERROR("otel_init", "Failed to parse OTEL configuration");
+ cleanup_otel_config(&g_otel_config);
+ return 0;
+ }
+
+ /* Initialize OTEL with Rust FFI */
+ const char* error = init_open_telemetry(g_otel_config.config);
+ if (error) {
+ VALKEY_LOG_ERROR_FMT("otel_init", "Failed to initialize OTEL: %s", error);
+ free_c_string((char*) error);
+ cleanup_otel_config(&g_otel_config);
+ return 0;
+ }
+
+ g_otel_config.enabled = true;
+ g_otel_initialized = true;
+ VALKEY_LOG_INFO("otel_init", "OpenTelemetry initialized successfully");
+ return 1;
+}
+
+/**
+ * Shutdown OpenTelemetry
+ */
+void valkey_glide_otel_shutdown(void) {
+ if (g_otel_config.enabled) {
+ g_otel_config.enabled = false;
+ g_otel_initialized = false;
+ VALKEY_LOG_INFO("otel_shutdown", "OpenTelemetry shutdown complete");
+ }
+}
+
+/**
+ * Create a span for command tracing
+ */
+uint64_t valkey_glide_create_span(enum RequestType request_type) {
+ if (!g_otel_config.enabled) {
+ return 0;
+ }
+ return create_otel_span(request_type);
+}
+
+/**
+ * Drop a span
+ */
+void valkey_glide_drop_span(uint64_t span_ptr) {
+ if (span_ptr != 0) {
+ drop_otel_span(span_ptr);
+ }
+}
+
+/**
+ * Parse OTEL configuration from PHP array
+ */
+int parse_otel_config_array(zval* config_array, valkey_glide_otel_config_t* otel_config) {
+ HashTable* ht = Z_ARRVAL_P(config_array);
+
+ /* Allocate main config */
+ otel_config->config = ecalloc(1, sizeof(struct OpenTelemetryConfig));
+
+ /* Set default flush interval */
+ otel_config->config->has_flush_interval_ms = true;
+ otel_config->config->flush_interval_ms = 5000;
+
+ /* Parse traces configuration */
+ zval* traces_val = zend_hash_str_find(ht, "traces", sizeof("traces") - 1);
+ if (traces_val && Z_TYPE_P(traces_val) == IS_ARRAY) {
+ otel_config->traces_config = ecalloc(1, sizeof(struct OpenTelemetryTracesConfig));
+ HashTable* traces_ht = Z_ARRVAL_P(traces_val);
+
+ /* Parse endpoint */
+ zval* endpoint_val = zend_hash_str_find(traces_ht, "endpoint", sizeof("endpoint") - 1);
+ if (endpoint_val && Z_TYPE_P(endpoint_val) == IS_STRING) {
+ otel_config->traces_config->endpoint = estrdup(Z_STRVAL_P(endpoint_val));
+ } else {
+ VALKEY_LOG_ERROR("otel_config",
+ "Traces endpoint is required when traces config is provided");
+ cleanup_otel_config(otel_config);
+ zend_throw_exception(get_valkey_glide_exception_ce(),
+ "Traces endpoint is required when traces config is provided",
+ 0);
+ return 0;
+ }
+
+ /* Parse sample_percentage with default of 1% */
+ zval* sample_val =
+ zend_hash_str_find(traces_ht, "sample_percentage", sizeof("sample_percentage") - 1);
+ if (sample_val && Z_TYPE_P(sample_val) == IS_LONG) {
+ long sample_pct = Z_LVAL_P(sample_val);
+ if (sample_pct < 0 || sample_pct > 100) {
+ VALKEY_LOG_ERROR("otel_config", "Sample percentage must be between 0 and 100");
+ cleanup_otel_config(otel_config);
+ zend_throw_exception(get_valkey_glide_exception_ce(),
+ "Sample percentage must be between 0 and 100",
+ 0);
+ return 0;
+ }
+ otel_config->traces_config->has_sample_percentage = true;
+ otel_config->traces_config->sample_percentage = (uint32_t) sample_pct;
+ } else {
+ /* Default to 1% */
+ otel_config->traces_config->has_sample_percentage = true;
+ otel_config->traces_config->sample_percentage = 1;
+ }
+
+ otel_config->config->traces = otel_config->traces_config;
+ }
+
+ /* Parse metrics configuration */
+ zval* metrics_val = zend_hash_str_find(ht, "metrics", sizeof("metrics") - 1);
+ if (metrics_val && Z_TYPE_P(metrics_val) == IS_ARRAY) {
+ otel_config->metrics_config = ecalloc(1, sizeof(struct OpenTelemetryMetricsConfig));
+ HashTable* metrics_ht = Z_ARRVAL_P(metrics_val);
+
+ /* Parse endpoint */
+ zval* endpoint_val = zend_hash_str_find(metrics_ht, "endpoint", sizeof("endpoint") - 1);
+ if (endpoint_val && Z_TYPE_P(endpoint_val) == IS_STRING) {
+ otel_config->metrics_config->endpoint = estrdup(Z_STRVAL_P(endpoint_val));
+ } else {
+ VALKEY_LOG_ERROR("otel_config",
+ "Metrics endpoint is required when metrics config is provided");
+ cleanup_otel_config(otel_config);
+ zend_throw_exception(get_valkey_glide_exception_ce(),
+ "Metrics endpoint is required when metrics config is provided",
+ 0);
+ return 0;
+ }
+
+ otel_config->config->metrics = otel_config->metrics_config;
+ }
+
+ /* Parse flush_interval_ms (override default if provided) */
+ zval* flush_val = zend_hash_str_find(ht, "flush_interval_ms", sizeof("flush_interval_ms") - 1);
+ if (flush_val && Z_TYPE_P(flush_val) == IS_LONG) {
+ long flush_ms = Z_LVAL_P(flush_val);
+ if (flush_ms <= 0) {
+ VALKEY_LOG_ERROR("otel_config", "Flush interval must be a positive integer");
+ cleanup_otel_config(otel_config);
+ zend_throw_exception(
+ get_valkey_glide_exception_ce(), "Flush interval must be a positive integer", 0);
+ return 0;
+ }
+ otel_config->config->flush_interval_ms = (uint32_t) flush_ms;
+ }
+
+ /* Validate at least one of traces or metrics is configured */
+ if (!otel_config->config->traces && !otel_config->config->metrics) {
+ VALKEY_LOG_ERROR("otel_config", "At least one of traces or metrics must be configured");
+ cleanup_otel_config(otel_config);
+ zend_throw_exception(get_valkey_glide_exception_ce(),
+ "At least one of traces or metrics must be configured",
+ 0);
+ return 0;
+ }
+
+ return 1;
+}
+
+/**
+ * Cleanup OTEL configuration
+ */
+void cleanup_otel_config(valkey_glide_otel_config_t* otel_config) {
+ if (otel_config->traces_config) {
+ if (otel_config->traces_config->endpoint) {
+ efree((void*) otel_config->traces_config->endpoint);
+ otel_config->traces_config->endpoint = NULL;
+ }
+ efree(otel_config->traces_config);
+ otel_config->traces_config = NULL;
+ }
+
+ if (otel_config->metrics_config) {
+ if (otel_config->metrics_config->endpoint) {
+ efree((void*) otel_config->metrics_config->endpoint);
+ otel_config->metrics_config->endpoint = NULL;
+ }
+ efree(otel_config->metrics_config);
+ otel_config->metrics_config = NULL;
+ }
+
+ if (otel_config->config) {
+ /* Reset pointers to avoid double-free */
+ otel_config->config->traces = NULL;
+ otel_config->config->metrics = NULL;
+ efree(otel_config->config);
+ otel_config->config = NULL;
+ }
+
+ otel_config->enabled = false;
+ otel_config->current_sample_percentage = 0;
+}
+
+/* Public API function implementations */
+
+bool valkey_glide_otel_is_initialized(void) {
+ return g_otel_config.enabled;
+}
+
+int32_t valkey_glide_otel_get_sample_percentage(void) {
+ if (!g_otel_config.enabled || !g_otel_config.traces_config) {
+ return -1; // Not initialized or no traces config
+ }
+ return (int32_t) g_otel_config.traces_config->sample_percentage;
+}
+
+bool valkey_glide_otel_set_sample_percentage(int32_t percentage) {
+ if (!g_otel_config.enabled || !g_otel_config.traces_config) {
+ return false; // Not initialized or no traces config
+ }
+
+ if (percentage < 0 || percentage > 100) {
+ return false; // Invalid percentage
+ }
+
+ g_otel_config.traces_config->sample_percentage = (uint32_t) percentage;
+ return true;
+}
+
+bool valkey_glide_otel_should_sample(void) {
+ int32_t percentage = valkey_glide_otel_get_sample_percentage();
+ return percentage > 0 && (percentage == 100 || (rand() % 100) < percentage);
+}
+
+uint64_t valkey_glide_otel_create_named_span(const char* name) {
+ if (!g_otel_config.enabled || !name || strlen(name) == 0) {
+ return 0; // Not initialized or invalid name
+ }
+
+ if (strlen(name) > 256) {
+ return 0; // Name too long
+ }
+
+ return create_named_otel_span(name);
+}
+
+bool valkey_glide_otel_end_span(uint64_t span_ptr) {
+ if (span_ptr == 0) {
+ return true; // Safe no-op for zero pointer
+ }
+
+ drop_otel_span(span_ptr);
+ return true;
+}
diff --git a/valkey_glide_otel.h b/valkey_glide_otel.h
new file mode 100644
index 00000000..1e0c7ca4
--- /dev/null
+++ b/valkey_glide_otel.h
@@ -0,0 +1,51 @@
+/*
+ +----------------------------------------------------------------------+
+ | Copyright (c) 2023-2025 The PHP Group |
+ +----------------------------------------------------------------------+
+ | This source file is subject to version 3.01 of the PHP license, |
+ | that is bundled with this package in the file LICENSE, and is |
+ | available through the world-wide-web at the following url: |
+ | http://www.php.net/license/3_01.txt |
+ | If you did not receive a copy of the PHP license and are unable to |
+ | obtain it through the world-wide-web, please send a note to |
+ | license@php.net so we can mail you a copy immediately. |
+ +----------------------------------------------------------------------+
+*/
+
+#ifndef VALKEY_GLIDE_OTEL_H
+#define VALKEY_GLIDE_OTEL_H
+
+#include "include/glide_bindings.h"
+#include "php.h"
+
+/* OTEL configuration structure for PHP */
+typedef struct {
+ bool enabled;
+ struct OpenTelemetryConfig* config;
+ struct OpenTelemetryTracesConfig* traces_config;
+ struct OpenTelemetryMetricsConfig* metrics_config;
+ int32_t current_sample_percentage;
+} valkey_glide_otel_config_t;
+
+/* Global OTEL configuration */
+extern valkey_glide_otel_config_t g_otel_config;
+
+/* Function declarations */
+int valkey_glide_otel_init(zval* config_array);
+void valkey_glide_otel_shutdown(void);
+uint64_t valkey_glide_create_span(enum RequestType request_type);
+void valkey_glide_drop_span(uint64_t span_ptr);
+
+/* Public API functions */
+bool valkey_glide_otel_is_initialized(void);
+int32_t valkey_glide_otel_get_sample_percentage(void);
+bool valkey_glide_otel_set_sample_percentage(int32_t percentage);
+bool valkey_glide_otel_should_sample(void);
+uint64_t valkey_glide_otel_create_named_span(const char* name);
+bool valkey_glide_otel_end_span(uint64_t span_ptr);
+
+/* Helper functions */
+int parse_otel_config_array(zval* config_array, valkey_glide_otel_config_t* otel_config);
+void cleanup_otel_config(valkey_glide_otel_config_t* otel_config);
+
+#endif /* VALKEY_GLIDE_OTEL_H */
diff --git a/valkey_glide_otel_commands.c b/valkey_glide_otel_commands.c
new file mode 100644
index 00000000..57b0462f
--- /dev/null
+++ b/valkey_glide_otel_commands.c
@@ -0,0 +1,61 @@
+/*
+ +----------------------------------------------------------------------+
+ | Copyright (c) 2023-2025 The PHP Group |
+ +----------------------------------------------------------------------+
+ | This source file is subject to version 3.01 of the PHP license, |
+ | that is bundled with this package in the file LICENSE, and is |
+ | available through the world-wide-web at the following url: |
+ | http://www.php.net/license/3_01.txt |
+ | If you did not receive a copy of the PHP license and are unable to |
+ | obtain it through the world-wide-web, please send a note to |
+ | license@php.net so we can mail you a copy immediately. |
+ +----------------------------------------------------------------------+
+*/
+
+#include "valkey_glide_otel_commands.h"
+
+#include "command_response.h"
+#include "logger.h"
+
+/**
+ * Execute command with OTEL span support
+ */
+CommandResult* execute_command_with_span(const void* client_adapter_ptr,
+ enum RequestType command_type,
+ unsigned long arg_count,
+ const uintptr_t* args,
+ const unsigned long* args_len,
+ uint64_t span_ptr) {
+ VALKEY_LOG_DEBUG_FMT(
+ "otel_command", "Executing command with span: %lu", (unsigned long) span_ptr);
+
+ /* Use the FFI command function with span support */
+ return command((void*) client_adapter_ptr,
+ (uintptr_t) client_adapter_ptr, /* request_id */
+ command_type,
+ arg_count,
+ args,
+ args_len,
+ NULL, /* route_bytes */
+ 0, /* route_bytes_len */
+ span_ptr);
+}
+
+/**
+ * Execute command with routing and OTEL span support
+ */
+CommandResult* execute_command_with_route_and_span(const void* client_adapter_ptr,
+ enum RequestType command_type,
+ unsigned long arg_count,
+ const uintptr_t* args,
+ const unsigned long* args_len,
+ zval* route,
+ uint64_t span_ptr) {
+ VALKEY_LOG_DEBUG_FMT(
+ "otel_command", "Executing routed command with span: %lu", (unsigned long) span_ptr);
+
+ /* For now, use the existing execute_command_with_route function */
+ /* TODO: Add proper route serialization and span support */
+ return execute_command_with_route(
+ (void*) client_adapter_ptr, command_type, arg_count, args, args_len, route);
+}
diff --git a/valkey_glide_otel_commands.h b/valkey_glide_otel_commands.h
new file mode 100644
index 00000000..0b8fc440
--- /dev/null
+++ b/valkey_glide_otel_commands.h
@@ -0,0 +1,37 @@
+/*
+ +----------------------------------------------------------------------+
+ | Copyright (c) 2023-2025 The PHP Group |
+ +----------------------------------------------------------------------+
+ | This source file is subject to version 3.01 of the PHP license, |
+ | that is bundled with this package in the file LICENSE, and is |
+ | available through the world-wide-web at the following url: |
+ | http://www.php.net/license/3_01.txt |
+ | If you did not receive a copy of the PHP license and are unable to |
+ | obtain it through the world-wide-web, please send a note to |
+ | license@php.net so we can mail you a copy immediately. |
+ +----------------------------------------------------------------------+
+*/
+
+#ifndef VALKEY_GLIDE_OTEL_COMMANDS_H
+#define VALKEY_GLIDE_OTEL_COMMANDS_H
+
+#include "command_response.h"
+#include "include/glide_bindings.h"
+
+/* Command execution wrappers with OTEL span support */
+CommandResult* execute_command_with_span(const void* client_adapter_ptr,
+ enum RequestType command_type,
+ unsigned long arg_count,
+ const uintptr_t* args,
+ const unsigned long* args_len,
+ uint64_t span_ptr);
+
+CommandResult* execute_command_with_route_and_span(const void* client_adapter_ptr,
+ enum RequestType command_type,
+ unsigned long arg_count,
+ const uintptr_t* args,
+ const unsigned long* args_len,
+ zval* route,
+ uint64_t span_ptr);
+
+#endif /* VALKEY_GLIDE_OTEL_COMMANDS_H */