From 5fdd052d895e6ea40207e134e21bd89b5b55673f Mon Sep 17 00:00:00 2001 From: yannic rieger Date: Tue, 21 Jul 2026 00:09:36 +0200 Subject: [PATCH 1/2] controlplane: add user/instance/chunk metrics these are: - explorer.control_plane.chunk.created.count - explorer.control_plane.instance.created.count - explorer.control_plane.user.registered.count --- controlplane/chunk/chunk.go | 11 ++++++ controlplane/chunk/metrics.go | 45 +++++++++++++++++++++++++ controlplane/chunk/service.go | 11 ++++-- controlplane/instance/metrics.go | 45 +++++++++++++++++++++++++ controlplane/instance/service.go | 30 +++++++++++++++-- controlplane/server.go | 58 +++++++++++++++++++------------- controlplane/user/metrics.go | 45 +++++++++++++++++++++++++ controlplane/user/service.go | 12 +++++-- 8 files changed, 228 insertions(+), 29 deletions(-) create mode 100644 controlplane/chunk/metrics.go create mode 100644 controlplane/instance/metrics.go create mode 100644 controlplane/user/metrics.go diff --git a/controlplane/chunk/chunk.go b/controlplane/chunk/chunk.go index 0cd4fe4d..05d51556 100644 --- a/controlplane/chunk/chunk.go +++ b/controlplane/chunk/chunk.go @@ -34,6 +34,8 @@ import ( "github.com/spacechunks/explorer/controlplane/contextkey" apierrs "github.com/spacechunks/explorer/controlplane/errors" "github.com/spacechunks/explorer/internal/resource" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/metric" ) func (s *svc) CreateChunk(ctx context.Context, chunk resource.Chunk) (resource.Chunk, error) { @@ -52,6 +54,15 @@ func (s *svc) CreateChunk(ctx context.Context, chunk resource.Chunk) (resource.C if err != nil { return resource.Chunk{}, err } + + s.metrics.createdChunksCount.Add( + ctx, + 1, + metric.WithAttributes( + attribute.String("name", chunk.Name), + attribute.String("owner_name", chunk.Owner.Nickname), + ), + ) return ret, nil } diff --git a/controlplane/chunk/metrics.go b/controlplane/chunk/metrics.go new file mode 100644 index 00000000..169e86fa --- /dev/null +++ b/controlplane/chunk/metrics.go @@ -0,0 +1,45 @@ +/* +A basic matchmaking service for the Chunk Explorer. +Copyright (C) 2026 Yannic Rieger + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ +package chunk + +import ( + "fmt" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/metric" +) + +type metrics struct { + createdChunksCount metric.Int64Counter +} + +func initMetrics() (metrics, error) { + meter := otel.Meter("github.com/spacechunks/explorer/controlplane/chunk") + + createdCount, err := meter.Int64Counter( + "explorer.control_plane.chunk.created.count", + metric.WithDescription("Total number of chunks created"), + ) + if err != nil { + return metrics{}, fmt.Errorf("chunk created counter: %w", err) + } + + return metrics{ + createdChunksCount: createdCount, + }, nil +} diff --git a/controlplane/chunk/service.go b/controlplane/chunk/service.go index 37dfd382..474c539b 100644 --- a/controlplane/chunk/service.go +++ b/controlplane/chunk/service.go @@ -69,6 +69,7 @@ type svc struct { s3Store blob.S3Store cfg Config access authz.AccessEvaluator + metrics metrics } func NewService( @@ -78,7 +79,12 @@ func NewService( s3Store blob.S3Store, access authz.AccessEvaluator, cfg Config, -) Service { +) (Service, error) { + m, err := initMetrics() + if err != nil { + return nil, err + } + return &svc{ logger: logger, repo: repo, @@ -86,5 +92,6 @@ func NewService( s3Store: s3Store, access: access, cfg: cfg, - } + metrics: m, + }, nil } diff --git a/controlplane/instance/metrics.go b/controlplane/instance/metrics.go new file mode 100644 index 00000000..cefc0d1f --- /dev/null +++ b/controlplane/instance/metrics.go @@ -0,0 +1,45 @@ +/* +A basic matchmaking service for the Chunk Explorer. +Copyright (C) 2026 Yannic Rieger + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ +package instance + +import ( + "fmt" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/metric" +) + +type metrics struct { + instanceCreatedCount metric.Int64Counter +} + +func initMetrics() (metrics, error) { + meter := otel.Meter("github.com/spacechunks/explorer/controlplane/instance") + + createdCount, err := meter.Int64Counter( + "explorer.control_plane.instance.created.count", + metric.WithDescription("Total number of instances started"), + ) + if err != nil { + return metrics{}, fmt.Errorf("started counter: %w", err) + } + + return metrics{ + instanceCreatedCount: createdCount, + }, nil +} diff --git a/controlplane/instance/service.go b/controlplane/instance/service.go index a99c8d10..13d0bf39 100644 --- a/controlplane/instance/service.go +++ b/controlplane/instance/service.go @@ -32,6 +32,8 @@ import ( apierrs "github.com/spacechunks/explorer/controlplane/errors" "github.com/spacechunks/explorer/controlplane/node" "github.com/spacechunks/explorer/internal/resource" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/metric" ) type Service interface { @@ -52,15 +54,27 @@ type svc struct { insRepo Repository nodeRepo node.Repository chunkRepo chunk.Repository + metrics metrics } -func NewService(logger *slog.Logger, insRepo Repository, nodeRepo node.Repository, chunkRepo chunk.Repository) Service { +func NewService( + logger *slog.Logger, + insRepo Repository, + nodeRepo node.Repository, + chunkRepo chunk.Repository, +) (Service, error) { + m, err := initMetrics() + if err != nil { + return nil, err + } + return &svc{ logger: logger, insRepo: insRepo, nodeRepo: nodeRepo, chunkRepo: chunkRepo, - } + metrics: m, + }, nil } func (s *svc) GetInstance(ctx context.Context, id string) (resource.Instance, error) { @@ -116,6 +130,8 @@ func (s *svc) RunFlavorVersion( return resource.Instance{}, fmt.Errorf("instance id: %w", err) } + version := flavor.Versions[idx] + ins, err := s.insRepo.CreateInstance(ctx, resource.Instance{ ID: instanceID.String(), FlavorVersion: flavor.Versions[idx], @@ -131,6 +147,16 @@ func (s *svc) RunFlavorVersion( return resource.Instance{}, fmt.Errorf("create instance: %w", err) } + s.metrics.instanceCreatedCount.Add( + ctx, + 1, + metric.WithAttributes( + attribute.String("flavor_name", flavor.Name), + attribute.String("flavor_version", version.Version), + attribute.String("chunk_name", ins.Chunk.Name), + ), + ) + return ins, nil } diff --git a/controlplane/server.go b/controlplane/server.go index c9e1ab46..a4f68a27 100644 --- a/controlplane/server.go +++ b/controlplane/server.go @@ -184,6 +184,40 @@ func (s *Server) Run(ctx context.Context) error { return fmt.Errorf("create validator: %w", err) } + insService, err := instance.NewService(s.logger, db, db, db) + if err != nil { + return fmt.Errorf("instance service: %w", err) + } + + userService, err := user.NewService( + db, + oidcProvider, + s.cfg.OAuthClientID, + s.cfg.APITokenIssuer, + s.cfg.APITokenExpiry, + key, + ) + if err != nil { + return fmt.Errorf("user service: %w", err) + } + + chunkService, err := chunk.NewService( + s.logger.With("component", "chunk-service"), + db, + db, + blobStore, + authz.NewRuleEvaluator(db), + chunk.Config{ + Registry: s.cfg.OCIRegistry, + Bucket: s.cfg.Bucket, + PresignedURLExpiry: s.cfg.PresignedURLExpiry, + ThumbnailMaxSizeKB: s.cfg.ThumbnailMaxSizeKB, + ChangesetTarballMaxSizeBytes: s.cfg.ChangeSetTarballMaxSizeBytes, + }) + if err != nil { + return fmt.Errorf("chunk service: %w", err) + } + var ( grpcServer = grpc.NewServer( grpc.Creds(insecure.NewCredentials()), @@ -196,30 +230,8 @@ func (s *Server) Run(ctx context.Context) error { ), ) - userService = user.NewService( - db, - oidcProvider, - s.cfg.OAuthClientID, - s.cfg.APITokenIssuer, - s.cfg.APITokenExpiry, - key, - ) - userServer = user.NewServer(userService) - chunkService = chunk.NewService( - s.logger.With("component", "chunk-service"), - db, - db, - blobStore, - authz.NewRuleEvaluator(db), - chunk.Config{ - Registry: s.cfg.OCIRegistry, - Bucket: s.cfg.Bucket, - PresignedURLExpiry: s.cfg.PresignedURLExpiry, - ThumbnailMaxSizeKB: s.cfg.ThumbnailMaxSizeKB, - ChangesetTarballMaxSizeBytes: s.cfg.ChangeSetTarballMaxSizeBytes, - }) + userServer = user.NewServer(userService) chunkServer = chunk.NewServer(chunkService) - insService = instance.NewService(s.logger, db, db, db) insServer = instance.NewServer(insService) ) diff --git a/controlplane/user/metrics.go b/controlplane/user/metrics.go new file mode 100644 index 00000000..93b1d709 --- /dev/null +++ b/controlplane/user/metrics.go @@ -0,0 +1,45 @@ +/* +A basic matchmaking service for the Chunk Explorer. +Copyright (C) 2026 Yannic Rieger + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ +package user + +import ( + "fmt" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/metric" +) + +type metrics struct { + registeredCount metric.Int64Counter +} + +func initMetrics() (metrics, error) { + meter := otel.Meter("github.com/spacechunks/explorer/controlplane/user") + + registeredCount, err := meter.Int64Counter( + "explorer.control_plane.user.registered.count", + metric.WithDescription("Total number of users registered"), + ) + if err != nil { + return metrics{}, fmt.Errorf("user registered counter: %w", err) + } + + return metrics{ + registeredCount: registeredCount, + }, nil +} diff --git a/controlplane/user/service.go b/controlplane/user/service.go index f70d2e7d..69e31c20 100644 --- a/controlplane/user/service.go +++ b/controlplane/user/service.go @@ -43,6 +43,7 @@ type service struct { issuer string apiTokenExpiry time.Duration signingKey *ecdsa.PrivateKey + metrics metrics } type idTokenClaims struct { @@ -56,7 +57,12 @@ func NewService( issuer string, apiTokenExpiry time.Duration, signingKey *ecdsa.PrivateKey, -) Service { +) (Service, error) { + m, err := initMetrics() + if err != nil { + return nil, err + } + return &service{ repo: repo, provider: provider, @@ -64,7 +70,8 @@ func NewService( issuer: issuer, apiTokenExpiry: apiTokenExpiry, signingKey: signingKey, - } + metrics: m, + }, nil } func (s *service) Register(ctx context.Context, nickname string, rawIDToken string, acceptPrivacyPolicy bool) error { @@ -93,6 +100,7 @@ func (s *service) Register(ctx context.Context, nickname string, rawIDToken stri return fmt.Errorf("create user: %w", err) } + s.metrics.registeredCount.Add(ctx, 1) return nil } From 462109bf90e7e61d23bbc5884cdb46219f000b48 Mon Sep 17 00:00:00 2001 From: yannic rieger Date: Tue, 21 Jul 2026 00:23:26 +0200 Subject: [PATCH 2/2] please linter --- controlplane/chunk/flavor_test.go | 36 +++++++++++++++++-------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/controlplane/chunk/flavor_test.go b/controlplane/chunk/flavor_test.go index 8b7b4140..d8a603a3 100644 --- a/controlplane/chunk/flavor_test.go +++ b/controlplane/chunk/flavor_test.go @@ -95,16 +95,18 @@ func TestCreateFlavor(t *testing.T) { ctx = context.Background() mockRepo = mock.NewMockChunkRepository(t) mockAccess = mock.NewMockAuthzAccessEvaluator(t) - svc = chunk.NewService( - slog.New(slog.NewTextHandler(os.Stdout, nil)), - mockRepo, - nil, - nil, - mockAccess, - chunk.Config{}, - ) ) + svc, err := chunk.NewService( + slog.New(slog.NewTextHandler(os.Stdout, nil)), + mockRepo, + nil, + nil, + mockAccess, + chunk.Config{}, + ) + require.NoError(t, err) + ctx = context.WithValue(ctx, contextkey.ActorID, "blabla") tt.prep(mockRepo, mockAccess) @@ -584,16 +586,18 @@ func TestCreateFlavorVersion(t *testing.T) { ctx = context.Background() mockAccess = mock.NewMockAuthzAccessEvaluator(t) mockRepo = mock.NewMockChunkRepository(t) - svc = chunk.NewService( - slog.New(slog.NewTextHandler(os.Stdout, nil)), - mockRepo, - nil, - nil, - mockAccess, - chunk.Config{}, - ) ) + svc, err := chunk.NewService( + slog.New(slog.NewTextHandler(os.Stdout, nil)), + mockRepo, + nil, + nil, + mockAccess, + chunk.Config{}, + ) + require.NoError(t, err) + ctx = context.WithValue(ctx, contextkey.ActorID, "blabla") tt.prep(mockRepo, tt.newVersion, tt.prevVersion, mockAccess)