diff --git a/cmd/server/main.go b/cmd/server/main.go index 5ae0db9..963a622 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -12,14 +12,14 @@ // @license.name MIT // @license.url https://opensource.org/licenses/MIT -// @host localhost:8080 -// @BasePath /api -// @schemes http https +// @host localhost:8080 +// @BasePath /api +// @schemes http https // // @securityDefinitions.apikey ApiKeyAuth -// @in header -// @name Authorization -// @description JWT authentication using Bearer token. Format: Bearer +// @in header +// @name Authorization +// @description JWT authentication using Bearer token. Format: Bearer package main diff --git a/pkg/config/config.go b/pkg/config/config.go index ec97778..82b692a 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -43,11 +43,17 @@ type LoggingConfig struct { // TelemetryConfig holds OpenTelemetry-related configuration type TelemetryConfig struct { - Enabled bool `mapstructure:"enabled"` - OTLPEndpoint string `mapstructure:"otlp_endpoint"` - ServiceName string `mapstructure:"service_name"` - Insecure bool `mapstructure:"insecure"` - Sampler SamplerConfig `mapstructure:"sampler"` + Enabled bool `mapstructure:"enabled"` + OTLPEndpoint string `mapstructure:"otlp_endpoint"` + ServiceName string `mapstructure:"service_name"` + Insecure bool `mapstructure:"insecure"` + Sampler SamplerConfig `mapstructure:"sampler"` + Persistence PersistenceTelemetryConfig `mapstructure:"persistence"` +} + +// PersistenceTelemetryConfig holds persistence layer telemetry configuration +type PersistenceTelemetryConfig struct { + Enabled bool `mapstructure:"enabled"` } // APIConfig holds API version configuration @@ -107,6 +113,7 @@ func LoadConfig() (*Config, error) { v.SetDefault("telemetry.insecure", true) v.SetDefault("telemetry.sampler.type", "parentbased_always_on") v.SetDefault("telemetry.sampler.ratio", 1.0) + v.SetDefault("telemetry.persistence.enabled", false) // API defaults v.SetDefault("api.v2_enabled", false) @@ -215,6 +222,11 @@ func (c *Config) GetServiceName() string { return c.Telemetry.ServiceName } +// GetPersistenceTelemetryEnabled returns whether persistence layer telemetry is enabled +func (c *Config) GetPersistenceTelemetryEnabled() bool { + return c.Telemetry.Enabled && c.Telemetry.Persistence.Enabled +} + // GetTelemetryInsecure returns whether to use insecure connection func (c *Config) GetTelemetryInsecure() bool { return c.Telemetry.Insecure diff --git a/pkg/server/server.go b/pkg/server/server.go index ce0b591..1cf4f00 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -77,7 +77,7 @@ func initializeUserServices(cfg *config.Config) (user.UserRepository, user.UserS dbPath := "file::memory:?cache=shared" // Create user repository - repo, err := user.NewSQLiteRepository(dbPath) + repo, err := user.NewSQLiteRepository(dbPath, cfg) if err != nil { return nil, nil, fmt.Errorf("failed to create user repository: %w", err) } diff --git a/pkg/user/sqlite_repository.go b/pkg/user/sqlite_repository.go index caec64d..7556fd8 100644 --- a/pkg/user/sqlite_repository.go +++ b/pkg/user/sqlite_repository.go @@ -9,6 +9,10 @@ import ( "path/filepath" "time" + "dance-lessons-coach/pkg/config" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" "gorm.io/driver/sqlite" "gorm.io/gorm" "gorm.io/gorm/logger" @@ -16,14 +20,18 @@ import ( // SQLiteRepository implements UserRepository using SQLite type SQLiteRepository struct { - db *gorm.DB - dbPath string + db *gorm.DB + dbPath string + config *config.Config + spanPrefix string } // NewSQLiteRepository creates a new SQLite repository -func NewSQLiteRepository(dbPath string) (*SQLiteRepository, error) { +func NewSQLiteRepository(dbPath string, config *config.Config) (*SQLiteRepository, error) { repo := &SQLiteRepository{ - dbPath: dbPath, + dbPath: dbPath, + config: config, + spanPrefix: "user.repo.", } if err := repo.initializeDatabase(); err != nil { @@ -70,8 +78,17 @@ func (r *SQLiteRepository) initializeDatabase() error { // CreateUser creates a new user in the database func (r *SQLiteRepository) CreateUser(ctx context.Context, user *User) error { + // Create telemetry span + ctx, span := r.createSpan(ctx, "create_user") + if span != nil { + defer span.End() + } + result := r.db.WithContext(ctx).Create(user) if result.Error != nil { + if span != nil { + span.RecordError(result.Error) + } return fmt.Errorf("failed to create user: %w", result.Error) } return nil @@ -79,12 +96,22 @@ func (r *SQLiteRepository) CreateUser(ctx context.Context, user *User) error { // GetUserByUsername retrieves a user by username func (r *SQLiteRepository) GetUserByUsername(ctx context.Context, username string) (*User, error) { + // Create telemetry span + ctx, span := r.createSpan(ctx, "get_user_by_username") + if span != nil { + defer span.End() + span.SetAttributes(attribute.String("username", username)) + } + var user User result := r.db.WithContext(ctx).Where("username = ?", username).First(&user) if result.Error != nil { if errors.Is(result.Error, gorm.ErrRecordNotFound) { return nil, nil } + if span != nil { + span.RecordError(result.Error) + } return nil, fmt.Errorf("failed to get user by username: %w", result.Error) } return &user, nil @@ -172,3 +199,15 @@ func (r *SQLiteRepository) Close() error { } return sqlDB.Close() } + +// createSpan creates a new telemetry span if persistence telemetry is enabled +func (r *SQLiteRepository) createSpan(ctx context.Context, operation string) (context.Context, trace.Span) { + if r.config == nil || !r.config.GetPersistenceTelemetryEnabled() { + return ctx, trace.SpanFromContext(ctx) + } + + // Create a new span with the operation name + spanName := r.spanPrefix + operation + tr := otel.Tracer("user-repository") + return tr.Start(ctx, spanName) +} diff --git a/pkg/user/user_test.go b/pkg/user/user_test.go index 8a712b6..28bc9b9 100644 --- a/pkg/user/user_test.go +++ b/pkg/user/user_test.go @@ -6,17 +6,31 @@ import ( "testing" "time" + "dance-lessons-coach/pkg/config" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +// createTestConfig creates a test configuration with telemetry disabled +func createTestConfig() *config.Config { + return &config.Config{ + Telemetry: config.TelemetryConfig{ + Enabled: false, + Persistence: config.PersistenceTelemetryConfig{ + Enabled: false, + }, + }, + } +} + func TestSQLiteRepository(t *testing.T) { t.Run("CRUD operations", func(t *testing.T) { // Create a temporary database dbPath := "test_db.sqlite" defer os.Remove(dbPath) - repo, err := NewSQLiteRepository(dbPath) + cfg := createTestConfig() + repo, err := NewSQLiteRepository(dbPath, cfg) require.NoError(t, err) defer repo.Close() @@ -92,7 +106,8 @@ func TestAuthService(t *testing.T) { dbPath := "test_auth_db.sqlite" defer os.Remove(dbPath) - repo, err := NewSQLiteRepository(dbPath) + cfg := createTestConfig() + repo, err := NewSQLiteRepository(dbPath, cfg) require.NoError(t, err) defer repo.Close() @@ -162,7 +177,8 @@ func TestPasswordResetService(t *testing.T) { dbPath := "test_reset_db.sqlite" defer os.Remove(dbPath) - repo, err := NewSQLiteRepository(dbPath) + cfg := createTestConfig() + repo, err := NewSQLiteRepository(dbPath, cfg) require.NoError(t, err) defer repo.Close()