diff --git a/README.md b/README.md index 5b3014d..e93eda6 100755 --- a/README.md +++ b/README.md @@ -1,13 +1,11 @@ ## Process Compose -[![made-with-Go](https://img.shields.io/badge/Made%20with-Go-1f425f.svg)](https://go.dev/) [![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://GitHub.com/Naereen/StrapDown.js/graphs/commit-activity) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) ![Go Report](https://goreportcard.com/badge/github.com/F1bonacc1/process-compose) +[![made-with-Go](https://img.shields.io/badge/Made%20with-Go-1f425f.svg)](https://go.dev/) [![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg)](https://GitHub.com/Naereen/StrapDown.js/graphs/commit-activity) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) ![Go Report](https://goreportcard.com/badge/github.com/F1bonacc1/process-compose)[![Releases](https://img.shields.io/github/downloads/F1bonacc1/process-compose/total.svg)]() -**What?** Process Compose is like [docker-compose](https://github.com/docker/compose), but for orchestrating a suite of processes, not containers. +Process Compose is like [docker-compose](https://github.com/docker/compose), but for orchestrating a suite of processes, not containers. **Why?** Because sometimes you just don't want to deal with docker files, volume definitions, networks and docker registries. -**How?** Declare all the system processes dependencies in a single YAML (don't judge) file, monitor the execution and output with a built-in UI. - Main use cases would be: * Processes execution (in parallel or serially) @@ -192,7 +190,17 @@ TUI is the default run mode, but it's possible to disable it: ./process-compose -t=false ``` +Control the UI log buffer size: + +```yaml +log_level: info +log_length: 1200 #default: 1000 +processes: + process2: + command: "ls -R /" +``` +**Note**: Using a too large buffer will put a significant penalty on your CPU. #### ✅ Logger diff --git a/process-compose.yaml b/process-compose.yaml index b2864cd..c3f5f49 100644 --- a/process-compose.yaml +++ b/process-compose.yaml @@ -1,5 +1,6 @@ version: "0.5" log_level: debug +log_length: 3000 processes: process0: command: "ls ddd" diff --git a/src/app/config.go b/src/app/config.go index 0b8f157..dcff264 100644 --- a/src/app/config.go +++ b/src/app/config.go @@ -10,6 +10,7 @@ type Project struct { Version string `yaml:"version"` LogLocation string `yaml:"log_location,omitempty"` LogLevel string `yaml:"log_level,omitempty"` + LogLength int `yaml:"log_length,omitempty"` Processes Processes `yaml:"processes"` Environment []string `yaml:"environment,omitempty"` diff --git a/src/app/project.go b/src/app/project.go index 96ab7ab..44a6220 100644 --- a/src/app/project.go +++ b/src/app/project.go @@ -10,17 +10,25 @@ import ( "strings" "github.com/f1bonacc1/process-compose/src/pclog" + "github.com/joho/godotenv" "github.com/rs/zerolog" "github.com/rs/zerolog/log" "gopkg.in/yaml.v2" ) +const ( + DEFAULT_LOG_LENGTH = 1000 +) + var PROJ *Project -func (p *Project) Run() { +func (p *Project) init() { p.initProcessStates() p.initProcessLogs() +} + +func (p *Project) Run() { p.runningProcesses = make(map[string]*Process) runOrder := []ProcessConfig{} p.WithProcesses([]string{}, func(process ProcessConfig) error { @@ -51,7 +59,9 @@ func (p *Project) runProcess(proc ProcessConfig) { } procLog, err := p.getProcessLog(proc.Name) if err != nil { - procLog = pclog.NewLogBuffer(1000) + // we shouldn't get here + log.Error().Msgf("Error: Can't get log: %s using empty buffer", err.Error()) + procLog = pclog.NewLogBuffer(0) } process := NewProcess(p.Environment, procLogger, proc, p.GetProcessState(proc.Name), procLog, 1) p.addRunningProcess(process) @@ -109,7 +119,7 @@ func (p *Project) initProcessStates() { func (p *Project) initProcessLogs() { p.processLogs = make(map[string]*pclog.ProcessLogBuffer) for key := range p.Processes { - p.processLogs[key] = pclog.NewLogBuffer(1000) + p.processLogs[key] = pclog.NewLogBuffer(p.LogLength) } } @@ -189,7 +199,40 @@ func (p *Project) GetProcessLog(name string, offsetFromEnd, limit int) ([]string if err != nil { return nil, err } - return logs.GetLog(offsetFromEnd, limit), nil + return logs.GetLogRange(offsetFromEnd, limit), nil +} + +func (p *Project) GetProcessLogLine(name string, lineIndex int) (string, error) { + logs, err := p.getProcessLog(name) + if err != nil { + return "", err + } + return logs.GetLogLine(lineIndex), nil +} + +func (p *Project) GetProcessLogLength(name string) int { + logs, err := p.getProcessLog(name) + if err != nil { + return 0 + } + return logs.GetLogLength() +} + +func (p *Project) GetLogsAndSubscribe(name string, observer pclog.PcLogObserver) { + + logs, err := p.getProcessLog(name) + if err != nil { + return + } + logs.GetLogsAndSubscribe(observer) +} + +func (p *Project) UnSubscribeLogger(name string) { + logs, err := p.getProcessLog(name) + if err != nil { + return + } + logs.UnSubscribe() } func (p *Project) getProcesses(names ...string) ([]ProcessConfig, error) { @@ -287,6 +330,7 @@ func CreateProject(inputFile string) *Project { yamlFile = []byte(os.ExpandEnv(string(yamlFile))) var project Project + project.LogLength = DEFAULT_LOG_LENGTH err = yaml.Unmarshal(yamlFile, &project) if err != nil { log.Fatal().Msg(err.Error()) @@ -302,6 +346,7 @@ func CreateProject(inputFile string) *Project { } PROJ = &project + project.init() return &project } diff --git a/src/main.go b/src/main.go index f5262c4..9ca9459 100644 --- a/src/main.go +++ b/src/main.go @@ -105,7 +105,7 @@ func main() { if isTui { defer quiet()() go project.Run() - tui.SetupTui(version) + tui.SetupTui(version, project.LogLength) } else { project.Run() } diff --git a/src/pclog/logs_observer,go b/src/pclog/logs_observer.go similarity index 100% rename from src/pclog/logs_observer,go rename to src/pclog/logs_observer.go diff --git a/src/pclog/process_log_buffer.go b/src/pclog/process_log_buffer.go index fd95a24..8fc2e45 100644 --- a/src/pclog/process_log_buffer.go +++ b/src/pclog/process_log_buffer.go @@ -1,29 +1,42 @@ package pclog +import ( + "sync" +) + const ( slack = 100 ) type ProcessLogBuffer struct { - buffer []string - size int + buffer []string + size int + observer PcLogObserver + mx sync.Mutex } func NewLogBuffer(size int) *ProcessLogBuffer { return &ProcessLogBuffer{ - size: size, - buffer: make([]string, 0, size), + size: size, + buffer: make([]string, 0, size+slack), + observer: nil, } } func (b *ProcessLogBuffer) Write(message string) { + b.mx.Lock() + defer b.mx.Unlock() b.buffer = append(b.buffer, message) if len(b.buffer) > b.size+slack { b.buffer = b.buffer[slack:] } + if b.observer != nil { + b.observer.AddLine(message) + } + } -func (b ProcessLogBuffer) GetLog(offsetFromEnd, limit int) []string { +func (b *ProcessLogBuffer) GetLogRange(offsetFromEnd, limit int) []string { if len(b.buffer) == 0 { return []string{} } @@ -48,3 +61,36 @@ func (b ProcessLogBuffer) GetLog(offsetFromEnd, limit int) []string { } return b.buffer[len(b.buffer)-offsetFromEnd : offsetFromEnd+limit] } + +func (b *ProcessLogBuffer) GetLogLine(lineIndex int) string { + if len(b.buffer) == 0 { + return "" + } + + if lineIndex >= len(b.buffer) { + lineIndex = len(b.buffer) - 1 + } + + if lineIndex < 0 { + lineIndex = 0 + } + + return b.buffer[lineIndex] +} + +func (b *ProcessLogBuffer) GetLogLength() int { + return len(b.buffer) +} + +func (b *ProcessLogBuffer) GetLogsAndSubscribe(observer PcLogObserver) { + b.mx.Lock() + defer b.mx.Unlock() + observer.SetLines(b.buffer) + b.observer = observer +} + +func (b *ProcessLogBuffer) UnSubscribe() { + b.mx.Lock() + defer b.mx.Unlock() + b.observer = nil +} diff --git a/src/tui/log_viewer.go b/src/tui/log_viewer.go new file mode 100644 index 0000000..00042f8 --- /dev/null +++ b/src/tui/log_viewer.go @@ -0,0 +1,63 @@ +package tui + +import ( + "fmt" + "strings" + "sync" + + "github.com/rivo/tview" +) + +type LogView struct { + tview.TextView + isWrapOn bool + buffer *strings.Builder + mx sync.Mutex +} + +func NewLogView(maxLines int) *LogView { + l := &LogView{ + isWrapOn: true, + TextView: *tview.NewTextView().SetDynamicColors(true).SetScrollable(true).SetMaxLines(maxLines), + buffer: &strings.Builder{}, + } + l.SetBorder(true) + return l +} + +func (l *LogView) AddLine(line string) { + l.mx.Lock() + defer l.mx.Unlock() + if strings.Contains(strings.ToLower(line), "error") { + fmt.Fprintf(l.buffer, "[deeppink]%s[-:-:-]\n", tview.Escape(line)) + } else { + fmt.Fprintf(l.buffer, "%s\n", tview.Escape(line)) + } +} + +func (l *LogView) AddLines(lines []string) { + for _, line := range lines { + l.AddLine(line) + } +} + +func (l *LogView) SetLines(lines []string) { + l.Clear() + l.AddLines(lines) +} + +func (l *LogView) ToggleWrap() { + l.isWrapOn = !l.isWrapOn + l.SetWrap(l.isWrapOn) +} + +func (l *LogView) IsWrapOn() bool { + return l.isWrapOn +} + +func (l *LogView) Flush() { + l.mx.Lock() + defer l.mx.Unlock() + l.Write([]byte(l.buffer.String())) + l.buffer.Reset() +} diff --git a/src/tui/view.go b/src/tui/view.go index a696e23..b88afe6 100644 --- a/src/tui/view.go +++ b/src/tui/view.go @@ -4,7 +4,6 @@ import ( "fmt" "os" "strconv" - "strings" "time" "github.com/f1bonacc1/process-compose/src/app" @@ -16,23 +15,25 @@ type pcView struct { procTable *tview.Table statTable *tview.Table appView *tview.Application - logsText *tview.TextView + logsText *LogView statusText *tview.TextView helpText *tview.TextView procNames []string version string - logWrapOn bool + logFollow bool + loggedProc string } -func newPcView(version string) *pcView { +func newPcView(version string, logLength int) *pcView { pv := &pcView{ appView: tview.NewApplication(), - logsText: tview.NewTextView().SetDynamicColors(true).SetScrollable(true), + logsText: NewLogView(logLength), statusText: tview.NewTextView().SetDynamicColors(true), procNames: app.PROJ.GetLexicographicProcessNames(), version: version, - logWrapOn: true, + logFollow: true, helpText: tview.NewTextView().SetDynamicColors(true), + loggedProc: "", } pv.procTable = pv.createProcTable() pv.statTable = pv.createStatTable() @@ -42,15 +43,26 @@ func newPcView(version string) *pcView { switch event.Key() { case tcell.KeyF10: pv.appView.Stop() + case tcell.KeyF5: + pv.logFollow = !pv.logFollow + name := pv.getSelectedProcName() + if pv.logFollow { + pv.followLog(name) + go pv.updateLogs() + } else { + pv.unFollowLog() + } + pv.updateHelpTextView() case tcell.KeyF6: - pv.logWrapOn = !pv.logWrapOn - pv.logsText.SetWrap(pv.logWrapOn) + pv.logsText.ToggleWrap() pv.updateHelpTextView() } return event }) if len(pv.procNames) > 0 { - pv.logsText.SetTitle(pv.procNames[0]) + name := pv.procNames[0] + pv.logsText.SetTitle(name) + pv.followLog(name) } return pv } @@ -85,28 +97,29 @@ func (pv pcView) getSelectedProcName() string { return "" } -func (pv *pcView) fillLogs() { +func (pv *pcView) onTableSelectionChange(row, column int) { name := pv.getSelectedProcName() - logs, err := app.PROJ.GetProcessLog(name, 1000, 0) - if err != nil { - pv.logsText.SetBorder(true).SetTitle(err.Error()) - pv.logsText.Clear() - } else { - //pv.logsText.SetText(strings.Join(logs, "\n")) - pv.logsText.Clear() - for _, line := range logs { - if strings.Contains(strings.ToLower(line), "error") { - fmt.Fprintf(pv.logsText, "[deeppink]%s[-:-:-]\n", tview.Escape(line)) - } else { - fmt.Fprintf(pv.logsText, "%s\n", tview.Escape(line)) - } - } + pv.logsText.SetBorder(true).SetTitle(name) + pv.unFollowLog() + pv.followLog(name) + if !pv.logFollow { + // call follow and unfollow to update the buffer and stop following + // in case the following is disabled + pv.unFollowLog() } } -func (pv *pcView) onTableSelectionChange(row, column int) { - name := pv.getSelectedProcName() - pv.logsText.SetBorder(true).SetTitle(name) +func (pv *pcView) followLog(name string) { + pv.loggedProc = name + pv.logsText.Clear() + app.PROJ.GetLogsAndSubscribe(name, pv.logsText) +} + +func (pv *pcView) unFollowLog() { + if pv.loggedProc != "" { + app.PROJ.UnSubscribeLogger(pv.loggedProc) + } + pv.logsText.Flush() } func (pv *pcView) createProcTable() *tview.Table { @@ -171,11 +184,18 @@ func (pv *pcView) createStatTable() *tview.Table { func (pv *pcView) updateHelpTextView() { wrap := "Wrap On" - if pv.logWrapOn { + if pv.logsText.IsWrapOn() { wrap = "Wrap Off" } + follow := "Follow On" + if pv.logFollow { + follow = "Follow Off" + } pv.helpText.Clear() + fmt.Fprintf(pv.helpText, "%s ", "[lightskyblue:]LOGS:[-:-:-]") + fmt.Fprintf(pv.helpText, "%s%s%s ", "F5[black:green]", follow, "[-:-:-]") fmt.Fprintf(pv.helpText, "%s%s%s ", "F6[black:green]", wrap, "[-:-:-]") + fmt.Fprintf(pv.helpText, "%s ", "[lightskyblue::b]PROCESS:[-:-:-]") fmt.Fprintf(pv.helpText, "%s ", "F7[black:green]Start[-:-:-]") fmt.Fprintf(pv.helpText, "%s ", "F9[black:green]Kill[-:-:-]") fmt.Fprintf(pv.helpText, "%s ", "F10[black:green]Quit[-:-:-]") @@ -205,15 +225,19 @@ func (pv *pcView) updateTable() { } func (pv *pcView) updateLogs() { for { - time.Sleep(100 * time.Millisecond) pv.appView.QueueUpdateDraw(func() { - pv.fillLogs() + pv.logsText.Flush() }) + if !pv.logFollow { + break + } + time.Sleep(300 * time.Millisecond) } + } -func SetupTui(version string) { - pv := newPcView(version) +func SetupTui(version string, logLength int) { + pv := newPcView(version, logLength) go pv.updateTable() go pv.updateLogs() diff --git a/test_loop.bash b/test_loop.bash index 336b145..7693fa2 100755 --- a/test_loop.bash +++ b/test_loop.bash @@ -1,14 +1,19 @@ #!/usr/bin/env bash -LOOPS=3 +LOOPS=30000 for (( i=1; i<=LOOPS; i++ )) do - sleep 1 - + sleep 0.01 + if [[ -z "${PRINT_ERR}" ]]; then - echo "test loop $i $1 $ABC" + echo "test loop $i loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop loop $1 $ABC" else echo "test loop $i this is error $1 $PC_PROC_NAME" >&2 fi + + if [[ $i -eq 7 ]]; then + echo "test loop $i this is error $1 $PC_PROC_NAME" >&2 + fi + done exit $EXIT_CODE