Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions internal/app/cleanup.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,12 @@ func (s *Service) RunCleanupOnce(ctx context.Context, limit int) (*CleanupStats,
if artifact.State == core.ArtifactStateExpired {
continue
}
previousState := artifact.State
artifact.State = core.ArtifactStateExpired
if err := s.repo.SaveArtifact(ctx, artifact); err != nil {
return nil, fmt.Errorf("run cleanup: save expired artifact %q: %w", artifact.ID, err)
}
s.metrics.recordArtifactTransition(previousState, artifact.State, artifact.ConnectorKind)
stats.ArtifactsExpired++
s.appendAudit(ctx, &core.AuditEvent{
EventID: s.ids.New("evt"),
Expand Down Expand Up @@ -193,6 +195,7 @@ func (s *Service) transitionGrantState(ctx context.Context, session *core.Sessio
s.metrics.recordGrantTransition(grant.State)

if artifact != nil {
previousState := artifact.State
switch state {
case core.GrantStateRevoked:
artifact.State = core.ArtifactStateRevoked
Expand All @@ -204,6 +207,7 @@ func (s *Service) transitionGrantState(ctx context.Context, session *core.Sessio
if err := s.repo.SaveArtifact(ctx, artifact); err != nil {
return fmt.Errorf("transition grant %q to %q: save artifact %q: %w", grant.ID, state, artifact.ID, err)
}
s.metrics.recordArtifactTransition(previousState, artifact.State, artifact.ConnectorKind)
}

eventType := ""
Expand Down
90 changes: 74 additions & 16 deletions internal/app/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,16 @@ type MetricsOptions struct {

// Metrics records ASB domain-level counters, gauges, and histograms.
type Metrics struct {
sessionsActive *prometheus.GaugeVec
sessionsTotal *prometheus.CounterVec
grantsTotal *prometheus.CounterVec
grantTTL prometheus.Histogram
approvalsTotal *prometheus.CounterVec
approvalWait *prometheus.HistogramVec
policyEval *prometheus.CounterVec
budgetExhaust *prometheus.CounterVec
sessionsActive *prometheus.GaugeVec
sessionsTotal *prometheus.CounterVec
grantsTotal *prometheus.CounterVec
grantTTL prometheus.Histogram
approvalsTotal *prometheus.CounterVec
approvalWait *prometheus.HistogramVec
policyEval *prometheus.CounterVec
budgetExhaust *prometheus.CounterVec
artifactsActive *prometheus.GaugeVec
artifactUnwraps *prometheus.CounterVec
}

// NewMetrics creates Prometheus collectors for ASB domain metrics.
Expand Down Expand Up @@ -147,15 +149,45 @@ func NewMetrics(serviceName string, opts MetricsOptions) (*Metrics, error) {
return nil, err
}

artifactsActive, err := registerGaugeVec(
opts.Registerer,
prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: prefix + "_artifacts_active",
Help: "Current number of active ASB artifacts by connector kind.",
},
[]string{"connector_kind"},
),
)
if err != nil {
return nil, err
}

artifactUnwraps, err := registerCounterVec(
opts.Registerer,
prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: prefix + "_artifact_unwraps_total",
Help: "Count of ASB artifact unwrap operations by connector kind.",
},
[]string{"connector_kind"},
),
)
if err != nil {
return nil, err
}

return &Metrics{
sessionsActive: sessionsActive,
sessionsTotal: sessionsTotal,
grantsTotal: grantsTotal,
grantTTL: grantTTL,
approvalsTotal: approvalsTotal,
approvalWait: approvalWait,
policyEval: policyEval,
budgetExhaust: budgetExhaust,
sessionsActive: sessionsActive,
sessionsTotal: sessionsTotal,
grantsTotal: grantsTotal,
grantTTL: grantTTL,
approvalsTotal: approvalsTotal,
approvalWait: approvalWait,
policyEval: policyEval,
budgetExhaust: budgetExhaust,
artifactsActive: artifactsActive,
artifactUnwraps: artifactUnwraps,
}, nil
}

Expand Down Expand Up @@ -241,6 +273,32 @@ func (metrics *Metrics) recordBudgetExhaustion(handle string) {
metrics.budgetExhaust.WithLabelValues(labelOrUnknown(handle)).Inc()
}

func (metrics *Metrics) recordArtifactCreated(connectorKind string) {
if metrics == nil {
return
}
metrics.artifactsActive.WithLabelValues(labelOrUnknown(connectorKind)).Inc()
}

func (metrics *Metrics) recordArtifactTransition(previous, next core.ArtifactState, connectorKind string) {
if metrics == nil || previous == next {
return
}
if previous == core.ArtifactStateIssued {
metrics.artifactsActive.WithLabelValues(labelOrUnknown(connectorKind)).Dec()
}
if next == core.ArtifactStateIssued {
metrics.artifactsActive.WithLabelValues(labelOrUnknown(connectorKind)).Inc()
}
}

func (metrics *Metrics) recordArtifactUnwrap(connectorKind string) {
if metrics == nil {
return
}
metrics.artifactUnwraps.WithLabelValues(labelOrUnknown(connectorKind)).Inc()
}

func labelOrUnknown(value string) string {
value = strings.TrimSpace(value)
if value == "" {
Expand Down
118 changes: 118 additions & 0 deletions internal/app/metrics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ func TestServiceMetrics_CreateSessionAndIssueGrant(t *testing.T) {
if got := metricValueWithLabels(families, "asb_grants_total", map[string]string{"outcome": "issued"}); got != 1 {
t.Fatalf("issued grants = %v, want 1", got)
}
if got := metricValueWithLabels(families, "asb_artifacts_active", map[string]string{"connector_kind": "github"}); got != 1 {
t.Fatalf("active github artifacts = %v, want 1", got)
}
if got := histogramCountWithLabels(families, "asb_grant_ttl_seconds", nil); got != 1 {
t.Fatalf("grant TTL histogram count = %d, want 1", got)
}
Expand Down Expand Up @@ -370,6 +373,9 @@ func TestServiceMetrics_RevokeSession(t *testing.T) {
if got := metricValueWithLabels(families, "asb_grants_total", map[string]string{"outcome": "revoked"}); got != 1 {
t.Fatalf("revoked grants = %v, want 1", got)
}
if got := metricValueWithLabels(families, "asb_artifacts_active", map[string]string{"connector_kind": "github"}); got != 0 {
t.Fatalf("active github artifacts = %v, want 0", got)
}
grant, err := repo.GetGrant(ctx, grantResp.GrantID)
if err != nil {
t.Fatalf("GetGrant() error = %v", err)
Expand Down Expand Up @@ -661,6 +667,118 @@ func TestServiceMetrics_BudgetExhaustion(t *testing.T) {
}
}

func TestServiceMetrics_UnwrapArtifact(t *testing.T) {
t.Parallel()

ctx := context.Background()
now := testNow()
registry := prometheus.NewRegistry()
metrics, err := app.NewMetrics("asb", app.MetricsOptions{
Registerer: registry,
})
if err != nil {
t.Fatalf("NewMetrics() error = %v", err)
}

repo := memstore.NewRepository()
tools := toolregistry.New()
engine := policy.NewEngine()
signer := mustNewSigner(t)
connector := &fakeConnector{
kind: "vaultdb",
issued: &core.IssuedArtifact{
Kind: core.ArtifactKindWrappedSecret,
SecretData: map[string]string{
"username": "readonly",
"password": "redacted",
},
},
}
delivery := &fakeDeliveryAdapter{
mode: core.DeliveryModeWrappedSecret,
delivery: &core.Delivery{
Kind: core.DeliveryKindWrappedSecret,
ArtifactID: "art_unwrap_metrics",
},
}

mustPutTool(t, ctx, tools, core.Tool{
TenantID: "t_acme",
Tool: "vaultdb",
ManifestHash: "sha256:vaultdb",
RuntimeClass: core.RuntimeClassHosted,
AllowedDeliveryModes: []core.DeliveryMode{core.DeliveryModeWrappedSecret},
AllowedCapabilities: []string{"db.read"},
TrustTags: []string{"trusted", "db"},
})
mustPutPolicy(t, engine, core.Policy{
TenantID: "t_acme",
Capability: "db.read",
ResourceKind: core.ResourceKindDBRole,
AllowedDeliveryModes: []core.DeliveryMode{core.DeliveryModeWrappedSecret},
DefaultTTL: 10 * time.Minute,
MaxTTL: 10 * time.Minute,
ApprovalMode: core.ApprovalModeNone,
RequiredToolTags: []string{"trusted", "db"},
Condition: `true`,
})

svc, err := app.NewService(app.Config{
Clock: fixedClock(now),
IDs: fixedIDs("sess_unwrap_metrics", "evt_1", "gr_unwrap_metrics", "art_unwrap_metrics", "evt_2", "evt_3", "evt_4"),
Metrics: metrics,
Repository: repo,
Verifier: fakeVerifier{identity: workloadIdentity()},
SessionTokens: signer,
Policy: engine,
Tools: tools,
Connectors: fakeConnectorResolver{connector: connector},
Deliveries: map[core.DeliveryMode]core.DeliveryAdapter{
core.DeliveryModeWrappedSecret: delivery,
},
Audit: memory.NewSink(),
})
if err != nil {
t.Fatalf("NewService() error = %v", err)
}

sessionResp, err := svc.CreateSession(ctx, &core.CreateSessionRequest{
TenantID: "t_acme",
AgentID: "agent_db_reader",
RunID: "run_unwrap_metrics",
ToolContext: []string{"vaultdb"},
Attestation: &core.Attestation{Kind: core.AttestationKindK8SServiceAccountJWT, Token: "jwt"},
})
if err != nil {
t.Fatalf("CreateSession() error = %v", err)
}
grantResp, err := svc.RequestGrant(ctx, &core.RequestGrantRequest{
SessionToken: sessionResp.SessionToken,
Tool: "vaultdb",
Capability: "db.read",
ResourceRef: "dbrole:analytics_readonly",
DeliveryMode: core.DeliveryModeWrappedSecret,
})
if err != nil {
t.Fatalf("RequestGrant() error = %v", err)
}

if _, err := svc.UnwrapArtifact(ctx, &core.UnwrapArtifactRequest{
SessionToken: sessionResp.SessionToken,
ArtifactID: grantResp.Delivery.ArtifactID,
}); err != nil {
t.Fatalf("UnwrapArtifact() error = %v", err)
}

families := mustGatherMetrics(t, registry)
if got := metricValueWithLabels(families, "asb_artifacts_active", map[string]string{"connector_kind": "vaultdb"}); got != 0 {
t.Fatalf("active vaultdb artifacts = %v, want 0", got)
}
if got := metricValueWithLabels(families, "asb_artifact_unwraps_total", map[string]string{"connector_kind": "vaultdb"}); got != 1 {
t.Fatalf("vaultdb unwrap count = %v, want 1", got)
}
}

func mustGatherMetrics(t *testing.T, gatherer prometheus.Gatherer) []*dto.MetricFamily {
t.Helper()

Expand Down
3 changes: 3 additions & 0 deletions internal/app/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,8 @@ func (s *Service) UnwrapArtifact(ctx context.Context, req *core.UnwrapArtifactRe
if err != nil {
return nil, fmt.Errorf("unwrap artifact %q: mark artifact used: %w", req.ArtifactID, err)
}
s.metrics.recordArtifactTransition(artifact.State, usedArtifact.State, usedArtifact.ConnectorKind)
s.metrics.recordArtifactUnwrap(usedArtifact.ConnectorKind)

fields := make([]core.BrowserFillField, 0, len(usedArtifact.SecretData))
if artifact.ConnectorKind == "browser" {
Expand Down Expand Up @@ -754,6 +756,7 @@ func (s *Service) issueGrant(ctx context.Context, session *core.Session, grant *
if err := s.repo.SaveArtifact(ctx, storedArtifact); err != nil {
return nil, fmt.Errorf("issue grant %q: save artifact %q: %w", grant.ID, storedArtifact.ID, err)
}
s.metrics.recordArtifactCreated(storedArtifact.ConnectorKind)
if delivery.Handle != "" && s.runtime != nil {
if err := s.runtime.RegisterProxyHandle(ctx, delivery.Handle, budgetFromMetadata(artifact.Metadata), artifact.ExpiresAt); err != nil {
return nil, fmt.Errorf("issue grant %q: register proxy handle %q: %w", grant.ID, delivery.Handle, err)
Expand Down
Loading