Skip to content

feat(tracing): add accounts attribute to single-account controller spans#1360

Open
sylr wants to merge 1 commit into
mainfrom
feat/trace-account-attribute
Open

feat(tracing): add accounts attribute to single-account controller spans#1360
sylr wants to merge 1 commit into
mainfrom
feat/trace-account-attribute

Conversation

@sylr

@sylr sylr commented May 30, 2026

Copy link
Copy Markdown
Contributor

What

Adds an accounts OpenTelemetry span attribute to the controller trace spans that operate on accounts, so traces can be filtered/grouped by account in Signoz.

Covered operations:

  • GetAccount, GetAggregatedBalances, GetVolumesWithBalances — accounts extracted from the query builder ($match / $in on address / account).
  • SaveAccountMetadata, DeleteAccountMetadata — account taken from the explicit Input.Address.

The value is emitted under the same accounts StringSlice key already used by the bulk storage operations (storage/ledger/accounts.go), so one query (accounts CONTAINS "users:001") surfaces every span involving an account — single-account or bulk — without type collisions.

How

  • accountsFromQuery(query.Builder) []string walks the query expression tree, collecting concrete account addresses from $match and $in filters (pattern operators like $like are ignored), then dedupes (slices.Sort + slices.Compact).
  • setAccountsAttribute(ctx, accounts) guards against empty slices and sets the accounts StringSlice attribute on the current span.
  • common.ResourceQueryFromPaginatedQuery[…] extracts the underlying ResourceQuery (and thus the Builder) from any PaginatedQuery strategy (handles both value and pointer forms).

Tests

  • TestAccountsFromQuery — single/multiple matches, $in, alias dedup+sort, non-string values, $like exclusion, nil builder.
  • TestResourceQueryFromPaginatedQuery — extraction across Initial / Offset / Column pagination.
  • go build ./internal/..., go vet, and the controller/common test suites pass.

Known limitations (intentional, best-effort)

  • $not negation: query.Builder.Walk exposes no negation context, so a $not-wrapped address filter would still be tagged. Acceptable for best-effort observability tagging; flagged here for reviewers.
  • List/aggregate ops: on GetVolumesWithBalances / GetAggregatedBalances an address filter can be a segment prefix (e.g. users:) rather than a concrete account, so the attribute reflects the filter, not necessarily exact accounts.

🤖 Generated with Claude Code

Tag controller trace spans that operate on accounts (GetAccount,
GetAggregatedBalances, GetVolumesWithBalances, SaveAccountMetadata,
DeleteAccountMetadata) with an `accounts` span attribute. The same
StringSlice key already emitted by bulk storage operations is reused, so
a single query can surface every span involving a given account.

Accounts are extracted from the query builder ($match/$in on
address/account) for query-based methods and from the explicit address
for metadata methods, then deduplicated.

Constraint: query.Builder.Walk exposes no negation context, so $not filters are best-effort and may tag an excluded account
Rejected: GetResourceQuery() interface method | rippled into shared pagination plumbing and surfaced a latent type bug in Iterate
Confidence: high
Scope-risk: narrow
Not-tested: $not-negated address filters; partial-segment address filters on list/aggregate ops

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sylr sylr requested a review from a team as a code owner May 30, 2026 10:12
@coderabbitai

coderabbitai Bot commented May 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This PR enriches distributed traces with account context by introducing account extraction helpers and integrating them into five ledger controller methods. accountsFromQuery walks query builder expressions to extract concrete account addresses, while setAccountsAttribute emits those accounts to OpenTelemetry spans. A new pagination utility extracts resource queries from paginated query types, enabling account tracking in paginated operations. Tests validate the extraction logic and pagination conversion.

Changes

Account-based span enrichment

Layer / File(s) Summary
Span enrichment helpers and imports
internal/controller/ledger/controller_with_traces.go
Adds imports for slices and OpenTelemetry attribute, introduces accountsFromQuery to extract account addresses from address/account filters in query builders (supporting $match and $in operators, with sorting and deduplication), and adds setAccountsAttribute to emit accounts as a span attribute when non-empty.
Pagination query extraction utility
internal/storage/common/pagination.go
Adds ResourceQueryFromPaginatedQuery helper to extract the underlying ResourceQuery from any supported PaginatedQuery variant (InitialPaginatedQuery, OffsetPaginatedQuery, ColumnPaginatedQuery in both value and pointer forms), returning the extracted query and a success boolean.
Query-based controller method instrumentation
internal/controller/ledger/controller_with_traces.go
Instruments GetAccount and GetAggregatedBalances to set span accounts from their query builders, and GetVolumesWithBalances to extract a resource query from the paginated query and set accounts from that resource query's builder.
Direct account controller method instrumentation
internal/controller/ledger/controller_with_traces.go
Instruments SaveAccountMetadata and DeleteAccountMetadata to set span accounts directly from the parameters' Input.Address field.
Tests for span enrichment helpers and integration
internal/controller/ledger/controller_with_traces_test.go
Adds TestAccountsFromQuery covering nil builders, single/multiple address matches, combined AND/OR filters, IN queries, alias deduplication, and ignored pattern operators; adds TestResourceQueryFromPaginatedQuery covering initial/offset/column pagination variants.

🎯 3 (Moderate) | ⏱️ ~25 minutes

A rabbit traces through the ledger's silent night,
Setting accounts aglow in telemetry's light,
Each query and filter, now visible and bright,
Helping observers see the account's true sight! 📊✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: adding an accounts attribute to controller spans for tracing purposes.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, explaining what was added, how it works, test coverage, and known limitations.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/trace-account-attribute

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov

codecov Bot commented May 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 79.16667% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.66%. Comparing base (5affb55) to head (cd658f2).

Files with missing lines Patch % Lines
internal/storage/common/pagination.go 50.00% 8 Missing ⚠️
...ternal/controller/ledger/controller_with_traces.go 93.75% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1360      +/-   ##
==========================================
+ Coverage   79.53%   80.66%   +1.13%     
==========================================
  Files         206      206              
  Lines       11350    11217     -133     
==========================================
+ Hits         9027     9048      +21     
+ Misses       1779     1622     -157     
- Partials      544      547       +3     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
internal/controller/ledger/controller_with_traces_test.go (2)

54-58: 💤 Low value

Consider also covering the $in []string branch.

accountsFromQuery has separate handling for $in values of type []string and []any, but only the []any shape is exercised here. A query.In("address", []string{...}) case would close that gap.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/controller/ledger/controller_with_traces_test.go` around lines 54 -
58, Add a unit test that calls accountsFromQuery via the existing test harness
to exercise the []string-$in branch: add a new test case in the same table
(similar to the "in operator with several accounts" entry) using
query.In("address", []string{"users:001", "users:002"}) and assert
expectedAccounts == []string{"users:001", "users:002"} so the accountsFromQuery
code path for []string is covered; locate the table of tests in the
controller_with_traces_test.go file and mirror the existing []any test but pass
a []string value using query.In.

88-124: 💤 Low value

Coverage gap: pointer variants and the false path are untested.

ResourceQueryFromPaginatedQuery also handles pointer forms (*InitialPaginatedQuery, *OffsetPaginatedQuery, *ColumnPaginatedQuery) and returns (zero, false) for unrecognized inputs. Only value forms are covered here, so the pointer branches and the ok == false contract aren't validated.

♻️ Example additional cases
{
	name: "initial pointer",
	query: &common.InitialPaginatedQuery[ledger.GetVolumesOptions]{
		Options: common.ResourceQuery[ledger.GetVolumesOptions]{Builder: builder},
	},
},
// ...plus a nil/unsupported case asserting ok == false
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/controller/ledger/controller_with_traces_test.go` around lines 88 -
124, Add tests covering the pointer variants and the false-return path for
ResourceQueryFromPaginatedQuery: extend the table in
controller_with_traces_test.go to include entries using pointer types
(&common.InitialPaginatedQuery, &common.OffsetPaginatedQuery,
&common.ColumnPaginatedQuery) that wrap the same ResourceQuery{Builder: builder}
and assert the returned rq matches the builder and ok==true, and add an
unsupported/nil case (e.g. a nil interface or another type) asserting ok==false
and rq equals the zero-value ResourceQuery; reference
ResourceQueryFromPaginatedQuery, InitialPaginatedQuery, OffsetPaginatedQuery,
ColumnPaginatedQuery and use require.True/require.False and require.Equal to
validate results.
internal/storage/common/pagination.go (1)

45-58: ⚡ Quick win

Defensive handling for typed-nil paginated queries

ResourceQueryFromPaginatedQuery will panic if the PaginatedQuery interface contains a typed-nil pointer (e.g. (*InitialPaginatedQuery[...])(nil)), because the pointer type-switch branches dereference v.Options. Current pagination wiring (storagecommon.Extract/UnmarshalCursor used by getPaginatedQuery) returns value-form queries, so existing GetVolumesWithBalances paths won’t trigger this, but guarding the pointer branches keeps the (ResourceQuery[...]{}, false) contract for future callers.

Suggested fix
 func ResourceQueryFromPaginatedQuery[OptionsType any](q PaginatedQuery[OptionsType]) (ResourceQuery[OptionsType], bool) {
 	switch v := any(q).(type) {
 	case InitialPaginatedQuery[OptionsType]:
 		return v.Options, true
 	case *InitialPaginatedQuery[OptionsType]:
+		if v == nil {
+			return ResourceQuery[OptionsType]{}, false
+		}
 		return v.Options, true
 	case OffsetPaginatedQuery[OptionsType]:
 		return v.Options, true
 	case *OffsetPaginatedQuery[OptionsType]:
+		if v == nil {
+			return ResourceQuery[OptionsType]{}, false
+		}
 		return v.Options, true
 	case ColumnPaginatedQuery[OptionsType]:
 		return v.Options, true
 	case *ColumnPaginatedQuery[OptionsType]:
+		if v == nil {
+			return ResourceQuery[OptionsType]{}, false
+		}
 		return v.Options, true
 	default:
 		return ResourceQuery[OptionsType]{}, false
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/storage/common/pagination.go` around lines 45 - 58,
ResourceQueryFromPaginatedQuery currently dereferences v.Options in the pointer
type-switch branches and will panic on typed-nil pointers (e.g.
(*InitialPaginatedQuery[...])(nil)); update the pointer cases for
InitialPaginatedQuery, OffsetPaginatedQuery, and ColumnPaginatedQuery so they
first check if v == nil and, if so, return the zero ResourceQuery and false,
otherwise return v.Options, true — keep the existing value-type branches
unchanged and ensure the function preserves the (ResourceQuery[...]{}, false)
contract for nil pointers.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@internal/controller/ledger/controller_with_traces_test.go`:
- Around line 54-58: Add a unit test that calls accountsFromQuery via the
existing test harness to exercise the []string-$in branch: add a new test case
in the same table (similar to the "in operator with several accounts" entry)
using query.In("address", []string{"users:001", "users:002"}) and assert
expectedAccounts == []string{"users:001", "users:002"} so the accountsFromQuery
code path for []string is covered; locate the table of tests in the
controller_with_traces_test.go file and mirror the existing []any test but pass
a []string value using query.In.
- Around line 88-124: Add tests covering the pointer variants and the
false-return path for ResourceQueryFromPaginatedQuery: extend the table in
controller_with_traces_test.go to include entries using pointer types
(&common.InitialPaginatedQuery, &common.OffsetPaginatedQuery,
&common.ColumnPaginatedQuery) that wrap the same ResourceQuery{Builder: builder}
and assert the returned rq matches the builder and ok==true, and add an
unsupported/nil case (e.g. a nil interface or another type) asserting ok==false
and rq equals the zero-value ResourceQuery; reference
ResourceQueryFromPaginatedQuery, InitialPaginatedQuery, OffsetPaginatedQuery,
ColumnPaginatedQuery and use require.True/require.False and require.Equal to
validate results.

In `@internal/storage/common/pagination.go`:
- Around line 45-58: ResourceQueryFromPaginatedQuery currently dereferences
v.Options in the pointer type-switch branches and will panic on typed-nil
pointers (e.g. (*InitialPaginatedQuery[...])(nil)); update the pointer cases for
InitialPaginatedQuery, OffsetPaginatedQuery, and ColumnPaginatedQuery so they
first check if v == nil and, if so, return the zero ResourceQuery and false,
otherwise return v.Options, true — keep the existing value-type branches
unchanged and ensure the function preserves the (ResourceQuery[...]{}, false)
contract for nil pointers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d6d93743-8cd7-48fe-8214-6fbcd074adc2

📥 Commits

Reviewing files that changed from the base of the PR and between 5affb55 and cd658f2.

📒 Files selected for processing (3)
  • internal/controller/ledger/controller_with_traces.go
  • internal/controller/ledger/controller_with_traces_test.go
  • internal/storage/common/pagination.go

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants