Skip to content

HYPERFLEET-1490 - feat: add hyperfleet.resource_id span attribute to service layer - #328

Open
Ruclo wants to merge 1 commit into
openshift-hyperfleet:mainfrom
Ruclo:HYPERFLEET-1490
Open

HYPERFLEET-1490 - feat: add hyperfleet.resource_id span attribute to service layer#328
Ruclo wants to merge 1 commit into
openshift-hyperfleet:mainfrom
Ruclo:HYPERFLEET-1490

Conversation

@Ruclo

@Ruclo Ruclo commented Aug 6, 2026

Copy link
Copy Markdown

Summary

  • Set hyperfleet.resource_id and hyperfleet.resource_type span attributes on Get, Create, Patch, and Delete operations in pkg/services/resource.go
  • Enables resource-level trace correlation in Tempo — operators can query traces by resource ID to find the API request that created or mutated a specific cluster or nodepool
  • Add unit tests using tracetest.InMemoryExporter verifying span attributes are set for all 4 CRUD methods

Test plan

  • make lint passes (0 issues)
  • make test passes (1428 tests)
  • 4 new span attribute tests pass (Get, Create, Patch, Delete)

🤖 Generated with Claude Code

@openshift-ci
openshift-ci Bot requested review from pnguyen44 and sherine-k August 6, 2026 13:19
@openshift-ci

openshift-ci Bot commented Aug 6, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign rh-amarin for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Central YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Enterprise

Run ID: 470605be-1b72-42ad-8a7c-63daed8d80e6

📥 Commits

Reviewing files that changed from the base of the PR and between c746c38 and abeabd3.

📒 Files selected for processing (1)
  • pkg/services/resource.go
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • openshift-hyperfleet/architecture (manual)
  • openshift-hyperfleet/hyperfleet-api (manual)
  • openshift-hyperfleet/hyperfleet-sentinel (manual)
  • openshift-hyperfleet/hyperfleet-adapter (manual)
  • openshift-hyperfleet/hyperfleet-broker (manual)
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/services/resource.go

📝 Walkthrough

Summary by CodeRabbit

  • Observability

    • Resource operations now include resource IDs and types in OpenTelemetry traces, improving monitoring and troubleshooting.
  • Tests

    • Expanded coverage to verify tracing attributes across resource retrieval, creation, updates, deletion, ownership queries, forced deletion, and adapter status processing.

Walkthrough

The resource service now records hyperfleet.resource_id and hyperfleet.resource_type on OpenTelemetry spans. Coverage includes creation, retrieval, update, deletion, owner lookup, adapter status processing, and force deletion. Tests use an in-memory exporter and cover successful and failed operations.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: sherine-k

🚥 Pre-merge checks | ✅ 11
✅ Passed checks (11 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: adding the hyperfleet.resource_id span attribute to the service layer.
Description check ✅ Passed The description accurately covers the span attributes, affected operations, trace correlation use case, and test coverage.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Sec-02: Secrets In Log Output ✅ Passed No eligible log call includes a token, password, credential, or secret field/value; request headers are masked and DB strings are redacted. No CWE-532 finding.
No Hardcoded Secrets ✅ Passed The patch changes only two Go files. Added-line scans found no API keys, tokens, passwords, embedded URL credentials, private-key markers, or secret base64 strings; no CWE-798 finding.
No Weak Cryptography ✅ Passed PR changes only tracing and test code; no crypto/md5, crypto/des, crypto/rc4, SHA-1, ECB, custom crypto, or secret comparisons were found (CWE-327/CWE- timing risks absent).
No Injection Vectors ✅ Passed The production diff adds only OpenTelemetry attributes and validation. It adds no fmt.Sprintf query, command, YAML, template.HTML, or raw SQL sink; existing query formatting is unchanged.
No Privileged Containers ✅ Passed No CWE-250 finding: only Go files changed; Helm defaults use runAsNonRoot, UID 65532, dropped ALL capabilities, and allowPrivilegeEscalation false. Dockerfile root use is documented for make instal...
No Pii Or Sensitive Data In Logs ✅ Passed The PR adds only OpenTelemetry span attributes and tests; no slog, logr, zap, log, or fmt.Print* logging statements were added.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
✨ Simplify code
  • Create PR with simplified code

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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.

Inline comments:
In `@pkg/services/resource_test.go`:
- Around line 3160-3163: Update the t.Cleanup callback around tp.Shutdown to
check and report its error, and ensure all four ForceFlush results are checked
and reported rather than ignored. Use the test’s existing error-reporting
mechanism so incomplete span export or shutdown failures fail the test.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Central YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Enterprise

Run ID: 30542a9b-4b85-44d4-9812-6fa69da75e45

📥 Commits

Reviewing files that changed from the base of the PR and between cf39733 and 6117674.

📒 Files selected for processing (2)
  • pkg/services/resource.go
  • pkg/services/resource_test.go
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • openshift-hyperfleet/architecture (manual)
  • openshift-hyperfleet/hyperfleet-api (manual)
  • openshift-hyperfleet/hyperfleet-sentinel (manual)
  • openshift-hyperfleet/hyperfleet-adapter (manual)
  • openshift-hyperfleet/hyperfleet-broker (manual)

Comment thread pkg/services/resource_test.go
@hyperfleet-ci-bot

hyperfleet-ci-bot Bot commented Aug 6, 2026

Copy link
Copy Markdown

Risk Score: 1 — risk/low

Signal Detail Points
PR size 274 lines (>200) +1
Sensitive paths none +0
Test coverage Tests cover changed packages +0

Computed by hyperfleet-risk-scorer

@Ruclo
Ruclo force-pushed the HYPERFLEET-1490 branch from 6117674 to fbc5598 Compare August 6, 2026 13:31
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

Comment thread pkg/services/resource.go
Comment on lines +70 to +73
trace.SpanFromContext(ctx).SetAttributes(
attribute.String("hyperfleet.resource_id", id),
attribute.String("hyperfleet.resource_type", kind),
)

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.

hyperfleet.resource_type is set to the Kind (e.g. "Cluster") here and at the other three call sites, but the Tracing Standard and Sentinel's existing spans use the plural form (e.g. "clusters"). This mismatch means a TraceQL query on resource_type won't match across API and Sentinel spans for the same resource. Consider using the descriptor's plural form instead.

Comment on lines +3151 to +3166
func setupTestTracer(t *testing.T) (*sdktrace.TracerProvider, *tracetest.InMemoryExporter) {
t.Helper()
exporter := tracetest.NewInMemoryExporter()
tp := sdktrace.NewTracerProvider(
sdktrace.WithSampler(sdktrace.AlwaysSample()),
sdktrace.WithSyncer(exporter),
)
prev := otel.GetTracerProvider()
otel.SetTracerProvider(tp)
t.Cleanup(func() {
if err := tp.Shutdown(context.Background()); err != nil {
t.Errorf("failed to shutdown tracer: %v", err)
}
otel.SetTracerProvider(prev)
})
return tp, exporter

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.

setupTestTracer swaps the global otel TracerProvider for the test, then each test starts its span via the global otel.Tracer rather than the tp returned by this helper. Using tp.Tracer(...) directly would avoid touching process-global state, which would remove a potential source of flakiness if t.Parallel() is ever added to these tests.

@pnguyen44

pnguyen44 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

A few service methods aren't instrumented with these span attributes and would miss tracing coverage:

  • GetByID — used by GET /resources/{id}, ForceDelete, ListStatuses, and CreateStatus
  • ProcessAdapterStatus — used when adapters report status back to the API

Comment thread pkg/services/resource_test.go Outdated
return "", false
}

func TestResourceService_Get_SetsSpanAttributes(t *testing.T) {

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.

None of the four new tests exercise the no-op tracing path, even though "no-op safe" is called out in the acceptance criteria. Worth adding a test that calls one of these methods without an active tracer installed.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Every other test that calls one of the methods and doesn't set up the tracer makes sure of this already

Comment thread pkg/services/resource_test.go Outdated
return "", false
}

func TestResourceService_Get_SetsSpanAttributes(t *testing.T) {

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.

These tests only cover the happy path. Get/Patch/Delete tag the span with resource_id/resource_type even on a failed lookup (attributes are set before the DAO call), which is useful for debugging 404s, but that behavior isn't verified by a test.

Comment thread pkg/services/resource.go Outdated
Comment on lines +122 to +128
if err != nil {
return nil, handleCreateError(kind, err)
}
trace.SpanFromContext(ctx).SetAttributes(
attribute.String("hyperfleet.resource_id", resource.ID),
attribute.String("hyperfleet.resource_type", kind),
)

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.

Create sets span attributes after the DAO call (since resource.ID isn't known until insert succeeds), so a failed create gets no resource_id/resource_type on the span at all. This differs from Get/Patch/Delete, which do tag failed lookups. resource_type (kind) is known upfront here though, so it could be set before the DAO call. Not covered by any test either way.

Comment thread pkg/services/resource_test.go Outdated
Comment on lines +3180 to +3296
func TestResourceService_Get_SetsSpanAttributes(t *testing.T) {
RegisterTestingT(t)
setupTestDescriptors()

tp, exporter := setupTestTracer(t)

mockDao := newMockResourceDao()
svc, _, _ := newTestResourceService(mockDao)
mockDao.addResource(testResource("Channel", "ch-1", "stable"))

ctx, span := otel.Tracer("test").Start(context.Background(), "test")
_, svcErr := svc.Get(ctx, "Channel", "ch-1")
span.End()
Expect(svcErr).To(BeNil())

if err := tp.ForceFlush(context.Background()); err != nil {
t.Fatalf("failed to flush spans: %v", err)
}
spans := exporter.GetSpans()

resourceID, found := findSpanAttribute(spans, "hyperfleet.resource_id")
Expect(found).To(BeTrue(), "hyperfleet.resource_id attribute not found")
Expect(resourceID).To(Equal("ch-1"))

resourceType, found := findSpanAttribute(spans, "hyperfleet.resource_type")
Expect(found).To(BeTrue(), "hyperfleet.resource_type attribute not found")
Expect(resourceType).To(Equal("Channel"))
}

func TestResourceService_Create_SetsSpanAttributes(t *testing.T) {
RegisterTestingT(t)
setupTestDescriptors()

tp, exporter := setupTestTracer(t)

mockDao := newMockResourceDao()
svc, _, _ := newTestResourceService(mockDao)
resource := testResource("Channel", "ch-new", "beta")

ctx, span := otel.Tracer("test").Start(context.Background(), "test")
result, svcErr := svc.Create(ctx, "Channel", resource, nil)
span.End()
Expect(svcErr).To(BeNil())

if err := tp.ForceFlush(context.Background()); err != nil {
t.Fatalf("failed to flush spans: %v", err)
}
spans := exporter.GetSpans()

resourceID, found := findSpanAttribute(spans, "hyperfleet.resource_id")
Expect(found).To(BeTrue(), "hyperfleet.resource_id attribute not found")
Expect(resourceID).To(Equal(result.ID))
Expect(resourceID).ToNot(BeEmpty(), "resource_id should not be empty")

resourceType, found := findSpanAttribute(spans, "hyperfleet.resource_type")
Expect(found).To(BeTrue(), "hyperfleet.resource_type attribute not found")
Expect(resourceType).To(Equal("Channel"))
}

func TestResourceService_Patch_SetsSpanAttributes(t *testing.T) {
RegisterTestingT(t)
setupTestDescriptors()

tp, exporter := setupTestTracer(t)

mockDao := newMockResourceDao()
svc, _, _ := newTestResourceService(mockDao)
mockDao.addResource(testResource("Channel", "ch-1", "stable"))

patch := &api.ResourcePatch{Spec: map[string]interface{}{"key": "updated"}}
ctx, span := otel.Tracer("test").Start(context.Background(), "test")
_, svcErr := svc.Patch(ctx, "Channel", "ch-1", patch)
span.End()
Expect(svcErr).To(BeNil())

if err := tp.ForceFlush(context.Background()); err != nil {
t.Fatalf("failed to flush spans: %v", err)
}
spans := exporter.GetSpans()

resourceID, found := findSpanAttribute(spans, "hyperfleet.resource_id")
Expect(found).To(BeTrue(), "hyperfleet.resource_id attribute not found")
Expect(resourceID).To(Equal("ch-1"))

resourceType, found := findSpanAttribute(spans, "hyperfleet.resource_type")
Expect(found).To(BeTrue(), "hyperfleet.resource_type attribute not found")
Expect(resourceType).To(Equal("Channel"))
}

func TestResourceService_Delete_SetsSpanAttributes(t *testing.T) {
RegisterTestingT(t)
setupTestDescriptors()

tp, exporter := setupTestTracer(t)

mockDao := newMockResourceDao()
svc, _, _ := newTestResourceService(mockDao)
mockDao.addResource(testResource("Channel", "ch-1", "stable"))

ctx, span := otel.Tracer("test").Start(context.Background(), "test")
_, svcErr := svc.Delete(ctx, "Channel", "ch-1")
span.End()
Expect(svcErr).To(BeNil())

if err := tp.ForceFlush(context.Background()); err != nil {
t.Fatalf("failed to flush spans: %v", err)
}
spans := exporter.GetSpans()

resourceID, found := findSpanAttribute(spans, "hyperfleet.resource_id")
Expect(found).To(BeTrue(), "hyperfleet.resource_id attribute not found")
Expect(resourceID).To(Equal("ch-1"))

resourceType, found := findSpanAttribute(spans, "hyperfleet.resource_type")
Expect(found).To(BeTrue(), "hyperfleet.resource_type attribute not found")
Expect(resourceType).To(Equal("Channel"))
}

@pnguyen44 pnguyen44 Aug 6, 2026

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.

These four tests share the same setup/flush/assert scaffolding; only the method under test (and a couple minor per-case details) really changes. pkg/middleware/otel_test.go already shows a table-driven pattern for span tests in this repo, worth following the same shape here to cut the duplication.

@Ruclo
Ruclo force-pushed the HYPERFLEET-1490 branch from fbc5598 to c746c38 Compare August 7, 2026 10:21
…service layer

Set hyperfleet.resource_id and hyperfleet.resource_type span attributes
on Get, Create, Patch, and Delete operations for resource-level trace
correlation in Tempo.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@Ruclo
Ruclo force-pushed the HYPERFLEET-1490 branch from c746c38 to abeabd3 Compare August 7, 2026 10:56
@Ruclo
Ruclo requested a review from pnguyen44 August 7, 2026 11:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants