Skip to content

feat(telemetry): BusinessMetrics — transparently tenant-scoped instruments#669

Merged
pitabwire merged 1 commit into
mainfrom
feat/tenant-scoped-business-metrics
Jun 10, 2026
Merged

feat(telemetry): BusinessMetrics — transparently tenant-scoped instruments#669
pitabwire merged 1 commit into
mainfrom
feat/tenant-scoped-business-metrics

Conversation

@pitabwire

Copy link
Copy Markdown
Owner

Standard factory for business metrics where tenant_id/partition_id attach automatically from the security claims context on every Add/Record. Foundation for the fleet-wide analytics standardization. Test proves: tenant attrs present with claims, absent without, explicit attrs preserved.

…ments

The standard factory for product/business metrics. Counter/FloatCounter/
Histogram/Gauge/FloatGauge wrappers merge TenantAttributes(ctx) into
every measurement, so call sites cannot forget tenant attribution —
the existing opt-in WithTenantAttributes pattern proved forgettable
(23 fintech counters shipped tenant-less). Claim-less system paths
record without tenant attributes; explicit per-call attributes are
preserved. Instrument-creation errors fall back to noop instruments
(metrics never break the service), matching LatencyMeasure behaviour.

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request introduces a new BusinessMetrics factory in telemetry/business.go along with corresponding tests in telemetry/business_test.go. The factory transparently attaches tenant and partition attributes from the context to various metric types (Counter, FloatCounter, Histogram, Gauge, FloatGauge). The review feedback suggests optimizing the merged function to avoid unnecessary slice allocations when either tenant or explicit attributes are empty. Additionally, it recommends expanding the test suite to fully cover and verify the newly introduced Gauge and FloatGauge metrics.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread telemetry/business.go
Comment on lines +51 to +60
func merged(ctx context.Context, attrs []attribute.KeyValue) metric.MeasurementOption {
tenant := TenantAttributes(ctx)
if len(attrs) == 0 && len(tenant) == 0 {
return metric.WithAttributes()
}
all := make([]attribute.KeyValue, 0, len(tenant)+len(attrs))
all = append(all, tenant...)
all = append(all, attrs...)
return metric.WithAttributes(all...)
}

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.

medium

On high-throughput hot paths, minimizing slice allocations is crucial for performance. The current implementation of merged always allocates a new slice all when either tenant or attrs is non-empty, even if one of them is completely empty.

We can optimize this by returning metric.WithAttributes directly with the non-empty slice when the other is empty, avoiding any new slice allocation in those very common scenarios.

func merged(ctx context.Context, attrs []attribute.KeyValue) metric.MeasurementOption {
	tenant := TenantAttributes(ctx)
	if len(tenant) == 0 {
		if len(attrs) == 0 {
			return metric.WithAttributes()
		}
		return metric.WithAttributes(attrs...)
	}
	if len(attrs) == 0 {
		return metric.WithAttributes(tenant...)
	}
	all := make([]attribute.KeyValue, 0, len(tenant)+len(attrs))
	all = append(all, tenant...)
	all = append(all, attrs...)
	return metric.WithAttributes(all...)
}

Comment on lines +44 to +67
func dataPointAttrs(data metricdata.Aggregation) []attribute.Set {
switch d := data.(type) {
case metricdata.Sum[int64]:
out := make([]attribute.Set, 0, len(d.DataPoints))
for _, dp := range d.DataPoints {
out = append(out, dp.Attributes)
}
return out
case metricdata.Sum[float64]:
out := make([]attribute.Set, 0, len(d.DataPoints))
for _, dp := range d.DataPoints {
out = append(out, dp.Attributes)
}
return out
case metricdata.Histogram[float64]:
out := make([]attribute.Set, 0, len(d.DataPoints))
for _, dp := range d.DataPoints {
out = append(out, dp.Attributes)
}
return out
default:
return nil
}
}

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.

medium

The helper function dataPointAttrs currently only handles Sum and Histogram aggregations. It does not support Gauge aggregations, which means any future tests for Gauge or FloatGauge would return nil attributes and fail to verify tenant-scoping.

Adding support for metricdata.Gauge[int64] and metricdata.Gauge[float64] makes this helper complete and robust.

func dataPointAttrs(data metricdata.Aggregation) []attribute.Set {
	switch d := data.(type) {
	case metricdata.Sum[int64]:
		out := make([]attribute.Set, 0, len(d.DataPoints))
		for _, dp := range d.DataPoints {
			out = append(out, dp.Attributes)
		}
		return out
	case metricdata.Sum[float64]:
		out := make([]attribute.Set, 0, len(d.DataPoints))
		for _, dp := range d.DataPoints {
			out = append(out, dp.Attributes)
		}
		return out
	case metricdata.Histogram[float64]:
		out := make([]attribute.Set, 0, len(d.DataPoints))
		for _, dp := range d.DataPoints {
			out = append(out, dp.Attributes)
		}
		return out
	case metricdata.Gauge[int64]:
		out := make([]attribute.Set, 0, len(d.DataPoints))
		for _, dp := range d.DataPoints {
			out = append(out, dp.Attributes)
		}
		return out
	case metricdata.Gauge[float64]:
		out := make([]attribute.Set, 0, len(d.DataPoints))
		for _, dp := range d.DataPoints {
			out = append(out, dp.Attributes)
		}
		return out
	default:
		return nil
	}
}

Comment on lines +92 to +100
bm := telemetry.NewBusinessMetrics("biz-test")
created := bm.Counter("things_created_total", "things created")
amount := bm.FloatCounter("things_amount_total", "amount of things")
dur := bm.Histogram("things_duration_ms", "thing duration")

ctxA := tenantContext("tenant-a", "part-a")
created.Add(ctxA, 1, attribute.String("kind", "gold"))
amount.Add(ctxA, 12.5)
dur.Record(ctxA, 42.0)

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.

medium

The test currently only verifies Counter, FloatCounter, and Histogram. It should also verify that Gauge and FloatGauge are correctly created and transparently carry the tenant attributes.

Suggested change
bm := telemetry.NewBusinessMetrics("biz-test")
created := bm.Counter("things_created_total", "things created")
amount := bm.FloatCounter("things_amount_total", "amount of things")
dur := bm.Histogram("things_duration_ms", "thing duration")
ctxA := tenantContext("tenant-a", "part-a")
created.Add(ctxA, 1, attribute.String("kind", "gold"))
amount.Add(ctxA, 12.5)
dur.Record(ctxA, 42.0)
bm := telemetry.NewBusinessMetrics("biz-test")
created := bm.Counter("things_created_total", "things created")
amount := bm.FloatCounter("things_amount_total", "amount of things")
dur := bm.Histogram("things_duration_ms", "thing duration")
gauge := bm.Gauge("things_gauge", "gauge of things")
fgauge := bm.FloatGauge("things_fgauge", "float gauge of things")
ctxA := tenantContext("tenant-a", "part-a")
created.Add(ctxA, 1, attribute.String("kind", "gold"))
amount.Add(ctxA, 12.5)
dur.Record(ctxA, 42.0)
gauge.Record(ctxA, 100)
fgauge.Record(ctxA, 100.5)

Comment on lines +128 to +134
for _, name := range []string{"things_amount_total", "things_duration_ms"} {
s := attrSets(rm, name)
require.NotEmpty(t, s, name)
v, ok := s[0].Value("tenant_id")
require.True(t, ok, name+" must carry tenant_id")
require.Equal(t, "tenant-a", v.AsString())
}

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.

medium

Include the newly added things_gauge and things_fgauge in the verification loop to ensure they correctly carry the tenant attributes.

Suggested change
for _, name := range []string{"things_amount_total", "things_duration_ms"} {
s := attrSets(rm, name)
require.NotEmpty(t, s, name)
v, ok := s[0].Value("tenant_id")
require.True(t, ok, name+" must carry tenant_id")
require.Equal(t, "tenant-a", v.AsString())
}
for _, name := range []string{"things_amount_total", "things_duration_ms", "things_gauge", "things_fgauge"} {
s := attrSets(rm, name)
require.NotEmpty(t, s, name)
v, ok := s[0].Value("tenant_id")
require.True(t, ok, name+" must carry tenant_id")
require.Equal(t, "tenant-a", v.AsString())
}

@pitabwire pitabwire merged commit 1d965fe into main Jun 10, 2026
12 checks passed
@pitabwire pitabwire deleted the feat/tenant-scoped-business-metrics branch June 10, 2026 20:27
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.

1 participant