Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DemetrisChr somehow we need to figure out proper fix here. The idea is to use specific precompiled gem for the tests and avoid fetching it from the repository.

Maybe some other solution will fit better here. Like

gem install ${COUCHBASE_GEM_PATH}

and then, point bundler to use it from there.

or from the different angle, create local gem repository like

mkdir -p my-local-repo/gems
cp ${COUCHBASE_GEM_PATH} my-local-repo/gems/
gem generate_index --verbose --directory my-local-repo/

And then patch Gemfile to use something like

gem "couchbase", path: "my-local-repo/"

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
Expand Down
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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
Expand Down
52 changes: 52 additions & 0 deletions couchbase-opentelemetry/couchbase-opentelemetry.gemspec
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion ext/rcb_query.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
6 changes: 6 additions & 0 deletions lib/couchbase/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -410,5 +410,11 @@ class NoEnvironment < CouchbaseError

class ClusterClosed < CouchbaseError
end

class TracerError < CouchbaseError
end

class MeterError < CouchbaseError
end
end
end
10 changes: 4 additions & 6 deletions lib/couchbase/tracing/noop_span.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 4 additions & 0 deletions lib/couchbase/tracing/request_span.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions lib/couchbase/tracing/threshold_logging_span.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions lib/couchbase/utils/observability.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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?
Expand Down
5 changes: 5 additions & 0 deletions lib/couchbase/utils/observability_constants.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Loading
Loading