feat: add Tracer, a debugging aid

Adds a Tracer interface that is capable of logging all executed
tea.Cmd and all processed tea.Msg.

RemoteTracer is one standard implementation, which provides access
to a live stream of these event logs, through a TCP socket.
This commit is contained in:
Christian Muehlhaeuser 2022-11-08 17:05:49 +01:00
parent 2d10416631
commit d7516fc570
No known key found for this signature in database
GPG Key ID: 3A371AA367F6CC1F
3 changed files with 157 additions and 0 deletions

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"io" "io"
"github.com/charmbracelet/bubbletea/tracer"
"github.com/muesli/termenv" "github.com/muesli/termenv"
) )
@ -151,3 +152,10 @@ func WithANSICompressor() ProgramOption {
p.startupOptions |= withANSICompressor p.startupOptions |= withANSICompressor
} }
} }
// WithTracer sets a tracer for the program. This is useful for debugging.
func WithTracer(t tracer.Tracer) ProgramOption {
return func(p *Program) {
p.tracer = t
}
}

44
tea.go
View File

@ -16,10 +16,13 @@ import (
"io" "io"
"os" "os"
"os/signal" "os/signal"
"reflect"
"runtime/debug" "runtime/debug"
"sync" "sync"
"syscall" "syscall"
"time"
"github.com/charmbracelet/bubbletea/tracer"
"github.com/containerd/console" "github.com/containerd/console"
isatty "github.com/mattn/go-isatty" isatty "github.com/mattn/go-isatty"
"github.com/muesli/cancelreader" "github.com/muesli/cancelreader"
@ -122,6 +125,8 @@ type Program struct {
// as this value only comes into play on Windows, hence the ignore comment // as this value only comes into play on Windows, hence the ignore comment
// below. // below.
windowsStdin *os.File //nolint:golint,structcheck,unused windowsStdin *os.File //nolint:golint,structcheck,unused
tracer tracer.Tracer
} }
// Quit is a special command that tells the Bubble Tea program to exit. // Quit is a special command that tells the Bubble Tea program to exit.
@ -244,8 +249,17 @@ func (p *Program) handleCommands(cmds chan Cmd) chan struct{} {
// possible to cancel them so we'll have to leak the goroutine // possible to cancel them so we'll have to leak the goroutine
// until Cmd returns. // until Cmd returns.
go func() { go func() {
tc := tracer.NewCommand()
if p.tracer != nil {
p.tracer.Command(tc)
}
msg := cmd() // this can be long. msg := cmd() // this can be long.
p.Send(msg) p.Send(msg)
if p.tracer != nil {
p.traceCommand(tc, msg)
}
}() }()
} }
} }
@ -254,6 +268,34 @@ func (p *Program) handleCommands(cmds chan Cmd) chan struct{} {
return ch return ch
} }
func (p *Program) traceCommand(tc tracer.Command, msg Msg) {
t := "nil"
if msg != nil {
t = reflect.TypeOf(msg).String()
}
tc.Finished = time.Now()
tc.Type = t
tc.Msg = fmt.Sprintf("%v", msg)
p.tracer.Command(tc)
}
func (p *Program) traceMessage(model Model, msg Msg) {
if p.tracer != nil {
t := "nil"
if msg != nil {
t = reflect.TypeOf(msg).String()
}
p.tracer.Message(tracer.Message{
Model: reflect.TypeOf(model).String(),
Type: t,
Msg: fmt.Sprintf("%v", msg),
})
}
}
// eventLoop is the central message loop. It receives and handles the default // eventLoop is the central message loop. It receives and handles the default
// Bubble Tea messages, update the model and triggers redraws. // Bubble Tea messages, update the model and triggers redraws.
func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) { func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
@ -266,6 +308,8 @@ func (p *Program) eventLoop(model Model, cmds chan Cmd) (Model, error) {
return model, err return model, err
case msg := <-p.msgs: case msg := <-p.msgs:
p.traceMessage(model, msg)
// Handle special internal messages. // Handle special internal messages.
switch msg := msg.(type) { switch msg := msg.(type) {
case quitMsg: case quitMsg:

105
tracer/tracer.go Normal file
View File

@ -0,0 +1,105 @@
package tracer
import (
"encoding/json"
"net"
"sync"
"time"
)
type Tracer interface {
Command(c Command)
Message(m Message)
}
type Event struct {
Timestamp time.Time
Message Message
Command Command
}
type Message struct {
Model string
Type string
Msg string
}
type Command struct {
ID int
Started time.Time
Finished time.Time
Type string
Msg string
}
func NewCommand() Command {
mtx.Lock()
defer mtx.Unlock()
id++
return Command{
ID: id,
Started: time.Now(),
}
}
var (
ch = make(chan Event)
mtx sync.Mutex
id int
)
type RemoteTracer struct {
}
func NewRemoteTracer() (*RemoteTracer, error) {
listen, err := net.Listen("tcp", ":13337")
if err != nil {
return nil, err
}
go func() {
for {
conn, err := listen.Accept()
if err != nil {
return
}
go handleTraceConn(conn)
}
}()
t := &RemoteTracer{}
return t, nil
}
func (rt *RemoteTracer) Command(c Command) {
ev := Event{
Timestamp: time.Now(),
Command: c,
}
ch <- ev
}
func (rt *RemoteTracer) Message(m Message) {
ev := Event{
Timestamp: time.Now(),
Message: m,
}
ch <- ev
}
func handleTraceConn(conn net.Conn) {
for ev := range ch {
b, _ := json.Marshal(ev)
_, _ = conn.Write(b)
_, err := conn.Write([]byte("\n"))
if err != nil {
return
}
}
// close conn
_ = conn.Close()
}