From 12c34625802902de4f25b1b60d75f3e499778ef4 Mon Sep 17 00:00:00 2001 From: Dimitris Christodoulou Date: Thu, 22 Jan 2026 11:21:24 +0000 Subject: [PATCH 1/3] RCBC-524: OpenTelemetry integration --- Gemfile | 3 + .../couchbase-opentelemetry.gemspec | 52 +++++ .../couchbase/metrics/open_telemetry_meter.rb | 49 +++++ .../metrics/open_telemetry_value_recorder.rb | 42 ++++ .../tracing/open_telemetry_request_span.rb | 49 +++++ .../tracing/open_telemetry_request_tracer.rb | 50 +++++ lib/couchbase/errors.rb | 6 + lib/couchbase/tracing/noop_span.rb | 10 +- lib/couchbase/tracing/request_span.rb | 4 + .../tracing/threshold_logging_span.rb | 2 + lib/couchbase/utils/observability.rb | 8 + .../utils/observability_constants.rb | 5 + test/opentelemetry_test.rb | 189 ++++++++++++++++++ test/utils/tracing/test_span.rb | 5 + 14 files changed, 468 insertions(+), 6 deletions(-) create mode 100644 couchbase-opentelemetry/couchbase-opentelemetry.gemspec create mode 100644 couchbase-opentelemetry/lib/couchbase/metrics/open_telemetry_meter.rb create mode 100644 couchbase-opentelemetry/lib/couchbase/metrics/open_telemetry_value_recorder.rb create mode 100644 couchbase-opentelemetry/lib/couchbase/tracing/open_telemetry_request_span.rb create mode 100644 couchbase-opentelemetry/lib/couchbase/tracing/open_telemetry_request_tracer.rb create mode 100644 test/opentelemetry_test.rb diff --git a/Gemfile b/Gemfile index fb7d7fa1..26b3f6f2 100644 --- a/Gemfile +++ b/Gemfile @@ -18,6 +18,7 @@ source "https://rubygems.org" git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } gemspec +gemspec path: "couchbase-opentelemetry" gem "rake" @@ -35,6 +36,8 @@ group :development do gem "minitest", "< 6.0" gem "minitest-reporters" gem "mutex_m" + gem "opentelemetry-metrics-sdk" + gem "opentelemetry-sdk" gem "rack" gem "reek" gem "rubocop", require: false diff --git a/couchbase-opentelemetry/couchbase-opentelemetry.gemspec b/couchbase-opentelemetry/couchbase-opentelemetry.gemspec new file mode 100644 index 00000000..3dc74d82 --- /dev/null +++ b/couchbase-opentelemetry/couchbase-opentelemetry.gemspec @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +# Copyright 2026-Present Couchbase, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +lib = File.expand_path("lib", __dir__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) + +Gem::Specification.new do |spec| + spec.name = "couchbase-opentelemetry" + spec.version = "0.0.1" + spec.authors = ["Sergey Avseyev"] + spec.email = ["sergey.avseyev@gmail.com"] + spec.summary = "OpenTelemetry integration for the Couchbase Ruby Client" + spec.description = "OpenTelemetry integration for the Couchbase Ruby Client" + spec.homepage = "https://www.couchbase.com" + spec.license = "Apache-2.0" + spec.required_ruby_version = "> 3.1" + + spec.metadata = { + "homepage_uri" => "https://docs.couchbase.com/ruby-sdk/current/hello-world/start-using-sdk.html", + "bug_tracker_uri" => "https://jira.issues.couchbase.com/browse/RCBC", + "mailing_list_uri" => "https://www.couchbase.com/forums/c/ruby-sdk", + "source_code_uri" => "https://github.com/couchbase/couchbase-ruby-client/tree/#{spec.version}", + "changelog_uri" => "https://github.com/couchbase/couchbase-ruby-client/releases/tag/#{spec.version}", + "documentation_uri" => "https://docs.couchbase.com/sdk-api/couchbase-ruby-client-#{spec.version}/index.html", + "github_repo" => "https://github.com/couchbase/couchbase-ruby-client", + "rubygems_mfa_required" => "true", + } + + spec.files = Dir.glob([ + "lib/**/*.rb", + ], File::FNM_DOTMATCH).select { |path| File.file?(path) } + spec.bindir = "exe" + spec.executables = spec.files.grep(/^exe\//) { |f| File.basename(f) } + spec.require_paths = ["lib"] + + spec.add_dependency "concurrent-ruby", "~> 1.3" + spec.add_dependency "opentelemetry-api", "~> 1.7" + spec.add_dependency "opentelemetry-metrics-api", "~> 0.4.0" +end diff --git a/couchbase-opentelemetry/lib/couchbase/metrics/open_telemetry_meter.rb b/couchbase-opentelemetry/lib/couchbase/metrics/open_telemetry_meter.rb new file mode 100644 index 00000000..82b5525d --- /dev/null +++ b/couchbase-opentelemetry/lib/couchbase/metrics/open_telemetry_meter.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +# Copyright 2026-Present Couchbase, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +require "couchbase/metrics/meter" +require "couchbase/errors" +require_relative "open_telemetry_value_recorder" + +require "opentelemetry-metrics-api" + +module Couchbase + module Metrics + class OpenTelemetryMeter < Meter + def initialize(meter_provider) + super() + @histogram_cache = Concurrent::Map.new + begin + @wrapped = meter_provider.meter("com.couchbase.client/ruby") + rescue StandardError => e + raise Error::MeterError.new("Failed to create OpenTelemetry Meter: #{e.message}", nil, e) + end + end + + def value_recorder(name, tags) + unit = tags.delete("__unit") + + otel_histogram = @histogram_cache.compute_if_absent(name) do + @wrapped.create_histogram(name, unit: unit) + end + + OpenTelemetryValueRecorder.new(otel_histogram, tags, unit: unit) + rescue StandardError => e + raise Error::MeterError.new("Failed to create OpenTelemetry Histogram: #{e.message}", nil, e) + end + end + end +end diff --git a/couchbase-opentelemetry/lib/couchbase/metrics/open_telemetry_value_recorder.rb b/couchbase-opentelemetry/lib/couchbase/metrics/open_telemetry_value_recorder.rb new file mode 100644 index 00000000..28bbfd69 --- /dev/null +++ b/couchbase-opentelemetry/lib/couchbase/metrics/open_telemetry_value_recorder.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +# Copyright 2026-Present Couchbase, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +require "couchbase/metrics/value_recorder" + +module Couchbase + module Metrics + class OpenTelemetryValueRecorder < ValueRecorder + def initialize(recorder, tags, unit: nil) + super() + @wrapped = recorder + @tags = tags + @unit = unit + end + + def record_value(value) + value = + case @unit + when "s" + value / 1_000_000.0 + else + value + end + + @wrapped.record(value, attributes: @tags) + end + end + end +end diff --git a/couchbase-opentelemetry/lib/couchbase/tracing/open_telemetry_request_span.rb b/couchbase-opentelemetry/lib/couchbase/tracing/open_telemetry_request_span.rb new file mode 100644 index 00000000..af17b6d6 --- /dev/null +++ b/couchbase-opentelemetry/lib/couchbase/tracing/open_telemetry_request_span.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +# Copyright 2026-Present Couchbase, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +require "couchbase/tracing/request_span" + +require "opentelemetry-api" + +module Couchbase + module Tracing + class OpenTelemetryRequestSpan < RequestSpan + def initialize(span) + super() + + @wrapped = span + end + + def set_attribute(key, value) + @wrapped.set_attribute(key, value) + end + + def status=(status_code) + @wrapped.status = if status_code == :ok + ::OpenTelemetry::Trace::Status.ok + elsif status_code == :error + ::OpenTelemetry::Trace::Status.error + else + ::OpenTelemetry::Trace::Status.unset + end + end + + def finish(end_timestamp: nil) + @wrapped.finish(end_timestamp: end_timestamp) + end + end + end +end diff --git a/couchbase-opentelemetry/lib/couchbase/tracing/open_telemetry_request_tracer.rb b/couchbase-opentelemetry/lib/couchbase/tracing/open_telemetry_request_tracer.rb new file mode 100644 index 00000000..776f7e70 --- /dev/null +++ b/couchbase-opentelemetry/lib/couchbase/tracing/open_telemetry_request_tracer.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +# Copyright 2026-Present Couchbase, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +require "opentelemetry-api" + +require "couchbase/tracing/request_tracer" +require "couchbase/errors" +require_relative "open_telemetry_request_span" + +module Couchbase + module Tracing + class OpenTelemetryRequestTracer < RequestTracer + def initialize(tracer_provider) + super() + begin + @wrapped = tracer_provider.tracer("com.couchbase.client/ruby") + rescue StandardError => e + raise Error::TracerError.new("Failed to create OpenTelemetry tracer: #{e.message}", nil, e) + end + end + + def request_span(name, parent: nil, start_timestamp: nil) + parent_context = parent.nil? ? nil : ::OpenTelemetry::Trace.context_with_span(parent.instance_variable_get(:@wrapped)) + OpenTelemetryRequestSpan.new( + @wrapped.start_span( + name, + with_parent: parent_context, + start_timestamp: start_timestamp, + kind: :client, + ), + ) + rescue StandardError => e + raise Error::TracerError.new("Failed to create OpenTelemetry span: #{e.message}", nil, e) + end + end + end +end diff --git a/lib/couchbase/errors.rb b/lib/couchbase/errors.rb index c1bc7170..61008b94 100644 --- a/lib/couchbase/errors.rb +++ b/lib/couchbase/errors.rb @@ -410,5 +410,11 @@ class NoEnvironment < CouchbaseError class ClusterClosed < CouchbaseError end + + class TracerError < CouchbaseError + end + + class MeterError < CouchbaseError + end end end diff --git a/lib/couchbase/tracing/noop_span.rb b/lib/couchbase/tracing/noop_span.rb index be598b50..9c3b1c3a 100644 --- a/lib/couchbase/tracing/noop_span.rb +++ b/lib/couchbase/tracing/noop_span.rb @@ -19,13 +19,11 @@ module Couchbase module Tracing class NoopSpan < RequestSpan - def set_attribute(*) - nil - end + def set_attribute(*); end - def finish(*) - nil - end + def status=(*); end + + def finish(*); end end end end diff --git a/lib/couchbase/tracing/request_span.rb b/lib/couchbase/tracing/request_span.rb index 8e7c42ff..d70f3814 100644 --- a/lib/couchbase/tracing/request_span.rb +++ b/lib/couchbase/tracing/request_span.rb @@ -22,6 +22,10 @@ def set_attribute(key, value) raise NotImplementedError, "The RequestSpan does not implement #set_attribute" end + def status=(status_code) + raise NotImplementedError, "The RequestSpan does not implement #status=" + end + def finish(end_timestamp: nil) raise NotImplementedError, "The RequestSpan does not implement #finish" end diff --git a/lib/couchbase/tracing/threshold_logging_span.rb b/lib/couchbase/tracing/threshold_logging_span.rb index 8ef94513..36cf474c 100644 --- a/lib/couchbase/tracing/threshold_logging_span.rb +++ b/lib/couchbase/tracing/threshold_logging_span.rb @@ -63,6 +63,8 @@ def set_attribute(key, value) end end + def status=(*); end + def finish(end_timestamp: nil) duration_us = (((end_timestamp || Time.now) - @start_timestamp) * 1_000_000).round case name diff --git a/lib/couchbase/utils/observability.rb b/lib/couchbase/utils/observability.rb index 8bfdfe12..5deaf452 100644 --- a/lib/couchbase/utils/observability.rb +++ b/lib/couchbase/utils/observability.rb @@ -47,6 +47,8 @@ def record_operation(op_name, parent_span, receiver, service = nil) rescue StandardError => e handler.add_error(e) raise e + else + handler.set_success ensure handler.finish end @@ -144,7 +146,12 @@ def add_retries(retries) @op_span.set_attribute(ATTR_RETRIES, retries.to_i) end + def set_success + @op_span.status = :ok + end + def add_error(error) + @op_span.status = :error @meter_attributes[ATTR_ERROR_TYPE] = if error.is_a?(Couchbase::Error::CouchbaseError) || error.is_a?(Couchbase::Error::InvalidArgument) error.class.name.split("::").last @@ -201,6 +208,7 @@ def convert_backend_timestamp(backend_timestamp) def create_meter_attributes attrs = { ATTR_SYSTEM_NAME => ATTR_VALUE_SYSTEM_NAME, + ATTR_RESERVED_UNIT => ATTR_VALUE_RESERVED_UNIT_SECONDS, } attrs[ATTR_CLUSTER_NAME] = @cluster_name unless @cluster_name.nil? attrs[ATTR_CLUSTER_UUID] = @cluster_uuid unless @cluster_uuid.nil? diff --git a/lib/couchbase/utils/observability_constants.rb b/lib/couchbase/utils/observability_constants.rb index 611a7d48..c4ca87a7 100644 --- a/lib/couchbase/utils/observability_constants.rb +++ b/lib/couchbase/utils/observability_constants.rb @@ -177,6 +177,9 @@ module Observability # rubocop:disable Metrics/ModuleLength ATTR_PEER_PORT = "network.peer.port" ATTR_SERVER_DURATION = "couchbase.server_duration" + # Reserved attributes + ATTR_RESERVED_UNIT = "__unit" + ATTR_VALUE_SYSTEM_NAME = "couchbase" ATTR_VALUE_DURABILITY_MAJORITY = "majority" @@ -190,6 +193,8 @@ module Observability # rubocop:disable Metrics/ModuleLength ATTR_VALUE_SERVICE_ANALYTICS = "analytics" ATTR_VALUE_SERVICE_MANAGEMENT = "management" + ATTR_VALUE_RESERVED_UNIT_SECONDS = "s" + METER_NAME_OPERATION_DURATION = "db.client.operation.duration" end end diff --git a/test/opentelemetry_test.rb b/test/opentelemetry_test.rb new file mode 100644 index 00000000..c92fcba4 --- /dev/null +++ b/test/opentelemetry_test.rb @@ -0,0 +1,189 @@ +# frozen_string_literal: true + +# Copyright 2026-Present Couchbase, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +require_relative "test_helper" + +require "couchbase/tracing/open_telemetry_request_tracer" +require "couchbase/metrics/open_telemetry_meter" + +require "opentelemetry-sdk" +require "opentelemetry-metrics-sdk" + +module Couchbase + class OpenTelemetryTest < Minitest::Test + include TestUtilities + + def setup + @span_exporter = ::OpenTelemetry::SDK::Trace::Export::InMemorySpanExporter.new + @tracer_provider = + begin + tracer_provider = ::OpenTelemetry::SDK::Trace::TracerProvider.new + tracer_provider.add_span_processor( + ::OpenTelemetry::SDK::Trace::Export::SimpleSpanProcessor.new(@span_exporter), + ) + tracer_provider + end + @tracer = Couchbase::Tracing::OpenTelemetryRequestTracer.new(@tracer_provider) + + @metric_exporter = ::OpenTelemetry::SDK::Metrics::Export::InMemoryMetricPullExporter.new + @metric_reader = ::OpenTelemetry::SDK::Metrics::Export::PeriodicMetricReader.new(exporter: @metric_exporter) + @meter_provider = + begin + meter_provider = ::OpenTelemetry::SDK::Metrics::MeterProvider.new + meter_provider.add_metric_reader(@metric_reader) + meter_provider + end + @meter = Couchbase::Metrics::OpenTelemetryMeter.new(@meter_provider) + + connect(Options::Cluster.new(tracer: @tracer, meter: @meter)) + @bucket = @cluster.bucket(env.bucket) + @collection = @bucket.default_collection + @parent_span = @tracer.request_span("parent_span") + end + + def teardown + disconnect + @tracer_provider.shutdown + @meter_provider.shutdown + end + + def assert_otel_span( + span_data, + name, + attributes: {}, + parent_span_id: nil, + status_code: OpenTelemetry::Trace::Status::UNSET + ) + assert_equal name, span_data.name + assert_equal :client, span_data.kind + assert_equal status_code, span_data.status.code + + if parent_span_id.nil? + assert_predicate span_data.parent_span_id.hex, :zero? + else + assert_equal parent_span_id, span_data.parent_span_id + end + + attributes.each do |key, value| + if value.nil? + assert span_data.attributes.key?(key), "Expected attribute #{key} to be present" + else + assert_equal value, span_data.attributes[key], "Expected attribute #{key} to have value #{value}" + end + end + end + + def test_opentelemetry_tracer + res = @collection.upsert(uniq_id(:otel_test), {foo: "bar"}, Options::Upsert.new(parent_span: @parent_span)) + + assert_predicate res.cas, :positive? + + @parent_span.finish + spans = @span_exporter.finished_spans.sort_by(&:start_timestamp) + + assert_otel_span( + spans[0], + "parent_span", + attributes: {}, + parent_span_id: nil, + ) + + assert_otel_span( + spans[1], + "upsert", + attributes: { + "db.system.name" => "couchbase", + "couchbase.cluster.name" => env.cluster_name, + "couchbase.cluster.uuid" => env.cluster_uuid, + "db.operation.name" => "upsert", + "db.namespace" => @bucket.name, + "couchbase.scope.name" => "_default", + "couchbase.collection.name" => "_default", + "couchbase.retries" => nil, + }, + parent_span_id: spans[0].span_id, + status_code: OpenTelemetry::Trace::Status::OK, + ) + + assert_otel_span( + spans[2], + "request_encoding", + attributes: { + "db.system.name" => "couchbase", + "couchbase.cluster.name" => env.cluster_name, + "couchbase.cluster.uuid" => env.cluster_uuid, + }, + parent_span_id: spans[1].span_id, + ) + + assert_otel_span( + spans[3], + "dispatch_to_server", + attributes: { + "db.system.name" => "couchbase", + "couchbase.cluster.name" => env.cluster_name, + "couchbase.cluster.uuid" => env.cluster_uuid, + "network.peer.address" => nil, + "network.peer.port" => nil, + "network.transport" => "tcp", + "server.address" => nil, + "server.port" => nil, + "couchbase.local_id" => nil, + }, + parent_span_id: spans[1].span_id, + ) + end + + def test_opentelemetry_meter + assert_raises(Error::DocumentNotFound) do + @collection.get(uniq_id(:does_not_exist)) + end + + @collection.insert(uniq_id(:otel_test), {foo: "bar"}) + + @metric_reader.force_flush + snapshots = @metric_exporter.metric_snapshots + + assert_equal 1, snapshots.size + + snapshot = snapshots[0] + + assert_equal "db.client.operation.duration", snapshot.name + assert_equal "s", snapshot.unit + assert_equal :histogram, snapshot.instrument_kind + assert_equal 2, snapshot.data_points.size + + snapshot.data_points.each_with_index do |p, idx| + assert_equal "couchbase", p.attributes["db.system.name"] + assert_equal env.cluster_name, p.attributes["couchbase.cluster.name"] + assert_equal env.cluster_uuid, p.attributes["couchbase.cluster.uuid"] + assert_equal env.bucket, p.attributes["db.namespace"] + assert_equal "_default", p.attributes["couchbase.scope.name"] + assert_equal "_default", p.attributes["couchbase.collection.name"] + assert_equal "kv", p.attributes["couchbase.service"] + + case idx + when 0 + assert_equal "get", p.attributes["db.operation.name"] + assert_equal "DocumentNotFound", p.attributes["error.type"] + when 1 + assert_equal "insert", p.attributes["db.operation.name"] + assert_nil p.attributes["error.type"] + end + end + end + end +end diff --git a/test/utils/tracing/test_span.rb b/test/utils/tracing/test_span.rb index 17a1c803..3d27eae9 100644 --- a/test/utils/tracing/test_span.rb +++ b/test/utils/tracing/test_span.rb @@ -24,6 +24,7 @@ class TestSpan < Couchbase::Tracing::RequestSpan attr_accessor :attributes attr_accessor :parent attr_accessor :children + attr_accessor :status_code def initialize(name, parent: nil, start_timestamp: nil) super() @@ -38,6 +39,10 @@ def set_attribute(key, value) @attributes[key] = value end + def status=(status_code) + @status_code = status_code + end + def finish(end_timestamp: nil) @end_time = end_timestamp.nil? ? Time.now : end_timestamp end From d4904f6dc38f13483a6581c0976e756843dda13e Mon Sep 17 00:00:00 2001 From: Sergey Avseyev Date: Thu, 22 Jan 2026 10:26:45 -0800 Subject: [PATCH 2/3] fix: improve regex pattern precision in CI workflow Update the gemspec replacement pattern from `/gemspec/` to `/gemspec$/` to ensure it only matches "gemspec" at the end of lines. This prevents potential partial matches that could cause incorrect Gemfile modifications during the test gem installation process. The $ anchor makes the regex more precise and robust for the automated gem unpacking and bundling workflow. --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6d143411..ccd9eae1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -471,7 +471,7 @@ jobs: COUCHBASE_GEM_PATH=$(realpath couchbase-*.gem) UNPACKED_GEM_PATH=$(gem unpack ${COUCHBASE_GEM_PATH} | grep "Unpacked gem" | cut -d "'" -f 2) gem unpack --spec --target ${UNPACKED_GEM_PATH} ${COUCHBASE_GEM_PATH} - ruby -i.bak -pe "gsub(/gemspec/, 'gem \"couchbase\", path: \"${UNPACKED_GEM_PATH}\"')" Gemfile + ruby -i.bak -pe "gsub(/gemspec$/, 'gem \"couchbase\", path: \"${UNPACKED_GEM_PATH}\"')" Gemfile bundle install bundle exec ruby -r bundler/setup -r couchbase -e 'pp Couchbase::VERSION, Couchbase::BUILD_INFO' - name: Test From 199e6de01e3a802f70bf46d229d36675efa1753a Mon Sep 17 00:00:00 2001 From: Sergey Avseyev Date: Thu, 22 Jan 2026 12:52:21 -0800 Subject: [PATCH 3/3] fix(rcb_query.cxx): add missing bucket_name to error message The bucket_name parameter was missing from the fmt::format call in cb_Backend_query_index_build_deferred, causing the error message to not include the bucket name when reporting deferred index build failures. Which in turn caused build failure with recent GCC, where fmtlib tries to validate format strings compile-time. --- ext/rcb_query.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ext/rcb_query.cxx b/ext/rcb_query.cxx index 72fcf063..d14be73c 100644 --- a/ext/rcb_query.cxx +++ b/ext/rcb_query.cxx @@ -536,8 +536,8 @@ cb_Backend_query_index_build_deferred(VALUE self, cb_throw_error( resp.ctx, fmt::format(R"(unable to build deferred indexes on the bucket "{}" ({}: {}))", + req.bucket_name, first_error.code, - first_error.message)); } else { cb_throw_error(