diff --git a/internal/commands/execute_workflow.go b/internal/commands/execute_workflow.go index ec64ab3..168b4a0 100644 --- a/internal/commands/execute_workflow.go +++ b/internal/commands/execute_workflow.go @@ -1,11 +1,11 @@ package commands import ( + logger "courgette/internal/logging" runner "courgette/internal/runner" workflow "courgette/internal/workflow" "errors" "fmt" - "log" ) func ExecuteWorkflow(configuration Configuration, workflowFile string) error { @@ -23,19 +23,19 @@ func ExecuteWorkflow(configuration Configuration, workflowFile string) error { workflow, err := workflow.FromYamlFile(workflowFile) if err != nil { - log.Fatalf("%#v", err) + logger.Error(logger.Red("Failed to read workflow (%s)"), workflowFile) + return err } validationErrors := workflow.Validate() if len(validationErrors) > 0 { for _, err := range validationErrors { - log.Printf("Validation error:: %#v", err) + logger.Error(logger.Red("Validation error: %s"), err) } - return errors.New("Jobs encountered errors.") + return errors.New("Workflow validation failed.") } - taskResult := runnerInstance.RunWorkflow(*workflow) if !taskResult.HasError() { @@ -43,7 +43,11 @@ func ExecuteWorkflow(configuration Configuration, workflowFile string) error { } for _, job := range taskResult.Context.Jobs { - log.Printf("Job %s: %s", job.Id, job.Status) + if job.Status == "success" { + logger.Info(logger.Green("Job %s: %s"), job.Id, job.Status) + } else if job.Status == "failed" { + logger.Error(logger.Red("Job %s: %s"), job.Id, job.Status) + } } return fmt.Errorf("Task %s failed with at least 1 error.", taskResult.Id) diff --git a/internal/commands/validate.go b/internal/commands/validate.go index a91e392..0fa5efe 100644 --- a/internal/commands/validate.go +++ b/internal/commands/validate.go @@ -1,27 +1,32 @@ package commands import ( + logger "courgette/internal/logging" workflow "courgette/internal/workflow" "errors" - "log" ) func ValidateWorkflow(configuration Configuration, workflowPath string) error { + logger.Info("Validating workflow at \"%s\".", workflowPath) + workflow, err := workflow.FromYamlFile(workflowPath) if err != nil { - log.Fatalf("%#v", err) + logger.Error(logger.Red("Failed to read and parse workflow from \"%s\"."), workflowPath) + return err } validationErrors := workflow.Validate() if len(validationErrors) > 0 { for _, err := range validationErrors { - log.Printf("Validation error:: %#v", err) + logger.Error(logger.Red("Validation error: %s"), err) } - return errors.New("Jobs encountered errors.") + return errors.New("Workflow validation failed.") } + logger.Info(logger.Green(logger.Bold("✅ Workflow \"%s\" is valid!")), workflowPath) + return nil } diff --git a/internal/logging/colors.go b/internal/logging/colors.go new file mode 100644 index 0000000..98621c1 --- /dev/null +++ b/internal/logging/colors.go @@ -0,0 +1,17 @@ +// Color utilities to enhance text printed to the screen. +// +// See: https://en.wikipedia.org/wiki/ANSI_escape_code + +package logging + +func Bold(text string) string { + return "\033[1m" + text + "\033[0m" +} + +func Green(text string) string { + return "\033[32m" + text + "\033[0m" +} + +func Red(text string) string { + return "\033[31m" + text + "\033[0m" +} diff --git a/internal/logging/logger.go b/internal/logging/logger.go new file mode 100644 index 0000000..724f906 --- /dev/null +++ b/internal/logging/logger.go @@ -0,0 +1,44 @@ +// Logging module +// +// This module is a wrapper around the built-in `log` package +// and adds more control around if and how logging shows up +// in the code. +package logging + +import ( + "log" + "os" +) + +type Logger struct { + Info log.Logger + Error log.Logger +} + +var Log Logger + +// Configures the loggers and initializes each logging level's instance. +// +// This should be run once and before any logging is done. +func ConfigureLogger() { + Log = Logger{ + Info: *log.New(os.Stdout, "[INFO] ", log.Ldate|log.Ltime), + Error: *log.New(os.Stderr, "[ERROR] ", log.Ldate|log.Ltime), + } +} + +func Info(message string, args ...any) { + if len(args) == 0 { + Log.Info.Print(message) + } else { + Log.Info.Printf(message, args...) + } +} + +func Error(message string, args ...any) { + if len(args) == 0 { + Log.Error.Print(message) + } else { + Log.Error.Printf(message, args...) + } +} diff --git a/internal/runner/runner.go b/internal/runner/runner.go index ec8fa49..cf3c204 100644 --- a/internal/runner/runner.go +++ b/internal/runner/runner.go @@ -1,10 +1,10 @@ package runner import ( + logger "courgette/internal/logging" workflow "courgette/internal/workflow" "errors" "fmt" - "log" "sync" ) @@ -35,7 +35,7 @@ func (r *Runner) DeferTask(task func()) { // Each task is executed within a go routine and the call will // wait until all the tasks are completed before returning. func (r *Runner) RunDeferredTasks() { - log.Printf("Running %d deferred tasks.", len(r.deferred)) + logger.Info("Running %d deferred tasks.", len(r.deferred)) var tracker sync.WaitGroup @@ -84,7 +84,7 @@ func (r *Runner) GetTask(taskId string) *Task { // that the jobs will be executed in, run the jobs's steps and // tear down the container once no longer useful. func (r *Runner) RunWorkflow(workflow workflow.Workflow) Task { - log.Printf("Executing workflow: %s", workflow.SourcePath) + logger.Info("Executing workflow: %s", workflow.SourcePath) task := r.GetTask(r.AddTask()) for _, job := range workflow.Jobs { @@ -94,7 +94,7 @@ func (r *Runner) RunWorkflow(workflow workflow.Workflow) Task { runnerImage := r.GetImageUriByLabel(job.RunsOn) containerName := r.GetContainerName(jobContext.Id) - log.Printf("Using image %s (label: %s)", runnerImage, job.RunsOn) + logger.Info("Using image %s (label: %s)", runnerImage, job.RunsOn) if pullError := r.Driver.Pull(runnerImage); pullError != nil { jobContext.SetStatus("failed").SetError(pullError) @@ -139,13 +139,13 @@ func (r *Runner) RunJobInContainer(imageUri string, containerId string, job work r.Driver.Start(imageUri, containerId) r.DeferTask(func() { - log.Printf("Started cleaning up %s", containerId) + logger.Info("Started cleaning up %s", containerId) r.Driver.Stop(containerId) }) - log.Printf("Started %s", containerId) + logger.Info("Started %s", containerId) for _, step := range job.Steps { - log.Printf("Run: %s", step.Run) + logger.Info("Run: %s", step.Run) if err := r.RunCommandInContainer(containerId, step.Run); err != nil { return err diff --git a/main.go b/main.go index 1aa7d1a..0744059 100644 --- a/main.go +++ b/main.go @@ -3,14 +3,15 @@ package main import ( "context" commands "courgette/internal/commands" + logger "courgette/internal/logging" "github.com/spf13/cobra" - "log" "os" ) var cli = &cobra.Command{ Use: "runner", PersistentPreRun: func(cmd *cobra.Command, args []string) { + logger.ConfigureLogger() configPath, err := cmd.Flags().GetString("config") ctx := cmd.Context() @@ -19,7 +20,7 @@ var cli = &cobra.Command{ configuration, err := commands.NewConfigFromFile(configPath) if err != nil { - log.Printf("Failed to parse configuration (%s)!", configPath) + logger.Error(logger.Red("Failed to parse configuration (%s)!"), configPath) os.Exit(1) } @@ -38,7 +39,7 @@ var execute = &cobra.Command{ config := cmd.Context().Value("config").(*commands.Configuration) if err := commands.ExecuteWorkflow(*config, args[0]); err != nil { - log.Printf("Failure: %s", err) + logger.Error(logger.Red("%s"), err) os.Exit(1) } }, @@ -52,7 +53,7 @@ var validate = &cobra.Command{ config := cmd.Context().Value("config").(*commands.Configuration) if err := commands.ValidateWorkflow(*config, args[0]); err != nil { - log.Printf("Failure: %s", err) + logger.Error(logger.Red("%s"), err) os.Exit(1) } },