Skip to content

Commit b6370eb

Browse files
yosiharanclaude
andauthored
feat(metrics): add instrumentation for Check method (#430)
Extends the existing metrics pattern (already covering WhoCanAccess and WhatCanTargetAccess) to the Check method, recording cache hit/miss, candidates sent to SDK, and result size per call. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 899b371 commit b6370eb

File tree

3 files changed

+98
-0
lines changed

3 files changed

+98
-0
lines changed

internal/services/authz.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ func (a *authzCache) DeleteFGARelations(ctx context.Context, relations []*descop
102102
}
103103

104104
func (a *authzCache) Check(ctx context.Context, relations []*descope.FGARelation) ([]*descope.FGACheck, error) {
105+
start := time.Now()
105106
// get cache and mgmt sdk
106107
projectCache, mgmtSDK, err := a.getOrCreateProjectCache(ctx)
107108
if err != nil {
@@ -111,6 +112,8 @@ func (a *authzCache) Check(ctx context.Context, relations []*descope.FGARelation
111112
cachedChecks, toCheckViaSDK, indexToCachedChecks := projectCache.CheckRelations(ctx, relations)
112113
// if all relations were found in cache, return
113114
if len(toCheckViaSDK) == 0 {
115+
// candidatesCount = 0 (nothing sent to SDK); filteredCount = 0 (Check doesn't filter, every relation gets an answer)
116+
a.recordMetric(ctx, metrics.APICheck, true, 0, 0, len(cachedChecks), start)
114117
return cachedChecks, nil
115118
}
116119
// fetch missing relations from sdk
@@ -131,6 +134,8 @@ func (a *authzCache) Check(ctx context.Context, relations []*descope.FGARelation
131134
j++
132135
}
133136
}
137+
// candidatesCount = relations sent to SDK (not in cache); filteredCount = 0 (Check doesn't filter, every relation gets an answer)
138+
a.recordMetric(ctx, metrics.APICheck, false, len(toCheckViaSDK), 0, len(result), start)
134139
return result, nil
135140
}
136141

internal/services/authz_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,98 @@ func TestWhatCanTargetAccess_MetricsRecorded_CacheHit(t *testing.T) {
661661
require.Equal(t, int64(2), agg.SumResultSize)
662662
}
663663

664+
func TestCheck_MetricsRecorded_FullCacheHit(t *testing.T) {
665+
ac, mockSDK, mockCache, collector := injectAuthzMocksWithCollector(t)
666+
ctx := cctx.AddProjectID(context.TODO(), "proj1")
667+
relations := []*descope.FGARelation{
668+
{Resource: "mario", Target: "luigi", Relation: "bigBro"},
669+
{Resource: "luigi", Target: "mario", Relation: "bigBro"},
670+
}
671+
mockSDK.MockFGA.CheckAssert = func(_ []*descope.FGARelation) {
672+
require.Fail(t, "should not be called on full cache hit")
673+
}
674+
mockCache.CheckRelationFunc = func(_ context.Context, _ *descope.FGARelation) (bool, bool, bool) {
675+
return true, true, true
676+
}
677+
mockCache.UpdateCacheWithChecksFunc = func(_ context.Context, _ []*descope.FGACheck) {
678+
require.Fail(t, "should not be called on full cache hit")
679+
}
680+
_, err := ac.Check(ctx, relations)
681+
require.NoError(t, err)
682+
snapshot := collector.SnapshotAndReset()
683+
require.Contains(t, snapshot, "proj1")
684+
agg := snapshot["proj1"][metrics.APICheck]
685+
require.NotNil(t, agg)
686+
require.Equal(t, int64(1), agg.TotalCalls)
687+
require.Equal(t, int64(1), agg.HitCount)
688+
require.Equal(t, int64(0), agg.MissCount)
689+
require.Equal(t, int64(0), agg.SumCandidates) // full hit: nothing sent to SDK
690+
require.Equal(t, int64(2), agg.SumResultSize)
691+
}
692+
693+
func TestCheck_MetricsRecorded_FullCacheMiss(t *testing.T) {
694+
ac, mockSDK, mockCache, collector := injectAuthzMocksWithCollector(t)
695+
ctx := cctx.AddProjectID(context.TODO(), "proj1")
696+
relations := []*descope.FGARelation{
697+
{Resource: "mario", Target: "luigi", Relation: "bigBro"},
698+
{Resource: "luigi", Target: "mario", Relation: "bigBro"},
699+
}
700+
sdkResponse := []*descope.FGACheck{
701+
{Allowed: true, Relation: relations[0], Info: &descope.FGACheckInfo{Direct: true}},
702+
{Allowed: false, Relation: relations[1], Info: &descope.FGACheckInfo{Direct: true}},
703+
}
704+
mockCache.CheckRelationFunc = func(_ context.Context, _ *descope.FGARelation) (bool, bool, bool) {
705+
return false, false, false
706+
}
707+
mockSDK.MockFGA.CheckResponse = sdkResponse
708+
mockCache.UpdateCacheWithChecksFunc = func(_ context.Context, _ []*descope.FGACheck) {}
709+
_, err := ac.Check(ctx, relations)
710+
require.NoError(t, err)
711+
snapshot := collector.SnapshotAndReset()
712+
require.Contains(t, snapshot, "proj1")
713+
agg := snapshot["proj1"][metrics.APICheck]
714+
require.NotNil(t, agg)
715+
require.Equal(t, int64(1), agg.TotalCalls)
716+
require.Equal(t, int64(0), agg.HitCount)
717+
require.Equal(t, int64(1), agg.MissCount)
718+
require.Equal(t, int64(2), agg.SumCandidates) // full miss: all 2 relations sent to SDK
719+
require.Equal(t, int64(2), agg.SumResultSize)
720+
}
721+
722+
func TestCheck_MetricsRecorded_PartialHit(t *testing.T) {
723+
ac, mockSDK, mockCache, collector := injectAuthzMocksWithCollector(t)
724+
ctx := cctx.AddProjectID(context.TODO(), "proj1")
725+
relations := []*descope.FGARelation{
726+
{Resource: "mario", Target: "luigi", Relation: "bigBro"},
727+
{Resource: "luigi", Target: "mario", Relation: "bigBro"}, // only this one in cache
728+
{Resource: "mario", Target: "bowser", Relation: "enemy"},
729+
}
730+
sdkResponse := []*descope.FGACheck{
731+
{Allowed: true, Relation: relations[0], Info: &descope.FGACheckInfo{Direct: true}},
732+
{Allowed: true, Relation: relations[2], Info: &descope.FGACheckInfo{Direct: true}},
733+
}
734+
mockCache.CheckRelationFunc = func(_ context.Context, r *descope.FGARelation) (bool, bool, bool) {
735+
if r.Resource == "luigi" && r.Target == "mario" {
736+
return false, true, true // cached
737+
}
738+
return false, false, false // not cached
739+
}
740+
mockSDK.MockFGA.CheckResponse = sdkResponse
741+
mockCache.UpdateCacheWithChecksFunc = func(_ context.Context, _ []*descope.FGACheck) {}
742+
_, err := ac.Check(ctx, relations)
743+
require.NoError(t, err)
744+
snapshot := collector.SnapshotAndReset()
745+
require.Contains(t, snapshot, "proj1")
746+
agg := snapshot["proj1"][metrics.APICheck]
747+
require.NotNil(t, agg)
748+
require.Equal(t, int64(1), agg.TotalCalls)
749+
require.Equal(t, int64(0), agg.HitCount)
750+
require.Equal(t, int64(1), agg.MissCount)
751+
require.Equal(t, int64(2), agg.SumCandidates) // 2 relations sent to SDK (1 was cached)
752+
require.Equal(t, int64(0), agg.SumFiltered)
753+
require.Equal(t, int64(3), agg.SumResultSize)
754+
}
755+
664756
func BenchmarkCheck(b *testing.B) {
665757
for _, numRelations := range []int{500, 1000, 5000} {
666758
name := fmt.Sprintf("relations=%d", numRelations)

internal/services/metrics/collector.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
type APIName string
99

1010
const (
11+
APICheck APIName = "Check"
1112
APIWhoCanAccess APIName = "WhoCanAccess"
1213
APIWhatCanTargetAccess APIName = "WhatCanTargetAccess"
1314
)

0 commit comments

Comments
 (0)