diff --git a/cmd/dev/app/testserver/testserver.go b/cmd/dev/app/testserver/testserver.go index 15f9ac53a6..d4b2bcf336 100644 --- a/cmd/dev/app/testserver/testserver.go +++ b/cmd/dev/app/testserver/testserver.go @@ -37,6 +37,7 @@ import ( "github.com/stacklok/minder/internal/controlplane/metrics" "github.com/stacklok/minder/internal/db/embedded" "github.com/stacklok/minder/internal/logger" + "github.com/stacklok/minder/internal/metrics/meters" "github.com/stacklok/minder/internal/providers/ratecache" provtelemetry "github.com/stacklok/minder/internal/providers/telemetry" "github.com/stacklok/minder/internal/service" @@ -102,5 +103,6 @@ func runTestServer(cmd *cobra.Command, _ []string) error { metrics.NewNoopMetrics(), provtelemetry.NewNoopMetrics(), []message.HandlerMiddleware{}, + &meters.NoopMeterFactory{}, ) } diff --git a/cmd/server/app/serve.go b/cmd/server/app/serve.go index decd771103..c311dcdb85 100644 --- a/cmd/server/app/serve.go +++ b/cmd/server/app/serve.go @@ -35,6 +35,7 @@ import ( cpmetrics "github.com/stacklok/minder/internal/controlplane/metrics" "github.com/stacklok/minder/internal/db" "github.com/stacklok/minder/internal/logger" + "github.com/stacklok/minder/internal/metrics/meters" "github.com/stacklok/minder/internal/providers/ratecache" provtelemetry "github.com/stacklok/minder/internal/providers/telemetry" "github.com/stacklok/minder/internal/service" @@ -138,6 +139,7 @@ var serveCmd = &cobra.Command{ cpmetrics.NewMetrics(), providerMetrics, []message.HandlerMiddleware{telemetryMiddleware.TelemetryStoreMiddleware}, + &meters.ExportingMeterFactory{}, ) }, } diff --git a/internal/engine/eval_status.go b/internal/engine/eval_status.go index a87d222d53..610ebe2369 100644 --- a/internal/engine/eval_status.go +++ b/internal/engine/eval_status.go @@ -27,6 +27,7 @@ import ( "github.com/stacklok/minder/internal/engine/entities" evalerrors "github.com/stacklok/minder/internal/engine/errors" engif "github.com/stacklok/minder/internal/engine/interfaces" + ent "github.com/stacklok/minder/internal/entities" pb "github.com/stacklok/minder/pkg/api/protobuf/go/minder/v1" ) @@ -52,6 +53,7 @@ func (e *Executor) createEvalStatusParams( RepoID: repoID, ArtifactID: artID, PullRequestID: prID, + ProjectID: inf.ProjectID, } // Prepare params for fetching the current rule evaluation from the database @@ -141,6 +143,12 @@ func (e *Executor) createOrUpdateEvalStatus( } // Upsert evaluation details + entityID, entityType, err := ent.EntityFromIDs(params.RepoID.UUID, params.ArtifactID.UUID, params.PullRequestID.UUID) + if err != nil { + return err + } + status := evalerrors.ErrorAsEvalStatus(params.GetEvalErr()) + e.metrics.CountEvalStatus(ctx, status, params.ProfileID, params.ProjectID, entityID, entityType) _, err = e.querier.UpsertRuleDetailsEval(ctx, db.UpsertRuleDetailsEvalParams{ RuleEvalID: id, Status: evalerrors.ErrorAsEvalStatus(params.GetEvalErr()), @@ -151,6 +159,7 @@ func (e *Executor) createOrUpdateEvalStatus( logger.Err(err).Msg("error upserting rule evaluation details") return err } + // Upsert remediation details _, err = e.querier.UpsertRuleDetailsRemediate(ctx, db.UpsertRuleDetailsRemediateParams{ RuleEvalID: id, @@ -161,6 +170,7 @@ func (e *Executor) createOrUpdateEvalStatus( if err != nil { logger.Err(err).Msg("error upserting rule remediation details") } + // Upsert alert details _, err = e.querier.UpsertRuleDetailsAlert(ctx, db.UpsertRuleDetailsAlertParams{ RuleEvalID: id, @@ -171,6 +181,7 @@ func (e *Executor) createOrUpdateEvalStatus( if err != nil { logger.Err(err).Msg("error upserting rule alert details") } + return err } diff --git a/internal/engine/executor.go b/internal/engine/executor.go index 3d915f9fe8..9b61ad1991 100644 --- a/internal/engine/executor.go +++ b/internal/engine/executor.go @@ -60,6 +60,7 @@ type Executor struct { // when the server is shutting down. terminationcontext context.Context providerManager manager.ProviderManager + metrics *ExecutorMetrics } // NewExecutor creates a new executor @@ -69,6 +70,7 @@ func NewExecutor( evt events.Publisher, providerManager manager.ProviderManager, handlerMiddleware []message.HandlerMiddleware, + metrics *ExecutorMetrics, ) *Executor { return &Executor{ querier: querier, @@ -77,6 +79,7 @@ func NewExecutor( terminationcontext: ctx, handlerMiddleware: handlerMiddleware, providerManager: providerManager, + metrics: metrics, } } diff --git a/internal/engine/executor_test.go b/internal/engine/executor_test.go index 3299d80cfb..8583b6e7ce 100644 --- a/internal/engine/executor_test.go +++ b/internal/engine/executor_test.go @@ -43,6 +43,7 @@ import ( "github.com/stacklok/minder/internal/engine/entities" "github.com/stacklok/minder/internal/events" "github.com/stacklok/minder/internal/logger" + "github.com/stacklok/minder/internal/metrics/meters" "github.com/stacklok/minder/internal/providers" "github.com/stacklok/minder/internal/providers/github/clients" ghmanager "github.com/stacklok/minder/internal/providers/github/manager" @@ -347,12 +348,16 @@ default allow = true`, providerManager, err := manager.NewProviderManager(providerStore, githubProviderManager) require.NoError(t, err) + execMetrics, err := engine.NewExecutorMetrics(&meters.NoopMeterFactory{}) + require.NoError(t, err) + e := engine.NewExecutor( ctx, mockStore, evt, providerManager, []message.HandlerMiddleware{}, + execMetrics, ) require.NoError(t, err, "expected no error") diff --git a/internal/engine/interfaces/interface.go b/internal/engine/interfaces/interface.go index 9401d5fbc7..c1bfdc1edc 100644 --- a/internal/engine/interfaces/interface.go +++ b/internal/engine/interfaces/interface.go @@ -136,6 +136,7 @@ type EvalStatusParams struct { RepoID uuid.NullUUID ArtifactID uuid.NullUUID PullRequestID uuid.NullUUID + ProjectID uuid.UUID EntityType db.Entities RuleTypeID uuid.UUID EvalStatusFromDb *db.ListRuleEvaluationsByProfileIdRow diff --git a/internal/engine/metrics.go b/internal/engine/metrics.go new file mode 100644 index 0000000000..cf99d1a116 --- /dev/null +++ b/internal/engine/metrics.go @@ -0,0 +1,111 @@ +// Copyright 2023 Stacklok, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package engine + +import ( + "context" + "fmt" + + "github.com/google/uuid" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/metric" + + "github.com/stacklok/minder/internal/db" + "github.com/stacklok/minder/internal/metrics/meters" +) + +// ExecutorMetrics encapsulates metrics operations for the executor +type ExecutorMetrics struct { + evalCounter metric.Int64Counter + remediationCounter metric.Int64Counter + alertCounter metric.Int64Counter +} + +// NewExecutorMetrics instantiates the ExecutorMetrics struct. +func NewExecutorMetrics(meterFactory meters.MeterFactory) (*ExecutorMetrics, error) { + meter := meterFactory.Build("executor") + evalCounter, err := meter.Int64Counter("eval.status", + metric.WithDescription("Number of rule evaluation statuses"), + metric.WithUnit("evaluations")) + if err != nil { + return nil, fmt.Errorf("failed to create eval counter: %w", err) + } + + remediationCounter, err := meter.Int64Counter("eval.remediation", + metric.WithDescription("Number of remediation statuses"), + metric.WithUnit("evaluations")) + if err != nil { + return nil, fmt.Errorf("failed to create remediation counter: %w", err) + } + + alertCounter, err := meter.Int64Counter("eval.alert", + metric.WithDescription("Number of alert statuses"), + metric.WithUnit("evaluations")) + if err != nil { + return nil, fmt.Errorf("failed to create alert counter: %w", err) + } + + return &ExecutorMetrics{ + evalCounter: evalCounter, + remediationCounter: remediationCounter, + alertCounter: alertCounter, + }, nil +} + +// CountEvalStatus counts evaluation events by status. +func (e *ExecutorMetrics) CountEvalStatus( + ctx context.Context, + status db.EvalStatusTypes, + profileID uuid.UUID, + projectID uuid.UUID, + entityID uuid.UUID, + entityType db.Entities, +) { + e.evalCounter.Add(ctx, 1, metric.WithAttributes( + attribute.String("profile_id", profileID.String()), + attribute.String("project_id", projectID.String()), + attribute.String("entity_id", entityID.String()), + attribute.String("entity_type", string(entityType)), + attribute.String("status", string(status)), + )) +} + +// CountRemediationStatus counts remediation events by status. +func (e *ExecutorMetrics) CountRemediationStatus( + ctx context.Context, + status string, + evalID uuid.UUID, + projectID uuid.UUID, +) { + e.evalCounter.Add(ctx, 1, metric.WithAttributes( + attribute.String("profile_id", evalID.String()), + attribute.String("project_id", projectID.String()), + attribute.String("status", status), + )) +} + +// CountAlertStatus counts alert events by status. +func (e *ExecutorMetrics) CountAlertStatus( + ctx context.Context, + status string, + evalID uuid.UUID, + projectID uuid.UUID, +) { + e.evalCounter.Add(ctx, 1, metric.WithAttributes( + attribute.String("profile_id", evalID.String()), + attribute.String("project_id", projectID.String()), + attribute.String("status", status), + )) +} diff --git a/internal/engine/rule_type_engine.go b/internal/engine/rule_type_engine.go index 20caccce9c..60036aa3d7 100644 --- a/internal/engine/rule_type_engine.go +++ b/internal/engine/rule_type_engine.go @@ -11,7 +11,6 @@ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. -// Package rule provides the CLI subcommand for managing rules package engine diff --git a/internal/entities/utils.go b/internal/entities/utils.go new file mode 100644 index 0000000000..531554dbe2 --- /dev/null +++ b/internal/entities/utils.go @@ -0,0 +1,44 @@ +// Copyright 2024 Stacklok, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package entities contains logic relating to entity management +package entities + +import ( + "fmt" + + "github.com/google/uuid" + + "github.com/stacklok/minder/internal/db" +) + +// EntityFromIDs takes the IDs of the three known entity types and +// returns a single ID along with the type of the entity. +// This assumes that exactly one of the IDs is not equal to uuid.Nil +func EntityFromIDs( + repositoryID uuid.UUID, + artifactID uuid.UUID, + pullRequestID uuid.UUID, +) (uuid.UUID, db.Entities, error) { + if repositoryID != uuid.Nil && artifactID == uuid.Nil && pullRequestID == uuid.Nil { + return repositoryID, db.EntitiesRepository, nil + } + if repositoryID == uuid.Nil && artifactID != uuid.Nil && pullRequestID == uuid.Nil { + return artifactID, db.EntitiesArtifact, nil + } + if repositoryID == uuid.Nil && artifactID == uuid.Nil && pullRequestID != uuid.Nil { + return pullRequestID, db.EntitiesPullRequest, nil + } + return uuid.Nil, "", fmt.Errorf("unexpected combination of IDs: %s %s %s", repositoryID, artifactID, pullRequestID) +} diff --git a/internal/metrics/meters/factory.go b/internal/metrics/meters/factory.go new file mode 100644 index 0000000000..64ff85d3cb --- /dev/null +++ b/internal/metrics/meters/factory.go @@ -0,0 +1,46 @@ +// Copyright 2024 Stacklok, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package meters contains the OpenTelemetry meter factories. +package meters + +import ( + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/metric/noop" +) + +// MeterFactory is an interface which hides the details of creating an +// OpenTelemetry metrics meter. This is used to select between a real exporter +// or noop for testing. +type MeterFactory interface { + // Build creates a meter with the specified name. + Build(name string) metric.Meter +} + +// ExportingMeterFactory uses the "real" OpenTelemetry metric meter +type ExportingMeterFactory struct{} + +// Build creates a meter with the specified name. +func (_ *ExportingMeterFactory) Build(name string) metric.Meter { + return otel.Meter(name) +} + +// NoopMeterFactory returns a noop metrics meter +type NoopMeterFactory struct{} + +// Build returns a noop meter implementation. +func (_ *NoopMeterFactory) Build(_ string) metric.Meter { + return noop.Meter{} +} diff --git a/internal/service/service.go b/internal/service/service.go index 44312c0e2f..37b368068d 100644 --- a/internal/service/service.go +++ b/internal/service/service.go @@ -35,6 +35,7 @@ import ( "github.com/stacklok/minder/internal/events" "github.com/stacklok/minder/internal/flags" "github.com/stacklok/minder/internal/marketplaces" + "github.com/stacklok/minder/internal/metrics/meters" "github.com/stacklok/minder/internal/profiles" "github.com/stacklok/minder/internal/projects" "github.com/stacklok/minder/internal/providers" @@ -67,6 +68,7 @@ func AllInOneServerService( serverMetrics metrics.Metrics, providerMetrics provtelemetry.ProviderMetrics, executorMiddleware []message.HandlerMiddleware, + meterFactory meters.MeterFactory, ) error { errg, ctx := errgroup.WithContext(ctx) @@ -167,6 +169,10 @@ func AllInOneServerService( // prepend the aggregator to the executor options executorMiddleware = append([]message.HandlerMiddleware{aggr.AggregateMiddleware}, executorMiddleware...) + executorMetrics, err := engine.NewExecutorMetrics(meterFactory) + if err != nil { + return fmt.Errorf("unable to create metrics for executor: %w", err) + } exec := engine.NewExecutor( ctx, @@ -174,6 +180,7 @@ func AllInOneServerService( evt, providerManager, executorMiddleware, + executorMetrics, ) evt.ConsumeEvents(exec)