diff --git a/internal/util/apiclient/websocket_log_reader.go b/internal/util/apiclient/websocket_log_reader.go new file mode 100644 index 0000000000..0c4d73305a --- /dev/null +++ b/internal/util/apiclient/websocket_log_reader.go @@ -0,0 +1,93 @@ +// Copyright 2024 Daytona Platforms Inc. +// SPDX-License-Identifier: Apache-2.0 + +package apiclient + +import ( + "fmt" + "sync" + "time" + + "github.com/daytonaio/daytona/cmd/daytona/config" + "github.com/daytonaio/daytona/pkg/logs" + logs_view "github.com/daytonaio/daytona/pkg/views/logs" + "github.com/gorilla/websocket" +) + +var workspaceLogsStarted bool + +func ReadWorkspaceLogs(activeProfile config.Profile, workspaceId string, projectNames []string, stopLogs *bool) { + var wg sync.WaitGroup + query := "follow=true" + + logs_view.CalculateLongestPrefixLength(projectNames) + + for index, projectName := range projectNames { + wg.Add(1) + go func(projectName string) { + defer wg.Done() + + for { + // Make sure workspace logs started before showing any project logs + if !workspaceLogsStarted { + time.Sleep(250 * time.Millisecond) + continue + } + + ws, _, err := GetWebsocketConn(fmt.Sprintf("/log/workspace/%s/%s", workspaceId, projectName), &activeProfile, &query) + // We want to retry getting the logs if it fails + if err != nil { + // TODO: return log.Trace once https://github.com/daytonaio/daytona/issues/696 is resolved + // log.Trace(apiclient_util.HandleErrorResponse(res, err)) + time.Sleep(500 * time.Millisecond) + continue + } + + readJSONLog(ws, stopLogs, index) + ws.Close() + break + } + }(projectName) + } + + for { + ws, _, err := GetWebsocketConn(fmt.Sprintf("/log/workspace/%s", workspaceId), &activeProfile, &query) + // We want to retry getting the logs if it fails + if err != nil { + // TODO: return log.Trace once https://github.com/daytonaio/daytona/issues/696 is resolved + // log.Trace(apiclient_util.HandleErrorResponse(res, err)) + time.Sleep(250 * time.Millisecond) + continue + } + + readJSONLog(ws, stopLogs, logs_view.WORKSPACE_INDEX) + ws.Close() + break + } + + wg.Wait() +} + +func readJSONLog(ws *websocket.Conn, stopLogs *bool, index int) { + logEntriesChan := make(chan logs.LogEntry) + go logs_view.DisplayLogs(logEntriesChan, index) + + for { + var logEntry logs.LogEntry + err := ws.ReadJSON(&logEntry) + if err != nil { + fmt.Println(err.Error()) + return + } + + logEntriesChan <- logEntry + + if !workspaceLogsStarted && index == logs_view.WORKSPACE_INDEX { + workspaceLogsStarted = true + } + + if *stopLogs { + return + } + } +} diff --git a/internal/util/log_reader.go b/internal/util/log_reader.go index 787f53012d..a166314e08 100644 --- a/internal/util/log_reader.go +++ b/internal/util/log_reader.go @@ -5,8 +5,12 @@ package util import ( "bufio" + "bytes" "context" + "encoding/json" "io" + + "github.com/daytonaio/daytona/pkg/logs" ) func ReadLog(ctx context.Context, logReader io.Reader, follow bool, c chan []byte, errChan chan error) { @@ -32,3 +36,45 @@ func ReadLog(ctx context.Context, logReader io.Reader, follow bool, c chan []byt } } } + +func ReadJSONLog(ctx context.Context, logReader io.Reader, follow bool, c chan interface{}, errChan chan error) { + var buffer bytes.Buffer + reader := bufio.NewReader(logReader) + delimiter := []byte(logs.LogDelimiter) + + for { + select { + case <-ctx.Done(): + return + default: + byteChunk := make([]byte, 1024) + n, err := reader.Read(byteChunk) + if err != nil { + if err != io.EOF { + errChan <- err + } else if !follow { + errChan <- io.EOF + return + } + } + buffer.Write(byteChunk[:n]) + data := buffer.Bytes() + + index := bytes.Index(data, delimiter) + + if index != -1 { // if the delimiter is found, process the log entry + + var logEntry logs.LogEntry + + err = json.Unmarshal(data[:index], &logEntry) + if err != nil { + return + } + + c <- logEntry + buffer.Reset() + buffer.Write(data[index+len(delimiter):]) // write remaining data to buffer + } + } + } +} diff --git a/pkg/api/controllers/log/websocket.go b/pkg/api/controllers/log/websocket.go index bb0d49ae07..e508b7b861 100644 --- a/pkg/api/controllers/log/websocket.go +++ b/pkg/api/controllers/log/websocket.go @@ -83,6 +83,69 @@ func readLog(ginCtx *gin.Context, logReader io.Reader) { } } +func writeJSONToWs(ws *websocket.Conn, c chan interface{}, errChan chan error) { + for { + value := <-c + err := ws.WriteJSON(value) + if err != nil { + errChan <- err + break + } + } +} + +func readJSONLog(ginCtx *gin.Context, logReader io.Reader) { + followQuery := ginCtx.Query("follow") + follow := followQuery == "true" + + ws, err := upgrader.Upgrade(ginCtx.Writer, ginCtx.Request, nil) + if err != nil { + log.Error(err) + return + } + defer ws.Close() + + msgChannel := make(chan interface{}) + errChannel := make(chan error) + ctx, cancel := context.WithCancel(context.Background()) + + defer cancel() + go util.ReadJSONLog(ctx, logReader, follow, msgChannel, errChannel) + go writeJSONToWs(ws, msgChannel, errChannel) + + go func() { + for { + select { + case <-ctx.Done(): + return + default: + err := <-errChannel + if err != nil { + if err.Error() != "EOF" { + log.Error(err) + } + ws.Close() + cancel() + } + } + } + }() + + for { + select { + case <-ctx.Done(): + return + default: + _, _, err := ws.ReadMessage() + if err != nil { + ws.Close() + cancel() + return + } + } + } +} + func ReadServerLog(ginCtx *gin.Context) { server := server.GetInstance(nil) @@ -106,7 +169,7 @@ func ReadWorkspaceLog(ginCtx *gin.Context) { return } - readLog(ginCtx, wsLogReader) + readJSONLog(ginCtx, wsLogReader) } func ReadProjectLog(ginCtx *gin.Context) { @@ -121,5 +184,5 @@ func ReadProjectLog(ginCtx *gin.Context) { return } - readLog(ginCtx, projectLogReader) + readJSONLog(ginCtx, projectLogReader) } diff --git a/pkg/builder/builder.go b/pkg/builder/builder.go index 5c0171d378..37f8ee2dac 100644 --- a/pkg/builder/builder.go +++ b/pkg/builder/builder.go @@ -9,7 +9,7 @@ import ( "path/filepath" "github.com/daytonaio/daytona/pkg/gitprovider" - "github.com/daytonaio/daytona/pkg/logger" + "github.com/daytonaio/daytona/pkg/logs" "github.com/daytonaio/daytona/pkg/server/containerregistries" "github.com/daytonaio/daytona/pkg/workspace" ) @@ -30,7 +30,7 @@ type BuilderConfig struct { // Namespace to be used when tagging and pushing the build image BuildImageNamespace string BasePath string - LoggerFactory logger.LoggerFactory + LoggerFactory logs.LoggerFactory DefaultProjectImage string DefaultProjectUser string DefaultProjectPostStartCommands []string @@ -56,7 +56,7 @@ type Builder struct { buildImageNamespace string serverConfigFolder string basePath string - loggerFactory logger.LoggerFactory + loggerFactory logs.LoggerFactory defaultProjectImage string defaultProjectUser string defaultProjectPostStartCommands []string diff --git a/pkg/builder/devcontainer.go b/pkg/builder/devcontainer.go index dc7b70e2b5..1f034301f6 100644 --- a/pkg/builder/devcontainer.go +++ b/pkg/builder/devcontainer.go @@ -20,6 +20,7 @@ import ( "github.com/daytonaio/daytona/pkg/builder/devcontainer" "github.com/daytonaio/daytona/pkg/containerregistry" "github.com/daytonaio/daytona/pkg/docker" + "github.com/daytonaio/daytona/pkg/logs" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/mount" @@ -92,7 +93,7 @@ func (b *DevcontainerBuilder) CleanUp() error { } func (b *DevcontainerBuilder) Publish() error { - projectLogger := b.loggerFactory.CreateProjectLogger(b.project.WorkspaceId, b.project.Name) + projectLogger := b.loggerFactory.CreateProjectLogger(b.project.WorkspaceId, b.project.Name, logs.LogSourceBuilder) defer projectLogger.Close() cliBuilder, err := b.getBuilderDockerClient() @@ -113,7 +114,7 @@ func (b *DevcontainerBuilder) Publish() error { } func (b *DevcontainerBuilder) buildDevcontainer() error { - projectLogger := b.loggerFactory.CreateProjectLogger(b.project.WorkspaceId, b.project.Name) + projectLogger := b.loggerFactory.CreateProjectLogger(b.project.WorkspaceId, b.project.Name, logs.LogSourceBuilder) defer projectLogger.Close() cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) @@ -204,7 +205,7 @@ func (b *DevcontainerBuilder) buildDevcontainer() error { } func (b *DevcontainerBuilder) readConfiguration() error { - projectLogger := b.loggerFactory.CreateProjectLogger(b.project.WorkspaceId, b.project.Name) + projectLogger := b.loggerFactory.CreateProjectLogger(b.project.WorkspaceId, b.project.Name, logs.LogSourceBuilder) defer projectLogger.Close() cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) @@ -278,7 +279,7 @@ func (b *DevcontainerBuilder) readConfiguration() error { func (b *DevcontainerBuilder) startContainer() error { ctx := context.Background() - projectLogger := b.loggerFactory.CreateProjectLogger(b.project.WorkspaceId, b.project.Name) + projectLogger := b.loggerFactory.CreateProjectLogger(b.project.WorkspaceId, b.project.Name, logs.LogSourceBuilder) defer projectLogger.Close() cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) diff --git a/pkg/builder/factory.go b/pkg/builder/factory.go index e7c3a996b0..9e8c9e349a 100644 --- a/pkg/builder/factory.go +++ b/pkg/builder/factory.go @@ -12,7 +12,7 @@ import ( "github.com/daytonaio/daytona/pkg/git" "github.com/daytonaio/daytona/pkg/gitprovider" - "github.com/daytonaio/daytona/pkg/logger" + "github.com/daytonaio/daytona/pkg/logs" "github.com/daytonaio/daytona/pkg/ports" "github.com/daytonaio/daytona/pkg/server/containerregistries" "github.com/daytonaio/daytona/pkg/workspace" @@ -30,7 +30,7 @@ type BuilderFactory struct { containerRegistryServer string buildImageNamespace string basePath string - loggerFactory logger.LoggerFactory + loggerFactory logs.LoggerFactory image string containerRegistryService containerregistries.IContainerRegistryService defaultProjectImage string @@ -68,7 +68,7 @@ func (f *BuilderFactory) Create(p workspace.Project, gpc *gitprovider.GitProvide return nil, err } - projectLogger := f.loggerFactory.CreateProjectLogger(p.WorkspaceId, p.Name) + projectLogger := f.loggerFactory.CreateProjectLogger(p.WorkspaceId, p.Name, logs.LogSourceBuilder) defer projectLogger.Close() gitservice := git.Service{ diff --git a/pkg/cmd/server/serve.go b/pkg/cmd/server/serve.go index 02c491395a..603797b95a 100644 --- a/pkg/cmd/server/serve.go +++ b/pkg/cmd/server/serve.go @@ -17,7 +17,7 @@ import ( "github.com/daytonaio/daytona/pkg/apikey" "github.com/daytonaio/daytona/pkg/builder" "github.com/daytonaio/daytona/pkg/db" - "github.com/daytonaio/daytona/pkg/logger" + "github.com/daytonaio/daytona/pkg/logs" "github.com/daytonaio/daytona/pkg/provider/manager" "github.com/daytonaio/daytona/pkg/provisioner" "github.com/daytonaio/daytona/pkg/server" @@ -63,7 +63,7 @@ var ServeCmd = &cobra.Command{ if err != nil { log.Fatal(err) } - loggerFactory := logger.NewLoggerFactory(logsDir) + loggerFactory := logs.NewLoggerFactory(logsDir) dbPath, err := getDbPath() if err != nil { diff --git a/pkg/cmd/workspace/create.go b/pkg/cmd/workspace/create.go index 1787f09129..db111294af 100644 --- a/pkg/cmd/workspace/create.go +++ b/pkg/cmd/workspace/create.go @@ -10,22 +10,21 @@ import ( "net/url" "os" "strings" - "sync" "time" - "github.com/daytonaio/daytona/internal" "github.com/daytonaio/daytona/internal/cmd/tailscale" "github.com/daytonaio/daytona/internal/util" apiclient_util "github.com/daytonaio/daytona/internal/util/apiclient" ssh_config "github.com/daytonaio/daytona/pkg/agent/ssh/config" "github.com/daytonaio/daytona/pkg/apiclient" workspace_util "github.com/daytonaio/daytona/pkg/cmd/workspace/util" + "github.com/daytonaio/daytona/pkg/logs" "github.com/daytonaio/daytona/pkg/views" + logs_view "github.com/daytonaio/daytona/pkg/views/logs" "github.com/daytonaio/daytona/pkg/views/target" "github.com/daytonaio/daytona/pkg/views/workspace/info" "github.com/daytonaio/daytona/pkg/workspace" "github.com/docker/docker/pkg/stringid" - "github.com/gorilla/websocket" "tailscale.com/tsnet" log "github.com/sirupsen/logrus" @@ -110,6 +109,19 @@ var CreateCmd = &cobra.Command{ projects[i].EnvVars = getEnvVariables(&projects[i], profileData) } + projectNames := []string{} + for _, project := range projects { + projectNames = append(projectNames, project.Name) + } + + logs_view.CalculateLongestPrefixLength(projectNames) + + requestSubmittedLog := logs.LogEntry{ + Msg: "Request submitted\n", + } + + logs_view.DisplayLogEntry(requestSubmittedLog, logs_view.WORKSPACE_INDEX) + target, err := getTarget(activeProfile.Name) if err != nil { log.Fatal(err) @@ -129,7 +141,7 @@ var CreateCmd = &cobra.Command{ id := stringid.GenerateRandomID() id = stringid.TruncateID(id) - go readWorkspaceLogs(activeProfile, id, projects, &stopLogs) + go apiclient_util.ReadWorkspaceLogs(activeProfile, id, projectNames, &stopLogs) createdWorkspace, res, err := apiClient.WorkspaceAPI.CreateWorkspace(ctx).Workspace(apiclient.CreateWorkspaceRequest{ Id: &id, @@ -283,51 +295,6 @@ func processCmdArguments(args []string, apiClient *apiclient.APIClient, projects return nil } -func readWorkspaceLogs(activeProfile config.Profile, workspaceId string, projects []apiclient.CreateWorkspaceRequestProject, stopLogs *bool) { - var wg sync.WaitGroup - for _, project := range projects { - wg.Add(1) - go func(project apiclient.CreateWorkspaceRequestProject) { - defer wg.Done() - query := "follow=true" - - for { - ws, _, err := apiclient_util.GetWebsocketConn(fmt.Sprintf("/log/workspace/%s/%s", workspaceId, project.Name), &activeProfile, &query) - // We want to retry getting the logs if it fails - if err != nil { - // TODO: return log.Trace once https://github.com/daytonaio/daytona/issues/696 is resolved - // log.Trace(apiclient_util.HandleErrorResponse(res, err)) - time.Sleep(500 * time.Millisecond) - continue - } - - readLog(ws, stopLogs) - ws.Close() - break - } - }(project) - } - - query := "follow=true" - - for { - ws, _, err := apiclient_util.GetWebsocketConn(fmt.Sprintf("/log/workspace/%s", workspaceId), &activeProfile, &query) - // We want to retry getting the logs if it fails - if err != nil { - // TODO: return log.Trace once https://github.com/daytonaio/daytona/issues/696 is resolved - // log.Trace(apiclient_util.HandleErrorResponse(res, err)) - time.Sleep(500 * time.Millisecond) - continue - } - - readLog(ws, stopLogs) - ws.Close() - break - } - - wg.Wait() -} - func waitForDial(tsConn *tsnet.Server, workspaceId string, projectName string, dialStartTime time.Time, dialTimeout time.Duration) error { for { if time.Since(dialStartTime) > dialTimeout { @@ -345,31 +312,6 @@ func waitForDial(tsConn *tsnet.Server, workspaceId string, projectName string, d return nil } -func readLog(ws *websocket.Conn, stopLogs *bool) { - if internal.Version == "v0.0.0-dev" { - err := ws.SetReadDeadline(time.Time{}) - if err != nil { - panic(err) - } - } - for { - _, msg, err := ws.ReadMessage() - if err != nil { - return - } - - if *stopLogs { - return - } - - fmt.Print(string(msg)) - - if *stopLogs { - return - } - } -} - func getEnvVariables(project *apiclient.CreateWorkspaceRequestProject, profileData *apiclient.ProfileData) *map[string]string { envVars := map[string]string{} diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go deleted file mode 100644 index 08495be49a..0000000000 --- a/pkg/logger/logger.go +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2024 Daytona Platforms Inc. -// SPDX-License-Identifier: Apache-2.0 - -package logger - -import ( - "io" -) - -type Logger interface { - io.WriteCloser - Cleanup() error -} - -type LoggerFactory interface { - CreateWorkspaceLogger(workspaceId string) Logger - CreateProjectLogger(workspaceId, projectName string) Logger - CreateWorkspaceLogReader(workspaceId string) (io.Reader, error) - CreateProjectLogReader(workspaceId, projectName string) (io.Reader, error) -} - -type loggerFactoryImpl struct { - logsDir string -} - -func NewLoggerFactory(logsDir string) LoggerFactory { - return &loggerFactoryImpl{logsDir: logsDir} -} diff --git a/pkg/logs/logger.go b/pkg/logs/logger.go new file mode 100644 index 0000000000..d717a723e5 --- /dev/null +++ b/pkg/logs/logger.go @@ -0,0 +1,47 @@ +// Copyright 2024 Daytona Platforms Inc. +// SPDX-License-Identifier: Apache-2.0 + +package logs + +import ( + "io" +) + +var LogDelimiter = "!-#_^*|\n" + +type Logger interface { + io.WriteCloser + Cleanup() error +} + +type LogSource string + +const ( + LogSourceServer LogSource = "server" + LogSourceProvider LogSource = "provider" + LogSourceBuilder LogSource = "builder" +) + +type LogEntry struct { + Source string `json:"source"` + WorkspaceId string `json:"workspaceId"` + ProjectName string `json:"projectName"` + Msg string `json:"msg"` + Level string `json:"level"` + Time string `json:"time"` +} + +type LoggerFactory interface { + CreateWorkspaceLogger(workspaceId string, source LogSource) Logger + CreateProjectLogger(workspaceId, projectName string, source LogSource) Logger + CreateWorkspaceLogReader(workspaceId string) (io.Reader, error) + CreateProjectLogReader(workspaceId, projectName string) (io.Reader, error) +} + +type loggerFactoryImpl struct { + logsDir string +} + +func NewLoggerFactory(logsDir string) LoggerFactory { + return &loggerFactoryImpl{logsDir: logsDir} +} diff --git a/pkg/logger/project.go b/pkg/logs/project.go similarity index 62% rename from pkg/logger/project.go rename to pkg/logs/project.go index 732e466e4a..9656896efa 100644 --- a/pkg/logger/project.go +++ b/pkg/logs/project.go @@ -1,12 +1,15 @@ // Copyright 2024 Daytona Platforms Inc. // SPDX-License-Identifier: Apache-2.0 -package logger +package logs import ( + "encoding/json" "io" "os" "path/filepath" + + "github.com/sirupsen/logrus" ) type projectLogger struct { @@ -14,6 +17,8 @@ type projectLogger struct { workspaceId string projectName string logFile *os.File + logger *logrus.Logger + source LogSource } func (pl *projectLogger) Write(p []byte) (n int, err error) { @@ -21,17 +26,36 @@ func (pl *projectLogger) Write(p []byte) (n int, err error) { filePath := filepath.Join(pl.logsDir, pl.workspaceId, pl.projectName, "log") err = os.MkdirAll(filepath.Dir(filePath), 0755) if err != nil { - return 0, err + return len(p), err } logFile, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { - return 0, err + return len(p), err } pl.logFile = logFile + pl.logger.SetOutput(pl.logFile) } - return pl.logFile.Write(p) + var entry LogEntry + entry.Msg = string(p) + entry.Source = string(pl.source) + entry.WorkspaceId = pl.workspaceId + entry.ProjectName = pl.projectName + + b, err := json.Marshal(entry) + if err != nil { + return len(p), err + } + + b = append(b, []byte(LogDelimiter)...) + + _, err = pl.logFile.Write(b) + if err != nil { + return len(p), err + } + + return len(p), nil } func (pl *projectLogger) Close() error { @@ -56,8 +80,16 @@ func (pl *projectLogger) Cleanup() error { return os.RemoveAll(projectLogsDir) } -func (l *loggerFactoryImpl) CreateProjectLogger(workspaceId, projectName string) Logger { - return &projectLogger{workspaceId: workspaceId, logsDir: l.logsDir, projectName: projectName} +func (l *loggerFactoryImpl) CreateProjectLogger(workspaceId, projectName string, source LogSource) Logger { + logger := logrus.New() + + return &projectLogger{ + workspaceId: workspaceId, + logsDir: l.logsDir, + projectName: projectName, + logger: logger, + source: source, + } } func (l *loggerFactoryImpl) CreateProjectLogReader(workspaceId, projectName string) (io.Reader, error) { diff --git a/pkg/logger/workspace.go b/pkg/logs/workspace.go similarity index 63% rename from pkg/logger/workspace.go rename to pkg/logs/workspace.go index f8706f311f..d1222ba53b 100644 --- a/pkg/logger/workspace.go +++ b/pkg/logs/workspace.go @@ -1,18 +1,23 @@ // Copyright 2024 Daytona Platforms Inc. // SPDX-License-Identifier: Apache-2.0 -package logger +package logs import ( + "encoding/json" "io" "os" "path/filepath" + + "github.com/sirupsen/logrus" ) type workspaceLogger struct { logsDir string workspaceId string logFile *os.File + logger *logrus.Logger + source LogSource } func (w *workspaceLogger) Write(p []byte) (n int, err error) { @@ -20,16 +25,34 @@ func (w *workspaceLogger) Write(p []byte) (n int, err error) { filePath := filepath.Join(w.logsDir, w.workspaceId, "log") err = os.MkdirAll(filepath.Dir(filePath), 0755) if err != nil { - return 0, err + return len(p), err } logFile, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { - return 0, err + return len(p), err } w.logFile = logFile + w.logger.SetOutput(w.logFile) } - return w.logFile.Write(p) + var entry LogEntry + entry.Msg = string(p) + entry.Source = string(w.source) + entry.WorkspaceId = w.workspaceId + + b, err := json.Marshal(entry) + if err != nil { + return len(p), err + } + + b = append(b, []byte(LogDelimiter)...) + + _, err = w.logFile.Write(b) + if err != nil { + return len(p), err + } + + return len(p), nil } func (w *workspaceLogger) Close() error { @@ -54,8 +77,15 @@ func (w *workspaceLogger) Cleanup() error { return os.RemoveAll(workspaceLogsDir) } -func (l *loggerFactoryImpl) CreateWorkspaceLogger(workspaceId string) Logger { - return &workspaceLogger{workspaceId: workspaceId, logsDir: l.logsDir} +func (l *loggerFactoryImpl) CreateWorkspaceLogger(workspaceId string, source LogSource) Logger { + logger := logrus.New() + + return &workspaceLogger{ + workspaceId: workspaceId, + logsDir: l.logsDir, + logger: logger, + source: source, + } } func (l *loggerFactoryImpl) CreateWorkspaceLogReader(workspaceId string) (io.Reader, error) { diff --git a/pkg/server/workspaces/create.go b/pkg/server/workspaces/create.go index a0e9f5c537..33f1974ffd 100644 --- a/pkg/server/workspaces/create.go +++ b/pkg/server/workspaces/create.go @@ -12,6 +12,7 @@ import ( "github.com/daytonaio/daytona/pkg/builder" "github.com/daytonaio/daytona/pkg/containerregistry" "github.com/daytonaio/daytona/pkg/gitprovider" + "github.com/daytonaio/daytona/pkg/logs" "github.com/daytonaio/daytona/pkg/provider" "github.com/daytonaio/daytona/pkg/server/workspaces/dto" "github.com/daytonaio/daytona/pkg/workspace" @@ -180,8 +181,10 @@ func (s *WorkspaceService) createWorkspace(ws *workspace.Workspace) (*workspace. return ws, err } - wsLogger := s.loggerFactory.CreateWorkspaceLogger(ws.Id) - wsLogger.Write([]byte("Creating workspace\n")) + wsLogger := s.loggerFactory.CreateWorkspaceLogger(ws.Id, logs.LogSourceServer) + defer wsLogger.Close() + + wsLogger.Write([]byte(fmt.Sprintf("Creating workspace %s (%s)\n", ws.Name, ws.Id))) err = s.provisioner.CreateWorkspace(ws, target) if err != nil { @@ -189,7 +192,7 @@ func (s *WorkspaceService) createWorkspace(ws *workspace.Workspace) (*workspace. } for i, project := range ws.Projects { - projectLogger := s.loggerFactory.CreateProjectLogger(ws.Id, project.Name) + projectLogger := s.loggerFactory.CreateProjectLogger(ws.Id, project.Name, logs.LogSourceServer) defer projectLogger.Close() gc, _ := s.gitProviderService.GetConfigForUrl(project.Repository.Url) diff --git a/pkg/server/workspaces/remove.go b/pkg/server/workspaces/remove.go index d9b6c29f31..501fa3895c 100644 --- a/pkg/server/workspaces/remove.go +++ b/pkg/server/workspaces/remove.go @@ -6,6 +6,7 @@ package workspaces import ( "fmt" + "github.com/daytonaio/daytona/pkg/logs" log "github.com/sirupsen/logrus" ) @@ -47,7 +48,7 @@ func (s *WorkspaceService) RemoveWorkspace(workspaceId string) error { // Should not fail the whole operation if the API key cannot be revoked log.Error(err) } - projectLogger := s.loggerFactory.CreateProjectLogger(workspace.Id, project.Name) + projectLogger := s.loggerFactory.CreateProjectLogger(workspace.Id, project.Name, logs.LogSourceServer) err = projectLogger.Cleanup() if err != nil { // Should not fail the whole operation if the project logger cannot be cleaned up @@ -55,7 +56,7 @@ func (s *WorkspaceService) RemoveWorkspace(workspaceId string) error { } } - logger := s.loggerFactory.CreateWorkspaceLogger(workspace.Id) + logger := s.loggerFactory.CreateWorkspaceLogger(workspace.Id, logs.LogSourceServer) err = logger.Cleanup() if err != nil { // Should not fail the whole operation if the workspace logger cannot be cleaned up @@ -106,7 +107,7 @@ func (s *WorkspaceService) ForceRemoveWorkspace(workspaceId string) error { log.Error(err) } - projectLogger := s.loggerFactory.CreateProjectLogger(workspace.Id, project.Name) + projectLogger := s.loggerFactory.CreateProjectLogger(workspace.Id, project.Name, logs.LogSourceServer) err = projectLogger.Cleanup() if err != nil { log.Error(err) diff --git a/pkg/server/workspaces/service.go b/pkg/server/workspaces/service.go index 88da0b2ddf..8df32380ce 100644 --- a/pkg/server/workspaces/service.go +++ b/pkg/server/workspaces/service.go @@ -8,7 +8,7 @@ import ( "io" "github.com/daytonaio/daytona/pkg/builder" - "github.com/daytonaio/daytona/pkg/logger" + "github.com/daytonaio/daytona/pkg/logs" "github.com/daytonaio/daytona/pkg/provider" "github.com/daytonaio/daytona/pkg/provisioner" "github.com/daytonaio/daytona/pkg/server/apikeys" @@ -48,7 +48,7 @@ type WorkspaceServiceConfig struct { DefaultProjectUser string DefaultProjectPostStartCommands []string ApiKeyService apikeys.IApiKeyService - LoggerFactory logger.LoggerFactory + LoggerFactory logs.LoggerFactory GitProviderService gitproviders.IGitProviderService BuilderFactory builder.IBuilderFactory } @@ -82,7 +82,7 @@ type WorkspaceService struct { defaultProjectImage string defaultProjectUser string defaultProjectPostStartCommands []string - loggerFactory logger.LoggerFactory + loggerFactory logs.LoggerFactory gitProviderService gitproviders.IGitProviderService builderFactory builder.IBuilderFactory } diff --git a/pkg/server/workspaces/service_test.go b/pkg/server/workspaces/service_test.go index c0b6190f59..ef3399b022 100644 --- a/pkg/server/workspaces/service_test.go +++ b/pkg/server/workspaces/service_test.go @@ -14,7 +14,7 @@ import ( "github.com/daytonaio/daytona/pkg/apikey" "github.com/daytonaio/daytona/pkg/containerregistry" "github.com/daytonaio/daytona/pkg/gitprovider" - "github.com/daytonaio/daytona/pkg/logger" + "github.com/daytonaio/daytona/pkg/logs" "github.com/daytonaio/daytona/pkg/provider" "github.com/daytonaio/daytona/pkg/server/workspaces" "github.com/daytonaio/daytona/pkg/server/workspaces/dto" @@ -99,7 +99,7 @@ func TestWorkspaceService(t *testing.T) { DefaultProjectPostStartCommands: defaultProjectPostStartCommands, ApiKeyService: apiKeyService, Provisioner: provisioner, - LoggerFactory: logger.NewLoggerFactory(logsDir), + LoggerFactory: logs.NewLoggerFactory(logsDir), GitProviderService: gitProviderService, BuilderFactory: mockBuilderFactory, }) diff --git a/pkg/server/workspaces/start.go b/pkg/server/workspaces/start.go index c17d6bda4c..2f864ed469 100644 --- a/pkg/server/workspaces/start.go +++ b/pkg/server/workspaces/start.go @@ -7,6 +7,7 @@ import ( "fmt" "io" + "github.com/daytonaio/daytona/pkg/logs" "github.com/daytonaio/daytona/pkg/provider" "github.com/daytonaio/daytona/pkg/workspace" @@ -24,7 +25,7 @@ func (s *WorkspaceService) StartWorkspace(workspaceId string) error { return err } - workspaceLogger := s.loggerFactory.CreateWorkspaceLogger(w.Id) + workspaceLogger := s.loggerFactory.CreateWorkspaceLogger(w.Id, logs.LogSourceServer) defer workspaceLogger.Close() wsLogWriter := io.MultiWriter(&util.InfoLogWriter{}, workspaceLogger) @@ -48,7 +49,7 @@ func (s *WorkspaceService) StartProject(workspaceId, projectName string) error { return err } - projectLogger := s.loggerFactory.CreateProjectLogger(w.Id, project.Name) + projectLogger := s.loggerFactory.CreateProjectLogger(w.Id, project.Name, logs.LogSourceServer) defer projectLogger.Close() return s.startProject(project, target, projectLogger) @@ -63,7 +64,7 @@ func (s *WorkspaceService) startWorkspace(workspace *workspace.Workspace, target } for _, project := range workspace.Projects { - projectLogger := s.loggerFactory.CreateProjectLogger(workspace.Id, project.Name) + projectLogger := s.loggerFactory.CreateProjectLogger(workspace.Id, project.Name, logs.LogSourceServer) defer projectLogger.Close() err = s.startProject(project, target, projectLogger) diff --git a/pkg/views/logs/display.go b/pkg/views/logs/display.go new file mode 100644 index 0000000000..952d4937eb --- /dev/null +++ b/pkg/views/logs/display.go @@ -0,0 +1,91 @@ +// Copyright 2024 Daytona Platforms Inc. +// SPDX-License-Identifier: Apache-2.0 + +package logs + +import ( + "fmt" + "strings" + + "github.com/charmbracelet/lipgloss" + "github.com/daytonaio/daytona/pkg/logs" + "github.com/daytonaio/daytona/pkg/views" +) + +var WORKSPACE_INDEX = -1 +var WORKSPACE_PREFIX = "WORKSPACE" + +var longestPrefixLength = len(WORKSPACE_PREFIX) +var maxPrefixLength = 20 + +func DisplayLogs(logEntriesChan <-chan logs.LogEntry, index int) { + for logEntry := range logEntriesChan { + DisplayLogEntry(logEntry, index) + } +} + +func DisplayLogEntry(logEntry logs.LogEntry, index int) { + line := logEntry.Msg + + prefixColor := getPrefixColor(index) + prefixText := logEntry.ProjectName + + if index == WORKSPACE_INDEX { + prefixText = WORKSPACE_PREFIX + } + + prefix := lipgloss.NewStyle().Foreground(prefixColor).Bold(true).Render(formatPrefixText(prefixText)) + + if index == WORKSPACE_INDEX { + line = fmt.Sprintf(" %s%s \033[1m%s\033[0m", prefix, views.CheckmarkSymbol, line) + } else { + // Check if carriage return exists and if it does, remove the characters before it unless it is the last character of the line + lastIndex := strings.LastIndex(line, "\r") + if lastIndex != -1 { + if !strings.HasSuffix(line, "\r") && !strings.HasSuffix(line, "\r\n") { + line = line[lastIndex+1:] + } + } + + line = fmt.Sprintf("\r %s%s", prefix, line) + } + + fmt.Print(line) +} + +func CalculateLongestPrefixLength(projectNames []string) { + for _, projectName := range projectNames { + if len(projectName) > longestPrefixLength { + longestPrefixLength = len(projectName) + } + } +} + +func formatPrefixText(input string) string { + prefixLength := longestPrefixLength + if prefixLength > maxPrefixLength { + prefixLength = maxPrefixLength + longestPrefixLength = maxPrefixLength + } + + // Trim input if longer than maxPrefixLength + if len(input) > prefixLength { + input = input[:prefixLength-3] + input += "..." + } + + // Pad input with spaces if shorter than maxPrefixLength + for len(input) < prefixLength { + input += " " + } + + input += " | " + return input +} + +func getPrefixColor(index int) lipgloss.AdaptiveColor { + if index == WORKSPACE_INDEX { + return views.Green + } + return views.LogPrefixColors[index%len(views.LogPrefixColors)] +} diff --git a/pkg/views/styles.go b/pkg/views/styles.go index 20136a322e..2115279dca 100644 --- a/pkg/views/styles.go +++ b/pkg/views/styles.go @@ -13,8 +13,11 @@ import ( var ( Green = lipgloss.AdaptiveColor{Light: "#23cc71", Dark: "#23cc71"} + Blue = lipgloss.AdaptiveColor{Light: "#017ffe", Dark: "#017ffe"} + Yellow = lipgloss.AdaptiveColor{Light: "#d4ed2d", Dark: "#d4ed2d"} + Cyan = lipgloss.AdaptiveColor{Light: "#3ef7e5", Dark: "#3ef7e5"} DimmedGreen = lipgloss.AdaptiveColor{Light: "#7be0a9", Dark: "#7be0a9"} - Orange = lipgloss.AdaptiveColor{Light: "#eb9834", Dark: "#eb9834"} + Orange = lipgloss.AdaptiveColor{Light: "#e3881b", Dark: "#e3881b"} Light = lipgloss.AdaptiveColor{Light: "#000", Dark: "#fff"} Dark = lipgloss.AdaptiveColor{Light: "#fff", Dark: "#000"} Gray = lipgloss.AdaptiveColor{Light: "243", Dark: "243"} @@ -37,6 +40,10 @@ var ( TableHeaderStyle = BaseCellStyle.Copy().Foreground(LightGray).Bold(false).Padding(0).MarginRight(4) ) +var LogPrefixColors = []lipgloss.AdaptiveColor{ + Blue, Yellow, Orange, Cyan, +} + func GetStyledSelectList(items []list.Item) list.Model { d := list.NewDefaultDelegate()