Skip to content

perf: cache static span attributes in Trilogy instrumentation#2089

Open
schlubbi wants to merge 3 commits intoopen-telemetry:mainfrom
schlubbi:optimize-trilogy-instrumentation
Open

perf: cache static span attributes in Trilogy instrumentation#2089
schlubbi wants to merge 3 commits intoopen-telemetry:mainfrom
schlubbi:optimize-trilogy-instrumentation

Conversation

@schlubbi
Copy link

Summary

The Trilogy instrumentation rebuilds a hash of static span attributes on every query() call. Attributes like db.system, net.peer.name, db.name, db.user, and peer_service never change after connection initialization, yet a new hash is allocated, populated, and then merged on every query. This PR caches these static attributes at initialization time and uses Hash#dup on the hot path.

Changes

1. Cache base attributes at initialization

A new populate_base_attributes method runs during initialize and builds a frozen hash of static attributes (db.system, net.peer.name, db.name, db.user, peer_service). On each query() call, client_attributes now calls @_otel_base_attributes.dup instead of constructing a new hash from scratch.

2. Cache database_name and database_user as instance variables

Previously, database_name and database_user performed a hash lookup on connection_options on every call. These values are now cached as @_otel_database_name and @_otel_database_user during initialization.

3. Single Trilogy.attributes lookup per query

The query() method previously called OpenTelemetry::Instrumentation::Trilogy.attributes twice — once to extract DB_OPERATION for span naming, and again to merge into span attributes. It now reads the context attributes once into a local variable and passes it to both uses.

Dynamic attributes preserved

  • db.instance.id (@connected_host) is added per-call since it can change after connection (e.g., after failover)
  • db.statement is inherently per-query
  • Trilogy.attributes (context propagation) is per-call

Benchmarks

client_attributes micro-benchmark

Measured with Process.clock_gettime over 500k iterations. Test setup: Trilogy.allocate with typical connection options (host, database, username), db_statement: :omit config.

Method Before After Speedup
client_attributes (no sql) 864 ns 163 ns 5.3×
client_attributes (with sql, omit) 946 ns 235 ns 4.0×
database_name 318 ns 58 ns 5.5×
database_user 193 ns 59 ns 3.3×
Hash construction comparison

Comparing strategies for building the per-query attributes hash:

Strategy Cost
Hash literal (4 keys) — current approach 186 ns
Hash#dup from frozen cached hash — new approach 104 ns

The cached dup approach is ~45% faster for hash construction alone, but the real win comes from eliminating the repeated method calls (database_name, database_user, config[:peer_service], connection_options.fetch) that populated the hash on every call.

Test coverage

  • All 59 existing integration tests pass (tested against MySQL 8.0)
  • Added new unit test file (patches/client_attributes_test.rb) with 19 tests covering:
    • All static attribute keys (db.system, net.peer.name, db.name, db.user, peer_service, db.instance.id)
    • Nil/missing connection options (unknown sock fallback, omitted keys)
    • Hash independence (each call returns a distinct hash)
    • SQL statement handling (include, omit, obfuscate modes)
    • database_name and database_user accessor behavior

Cache base span attributes (db.system, net.peer.name, db.name, db.user,
peer_service) at connection initialization rather than rebuilding a new
hash on every query. Use Hash#dup from the frozen cached hash instead
of constructing from scratch each time.

Also cache database_name and database_user as instance variables, and
read Trilogy.attributes once per query() call instead of twice.

Benchmarked improvements:
- client_attributes (no sql):  864ns → 163ns (5.3×)
- client_attributes (with sql): 946ns → 235ns (4.0×)
- database_name: 318ns → 58ns (5.5×)
- database_user: 193ns → 59ns (3.3×)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@linux-foundation-easycla
Copy link

linux-foundation-easycla bot commented Mar 16, 2026

CLA Signed

The committers listed above are authorized under a signed CLA.

- Align hash arguments per Layout/HashAlignment
- Remove trailing commas per Style/TrailingCommaInHashLiteral
- Use single-quoted strings per Style/StringLiterals

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@@ -92,11 +103,11 @@ def client_attributes(sql = nil)
end

def database_name
Copy link
Contributor

Choose a reason for hiding this comment

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

This question applies to database_name and `database_user.

Do we need these helper methods anymore since you are referencing them using direct instance variable names elsewhere?

Comment on lines +81 to +82
attributes[::OpenTelemetry::SemanticConventions::Trace::DB_NAME] = @_otel_database_name if @_otel_database_name
attributes[::OpenTelemetry::SemanticConventions::Trace::DB_USER] = @_otel_database_user if @_otel_database_user
Copy link
Contributor

Choose a reason for hiding this comment

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

Now that I am looking at this a bit closer, why did we treat the DB name and user separately from the net.peer.name attribute before this change 🤔?

Do we use the ivars for any other reason than to populate these attributes?

private

def client_attributes(sql = nil)
def populate_base_attributes
Copy link
Contributor

Choose a reason for hiding this comment

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

populate an build are only called together in the constructor right?

Later on the code only uses the ivar https://github.com/open-telemetry/opentelemetry-ruby-contrib/pull/2089/changes#diff-6ba5c9beb3eb94883042f29a069003cab7f6296395e612e75b0abe00c98abbb3R88

Can we inline this all and only keep the has around once with all of the static attributes in it?

- Inline populate_base_attributes into initialize
- Merge build_base_attributes into _build_otel_base_attributes
- Remove database_name and database_user accessor methods
- Use local variable for database_user in builder (no ivar needed)
- Keep @_otel_database_name as ivar (needed by query() for span naming)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants