rdev/internal/service/operation_service.go
jordan d69da6d627 feat: add structured logging infrastructure and SDLC extensions
Major changes:
- Add internal/logging package with field constants, context propagation,
  sensitive data auto-redaction, and per-component log levels
- Add worker timeout constants (TimeoutQuickOp, TimeoutHealthCheck, etc.)
- Extend SDLC with callback handlers, generate endpoints, and executor
- Add new cookbook trees for aeries and slackpath progression
- Add skeleton templates for queue, realtime, and microservices
- Add worker component template with async job processing
- Refactor services and handlers to use new logging infrastructure
- Split component.go into component_infra.go and component_listing.go

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-04 22:56:04 -07:00

226 lines
6.2 KiB
Go

// Package service provides business logic services.
package service
import (
"context"
"time"
"github.com/google/uuid"
"github.com/orchard9/rdev/internal/domain"
"github.com/orchard9/rdev/internal/logging"
"github.com/orchard9/rdev/internal/port"
)
// OperationService provides business logic for tracking operations.
// It wraps the repository with convenient methods for step-by-step tracking.
type OperationService struct {
repo port.OperationRepository
}
// NewOperationService creates a new operation service.
func NewOperationService(repo port.OperationRepository) *OperationService {
return &OperationService{
repo: repo,
}
}
// StartOperation creates a new operation and returns its ID.
// The operation starts in "running" status.
func (s *OperationService) StartOperation(
ctx context.Context,
projectID string,
opType domain.OperationType,
input map[string]any,
requestID string,
) (string, error) {
op := &domain.Operation{
ID: uuid.New().String(),
ProjectID: projectID,
Type: opType,
Status: domain.OperationStatusRunning,
RequestID: requestID,
StartedAt: time.Now(),
Input: input,
Steps: []domain.OperationStep{},
}
log := logging.FromContext(ctx).WithService("operation")
if err := s.repo.Create(ctx, op); err != nil {
log.Error("failed to create operation",
logging.FieldError, err,
logging.FieldProjectID, projectID,
"type", opType,
)
return "", err
}
log.Info("operation started",
"operation_id", op.ID,
logging.FieldProjectID, projectID,
"type", opType,
)
return op.ID, nil
}
// StartStep adds a new step to an operation and marks it as running.
func (s *OperationService) StartStep(ctx context.Context, operationID, stepName string) error {
step := domain.OperationStep{
Name: stepName,
Status: domain.OperationStatusRunning,
StartedAt: time.Now(),
}
log := logging.FromContext(ctx).WithService("operation")
if err := s.repo.AddStep(ctx, operationID, step); err != nil {
log.Error("failed to start step",
logging.FieldError, err,
"operation_id", operationID,
"step", stepName,
)
return err
}
return nil
}
// CompleteStep marks a step as completed with optional output.
func (s *OperationService) CompleteStep(
ctx context.Context,
operationID, stepName string,
startedAt time.Time,
output map[string]any,
) error {
step := domain.OperationStep{
Name: stepName,
Status: domain.OperationStatusCompleted,
StartedAt: startedAt,
DurationMs: time.Since(startedAt).Milliseconds(),
Output: output,
}
log := logging.FromContext(ctx).WithService("operation")
if err := s.repo.UpdateStep(ctx, operationID, step); err != nil {
log.Error("failed to complete step",
logging.FieldError, err,
"operation_id", operationID,
"step", stepName,
)
return err
}
return nil
}
// FailStep marks a step as failed with error details.
func (s *OperationService) FailStep(
ctx context.Context,
operationID, stepName string,
startedAt time.Time,
errMsg, errDetail string,
) error {
step := domain.OperationStep{
Name: stepName,
Status: domain.OperationStatusFailed,
StartedAt: startedAt,
DurationMs: time.Since(startedAt).Milliseconds(),
Error: errMsg,
ErrorDetail: domain.TruncateErrorDetail(errDetail),
}
log := logging.FromContext(ctx).WithService("operation")
if err := s.repo.UpdateStep(ctx, operationID, step); err != nil {
log.Error("failed to fail step",
logging.FieldError, err,
"operation_id", operationID,
"step", stepName,
)
return err
}
return nil
}
// CompleteOperation marks the operation as completed with optional output.
func (s *OperationService) CompleteOperation(
ctx context.Context,
operationID string,
output map[string]any,
) error {
log := logging.FromContext(ctx).WithService("operation")
if err := s.repo.Complete(ctx, operationID, domain.OperationStatusCompleted, output, "", ""); err != nil {
log.Error("failed to complete operation",
logging.FieldError, err,
"operation_id", operationID,
)
return err
}
log.Info("operation completed",
"operation_id", operationID,
)
return nil
}
// FailOperation marks the operation as failed with error details.
func (s *OperationService) FailOperation(
ctx context.Context,
operationID string,
errMsg, errDetail string,
) error {
log := logging.FromContext(ctx).WithService("operation")
if err := s.repo.Complete(ctx, operationID, domain.OperationStatusFailed, nil, errMsg, errDetail); err != nil {
log.Error("failed to fail operation",
logging.FieldError, err,
"operation_id", operationID,
)
return err
}
log.Info("operation failed",
"operation_id", operationID,
logging.FieldError, errMsg,
)
return nil
}
// SetCommitSHA updates the commit SHA for an operation.
// Called after a git commit is created as part of the operation.
func (s *OperationService) SetCommitSHA(ctx context.Context, operationID, sha string) error {
return s.repo.SetCommitSHA(ctx, operationID, sha)
}
// SetExternalRef updates the external reference for an operation.
// Called when linking to external systems like Woodpecker builds.
func (s *OperationService) SetExternalRef(ctx context.Context, operationID, ref string) error {
op, err := s.repo.Get(ctx, operationID)
if err != nil {
return err
}
op.ExternalRef = ref
return s.repo.Update(ctx, op)
}
// FindByCommit finds the operation that created a specific commit.
// Used to link builds to the operation that triggered them.
func (s *OperationService) FindByCommit(ctx context.Context, projectID, sha string) (*domain.Operation, error) {
return s.repo.GetByCommitSHA(ctx, projectID, sha)
}
// Get retrieves an operation by ID.
func (s *OperationService) Get(ctx context.Context, operationID string) (*domain.Operation, error) {
return s.repo.Get(ctx, operationID)
}
// List returns operations matching the filter criteria.
func (s *OperationService) List(ctx context.Context, filter domain.OperationFilters) ([]*domain.Operation, error) {
return s.repo.List(ctx, filter)
}
// LinkToParent sets the triggered_by field to link to a parent operation.
func (s *OperationService) LinkToParent(ctx context.Context, operationID, parentID string) error {
return s.repo.SetTriggeredBy(ctx, operationID, parentID)
}