1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143
|
# frozen_string_literal: true
module Dalli
##
# Instrumentation support for Dalli. Provides hooks for distributed tracing
# via OpenTelemetry when the SDK is available.
#
# When OpenTelemetry is loaded, Dalli automatically creates spans for cache operations.
# When OpenTelemetry is not available, all tracing methods are no-ops with zero overhead.
#
# Dalli 5.0 uses the stable OTel semantic conventions for database spans.
#
# == Span Attributes
#
# All spans include the following default attributes:
# - +db.system.name+ - Always "memcached"
#
# Single-key operations (+get+, +set+, +delete+, +incr+, +decr+, etc.) add:
# - +db.operation.name+ - The operation name (e.g., "get", "set")
# - +server.address+ - The server hostname (e.g., "localhost")
# - +server.port+ - The server port as an integer (e.g., 11211); omitted for Unix sockets
#
# Multi-key operations (+get_multi+) add:
# - +db.operation.name+ - "get_multi"
# - +db.memcached.key_count+ - Number of keys requested
# - +db.memcached.hit_count+ - Number of keys found in cache
# - +db.memcached.miss_count+ - Number of keys not found
#
# Bulk write operations (+set_multi+, +delete_multi+) add:
# - +db.operation.name+ - The operation name
# - +db.memcached.key_count+ - Number of keys in the operation
#
# == Optional Attributes
#
# - +db.query.text+ - The operation and key(s), controlled by the +:otel_db_statement+ client option:
# - +:include+ - Full text (e.g., "get mykey")
# - +:obfuscate+ - Obfuscated (e.g., "get ?")
# - +nil+ (default) - Attribute omitted
# - +peer.service+ - Logical service name, set via the +:otel_peer_service+ client option
#
# == Error Handling
#
# When an exception occurs during a traced operation:
# - The exception is recorded on the span via +record_exception+
# - The span status is set to error with the exception message
# - The exception is re-raised to the caller
#
# @example Checking if tracing is enabled
# Dalli::Instrumentation.enabled? # => true if OpenTelemetry is loaded
#
##
module Instrumentation
# Default attributes included on all memcached spans.
# @return [Hash] frozen hash with 'db.system.name' => 'memcached'
DEFAULT_ATTRIBUTES = { 'db.system.name' => 'memcached' }.freeze
class << self
# Returns the OpenTelemetry tracer if available, nil otherwise.
#
# The tracer is cached after first lookup for performance.
# Uses the library name 'dalli' and current Dalli::VERSION.
#
# @return [OpenTelemetry::Trace::Tracer, nil] the tracer or nil if OTel unavailable
# rubocop:disable ThreadSafety/ClassInstanceVariable
def tracer
return @tracer if defined?(@tracer)
@tracer = (OpenTelemetry.tracer_provider.tracer('dalli', Dalli::VERSION) if defined?(OpenTelemetry))
end
# rubocop:enable ThreadSafety/ClassInstanceVariable
# Returns true if instrumentation is enabled (OpenTelemetry SDK is available).
#
# @return [Boolean] true if tracing is active, false otherwise
def enabled?
!tracer.nil?
end
# Wraps a block with a span if instrumentation is enabled.
#
# Creates a client span with the given name and attributes merged with
# DEFAULT_ATTRIBUTES. The block is executed within the span context.
# If an exception occurs, it is recorded on the span before re-raising.
#
# When tracing is disabled (OpenTelemetry not loaded), this method
# simply yields directly with zero overhead.
#
# @param name [String] the span name (e.g., 'get', 'set', 'delete')
# @param attributes [Hash] span attributes to merge with defaults.
# Common attributes include:
# - 'db.operation.name' - the operation name
# - 'server.address' - the server hostname
# - 'server.port' - the server port (integer)
# - 'db.memcached.key_count' - number of keys (for multi operations)
# @yield the cache operation to trace
# @return [Object] the result of the block
# @raise [StandardError] re-raises any exception from the block
#
# @example Tracing a set operation
# trace('set', { 'db.operation.name' => 'set', 'server.address' => 'localhost', 'server.port' => 11211 }) do
# server.set(key, value, ttl)
# end
#
def trace(name, attributes = {})
return yield unless enabled?
tracer.in_span(name, attributes: DEFAULT_ATTRIBUTES.merge(attributes), kind: :client) do |_span|
yield
end
end
# Like trace, but yields the span to allow adding attributes after execution.
#
# This is useful for operations where metrics are only known after the
# operation completes, such as get_multi where hit/miss counts depend
# on the cache response.
#
# When tracing is disabled, yields nil as the span argument.
#
# @param name [String] the span name (e.g., 'get_multi')
# @param attributes [Hash] initial span attributes to merge with defaults
# @yield [OpenTelemetry::Trace::Span, nil] the span object, or nil if disabled
# @return [Object] the result of the block
# @raise [StandardError] re-raises any exception from the block
#
# @example Recording hit/miss metrics after get_multi
# trace_with_result('get_multi', { 'db.operation.name' => 'get_multi' }) do |span|
# results = fetch_from_cache(keys)
# if span
# span.set_attribute('db.memcached.hit_count', results.size)
# span.set_attribute('db.memcached.miss_count', keys.size - results.size)
# end
# results
# end
#
def trace_with_result(name, attributes = {}, &)
return yield(nil) unless enabled?
tracer.in_span(name, attributes: DEFAULT_ATTRIBUTES.merge(attributes), kind: :client, &)
end
end
end
end
|