Skip to content

Commit 0fa268c

Browse files
Align OTel instrumentation with stable semantic conventions
Update span attributes to use stable OpenTelemetry semantic conventions: - db.system → db.system.name - db.operation → db.operation.name - Split server.address into hostname + server.port - Add db.query.text support via :otel_db_statement option - Add peer.service support via :otel_peer_service option - Add server.address/server.port to get_with_metadata and fetch_with_lock spans Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 62f694f commit 0fa268c

File tree

5 files changed

+289
-35
lines changed

5 files changed

+289
-35
lines changed

.rubocop_todo.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ Metrics/AbcSize:
1414
# Offense count: 9
1515
# Configuration parameters: CountComments, CountAsOne.
1616
Metrics/ClassLength:
17-
Max: 290
17+
Max: 300
1818

1919
# Offense count: 4
2020
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,21 @@
11
Dalli Changelog
22
=====================
33

4+
4.2.1
5+
==========
6+
7+
OpenTelemetry:
8+
9+
- Migrate to stable OTel semantic conventions
10+
- `db.system` renamed to `db.system.name`
11+
- `db.operation` renamed to `db.operation.name`
12+
- `server.address` now contains hostname only; `server.port` is a separate integer attribute
13+
- `get_with_metadata` and `fetch_with_lock` now include `server.address`/`server.port`
14+
- Add `db.query.text` span attribute with configurable modes
15+
- `:otel_db_statement` option: `:include`, `:obfuscate`, or `nil` (default: omitted)
16+
- Add `peer.service` span attribute
17+
- `:otel_peer_service` option for logical service naming
18+
419
4.2.0
520
==========
621

lib/dalli/client.rb

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,10 @@ class Client
5050
# useful for injecting a FIPS compliant hash object.
5151
# - :protocol - one of either :binary or :meta, defaulting to :binary. This sets the protocol that Dalli uses
5252
# to communicate with memcached.
53+
# - :otel_db_statement - controls the +db.query.text+ span attribute when OpenTelemetry is loaded.
54+
# +:include+ logs the full operation and key(s), +:obfuscate+ replaces keys with "?",
55+
# +nil+ (default) omits the attribute entirely.
56+
# - :otel_peer_service - when set, adds a +peer.service+ span attribute with this value for logical service naming.
5357
#
5458
def initialize(servers = nil, options = {})
5559
@normalized_servers = ::Dalli::ServersArgNormalizer.normalize_servers(servers)
@@ -134,8 +138,8 @@ def get_with_metadata(key, options = {})
134138
key = key.to_s
135139
key = @key_manager.validate_key(key)
136140

137-
Instrumentation.trace('get_with_metadata', { 'db.operation' => 'get_with_metadata' }) do
138-
server = ring.server_for_key(key)
141+
server = ring.server_for_key(key)
142+
Instrumentation.trace('get_with_metadata', trace_attrs('get_with_metadata', key, server)) do
139143
server.request(:meta_get, key, options)
140144
end
141145
rescue NetworkError => e
@@ -237,7 +241,8 @@ def fetch_with_lock(key, ttl: nil, lock_ttl: 30, recache_threshold: nil, req_opt
237241
key = key.to_s
238242
key = @key_manager.validate_key(key)
239243

240-
Instrumentation.trace('fetch_with_lock', { 'db.operation' => 'fetch_with_lock' }) do
244+
server = ring.server_for_key(key)
245+
Instrumentation.trace('fetch_with_lock', trace_attrs('fetch_with_lock', key, server)) do
241246
fetch_with_lock_request(key, ttl, lock_ttl, recache_threshold, req_options, &block)
242247
end
243248
rescue NetworkError => e
@@ -318,10 +323,7 @@ def set(key, value, ttl = nil, req_options = nil)
318323
def set_multi(hash, ttl = nil, req_options = nil)
319324
return if hash.empty?
320325

321-
Instrumentation.trace('set_multi', {
322-
'db.operation' => 'set_multi',
323-
'db.memcached.key_count' => hash.size
324-
}) do
326+
Instrumentation.trace('set_multi', multi_trace_attrs('set_multi', hash.size, hash.keys)) do
325327
pipelined_setter.process(hash, ttl_or_default(ttl), req_options)
326328
end
327329
end
@@ -378,10 +380,7 @@ def delete(key)
378380
def delete_multi(keys)
379381
return if keys.empty?
380382

381-
Instrumentation.trace('delete_multi', {
382-
'db.operation' => 'delete_multi',
383-
'db.memcached.key_count' => keys.size
384-
}) do
383+
Instrumentation.trace('delete_multi', multi_trace_attrs('delete_multi', keys.size, keys)) do
385384
pipelined_deleter.process(keys)
386385
end
387386
end
@@ -548,7 +547,30 @@ def get_multi_hash(keys)
548547
end
549548

550549
def get_multi_attributes(keys)
551-
{ 'db.operation' => 'get_multi', 'db.memcached.key_count' => keys.size }
550+
multi_trace_attrs('get_multi', keys.size, keys)
551+
end
552+
553+
def trace_attrs(operation, key, server)
554+
attrs = { 'db.operation.name' => operation, 'server.address' => server.hostname }
555+
attrs['server.port'] = server.port if server.socket_type == :tcp
556+
attrs['peer.service'] = @options[:otel_peer_service] if @options[:otel_peer_service]
557+
add_query_text(attrs, operation, key)
558+
end
559+
560+
def multi_trace_attrs(operation, key_count, keys)
561+
attrs = { 'db.operation.name' => operation, 'db.memcached.key_count' => key_count }
562+
attrs['peer.service'] = @options[:otel_peer_service] if @options[:otel_peer_service]
563+
add_query_text(attrs, operation, keys)
564+
end
565+
566+
def add_query_text(attrs, operation, key_or_keys)
567+
case @options[:otel_db_statement]
568+
when :include
569+
attrs['db.query.text'] = "#{operation} #{Array(key_or_keys).join(' ')}"
570+
when :obfuscate
571+
attrs['db.query.text'] = "#{operation} ?"
572+
end
573+
attrs
552574
end
553575

554576
def check_positive!(amt)
@@ -616,10 +638,7 @@ def perform(*all_args)
616638
key = @key_manager.validate_key(key)
617639

618640
server = ring.server_for_key(key)
619-
Instrumentation.trace(op.to_s, {
620-
'db.operation' => op.to_s,
621-
'server.address' => server.name
622-
}) do
641+
Instrumentation.trace(op.to_s, trace_attrs(op.to_s, key, server)) do
623642
server.request(op, key, *args)
624643
end
625644
rescue NetworkError => e

lib/dalli/instrumentation.rb

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,36 @@ module Dalli
88
# When OpenTelemetry is loaded, Dalli automatically creates spans for cache operations.
99
# When OpenTelemetry is not available, all tracing methods are no-ops with zero overhead.
1010
#
11+
# Dalli 4.2.1 uses the stable OTel semantic conventions for database spans.
12+
#
1113
# == Span Attributes
1214
#
1315
# All spans include the following default attributes:
14-
# - +db.system+ - Always "memcached"
16+
# - +db.system.name+ - Always "memcached"
1517
#
1618
# Single-key operations (+get+, +set+, +delete+, +incr+, +decr+, etc.) add:
17-
# - +db.operation+ - The operation name (e.g., "get", "set")
18-
# - +server.address+ - The memcached server handling the request (e.g., "localhost:11211")
19+
# - +db.operation.name+ - The operation name (e.g., "get", "set")
20+
# - +server.address+ - The server hostname (e.g., "localhost")
21+
# - +server.port+ - The server port as an integer (e.g., 11211); omitted for Unix sockets
1922
#
2023
# Multi-key operations (+get_multi+) add:
21-
# - +db.operation+ - "get_multi"
24+
# - +db.operation.name+ - "get_multi"
2225
# - +db.memcached.key_count+ - Number of keys requested
2326
# - +db.memcached.hit_count+ - Number of keys found in cache
2427
# - +db.memcached.miss_count+ - Number of keys not found
2528
#
2629
# Bulk write operations (+set_multi+, +delete_multi+) add:
27-
# - +db.operation+ - The operation name
30+
# - +db.operation.name+ - The operation name
2831
# - +db.memcached.key_count+ - Number of keys in the operation
2932
#
33+
# == Optional Attributes
34+
#
35+
# - +db.query.text+ - The operation and key(s), controlled by the +:otel_db_statement+ client option:
36+
# - +:include+ - Full text (e.g., "get mykey")
37+
# - +:obfuscate+ - Obfuscated (e.g., "get ?")
38+
# - +nil+ (default) - Attribute omitted
39+
# - +peer.service+ - Logical service name, set via the +:otel_peer_service+ client option
40+
#
3041
# == Error Handling
3142
#
3243
# When an exception occurs during a traced operation:
@@ -40,8 +51,8 @@ module Dalli
4051
##
4152
module Instrumentation
4253
# Default attributes included on all memcached spans.
43-
# @return [Hash] frozen hash with 'db.system' => 'memcached'
44-
DEFAULT_ATTRIBUTES = { 'db.system' => 'memcached' }.freeze
54+
# @return [Hash] frozen hash with 'db.system.name' => 'memcached'
55+
DEFAULT_ATTRIBUTES = { 'db.system.name' => 'memcached' }.freeze
4556

4657
class << self
4758
# Returns the OpenTelemetry tracer if available, nil otherwise.
@@ -75,15 +86,16 @@ def enabled?
7586
# @param name [String] the span name (e.g., 'get', 'set', 'delete')
7687
# @param attributes [Hash] span attributes to merge with defaults.
7788
# Common attributes include:
78-
# - 'db.operation' - the operation name
79-
# - 'server.address' - the target server
89+
# - 'db.operation.name' - the operation name
90+
# - 'server.address' - the server hostname
91+
# - 'server.port' - the server port (integer)
8092
# - 'db.memcached.key_count' - number of keys (for multi operations)
8193
# @yield the cache operation to trace
8294
# @return [Object] the result of the block
8395
# @raise [StandardError] re-raises any exception from the block
8496
#
8597
# @example Tracing a set operation
86-
# trace('set', { 'db.operation' => 'set', 'server.address' => 'localhost:11211' }) do
98+
# trace('set', { 'db.operation.name' => 'set', 'server.address' => 'localhost', 'server.port' => 11211 }) do
8799
# server.set(key, value, ttl)
88100
# end
89101
#
@@ -114,7 +126,7 @@ def trace(name, attributes = {})
114126
# @raise [StandardError] re-raises any exception from the block
115127
#
116128
# @example Recording hit/miss metrics after get_multi
117-
# trace_with_result('get_multi', { 'db.operation' => 'get_multi' }) do |span|
129+
# trace_with_result('get_multi', { 'db.operation.name' => 'get_multi' }) do |span|
118130
# results = fetch_from_cache(keys)
119131
# if span
120132
# span.set_attribute('db.memcached.hit_count', results.size)

0 commit comments

Comments
 (0)