TUN-3198: Handle errors while running tunnel UI

This commit is contained in:
Rachel Williams 2020-07-29 15:48:27 -07:00 committed by Areg Harutyunyan
parent 8a829b773a
commit 26fc20d406
5 changed files with 120 additions and 15 deletions

View File

@ -36,6 +36,7 @@ import (
"github.com/cloudflare/cloudflared/tunneldns"
"github.com/cloudflare/cloudflared/tunnelstore"
"github.com/cloudflare/cloudflared/websocket"
"github.com/rivo/tview"
"github.com/coreos/go-systemd/daemon"
"github.com/facebookgo/grace/gracenet"
@ -215,11 +216,23 @@ func TunnelCommand(c *cli.Context) error {
if name := c.String("name"); name != "" {
return adhocNamedTunnel(c, name)
}
if c.IsSet("launch-ui") {
// Create textView to stream logs to
logTextView := ui.NewDynamicColorTextView()
logger, err := createLoggerConfigured(c, false, logTextView)
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
return StartServer(c, version, shutdownC, graceShutdownC, nil, logger, logTextView)
}
logger, err := createLogger(c, false)
if err != nil {
return errors.Wrap(err, "error setting up logger")
}
return StartServer(c, version, shutdownC, graceShutdownC, nil, logger)
return StartServer(c, version, shutdownC, graceShutdownC, nil, logger, nil)
}
func Init(v string, s, g chan struct{}) {
@ -268,7 +281,7 @@ func routeFromFlag(c *cli.Context, tunnelID uuid.UUID) (tunnelstore.Route, bool)
return nil, false
}
func createLogger(c *cli.Context, isTransport bool) (logger.Service, error) {
func createLogger(c *cli.Context, isTransport bool) (*logger.OutputWriter, error) {
loggerOpts := []logger.Option{}
logPath := c.String("logfile")
@ -297,7 +310,26 @@ func createLogger(c *cli.Context, isTransport bool) (logger.Service, error) {
return logger.New(loggerOpts...)
}
func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan struct{}, namedTunnel *origin.NamedTunnelConfig, logger logger.Service) error {
// Create logger configured for use in UI
func createLoggerConfigured(c *cli.Context, isTransport bool, logTextView *tview.TextView) (logger.Service, error) {
l, err := createLogger(c, isTransport)
if err != nil {
return nil, errors.Wrap(err, "Error creating logger")
}
logLevel := c.String("loglevel")
supportedLevels, err := logger.GetSupportedLevels(logLevel)
if err != nil {
return nil, errors.Wrap(err, "Error parsing supported levels")
}
// Add TextView as a group to write output to
l.Add(logTextView, logger.NewUIFormatter(time.RFC3339), supportedLevels...)
return l, nil
}
func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan struct{}, namedTunnel *origin.NamedTunnelConfig, logger logger.Service, logTextView *tview.TextView) error {
_ = raven.SetDSN(sentryDSN)
var wg sync.WaitGroup
listeners := gracenet.Net{}
@ -537,7 +569,7 @@ func StartServer(c *cli.Context, version string, shutdownC, graceShutdownC chan
tunnelConfig.TunnelEventChan = tunnelEventChan
tunnelInfo := ui.NewUIModel(version, hostname, metricsListener.Addr().String(), tunnelConfig.OriginUrl, tunnelConfig.HAConnections)
tunnelInfo.LaunchUI(ctx, logger, tunnelEventChan)
tunnelInfo.LaunchUI(ctx, logger, tunnelEventChan, logTextView)
}
return waitToShutdown(&wg, errC, shutdownC, graceShutdownC, c.Duration("grace-period"), logger)
@ -1138,7 +1170,7 @@ func tunnelFlags(shouldHide bool) []cli.Flag {
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: "launch-ui",
Usage: "Launch tunnel UI and disable logs",
Usage: "Launch tunnel UI. Tunnel logs are scrollable via 'j', 'k', or arrow keys.",
Value: false,
Hidden: shouldHide,
}),

View File

@ -10,20 +10,23 @@ import (
"github.com/cloudflare/cloudflared/certutil"
"github.com/cloudflare/cloudflared/cmd/cloudflared/config"
"github.com/cloudflare/cloudflared/cmd/cloudflared/ui"
"github.com/cloudflare/cloudflared/logger"
"github.com/cloudflare/cloudflared/origin"
"github.com/cloudflare/cloudflared/tunnelrpc/pogs"
"github.com/cloudflare/cloudflared/tunnelstore"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/rivo/tview"
"github.com/urfave/cli/v2"
)
// subcommandContext carries structs shared between subcommands, to reduce number of arguments needed to pass between subcommands,
// and make sure they are only initialized once
type subcommandContext struct {
c *cli.Context
logger logger.Service
c *cli.Context
logger logger.Service
uiTextView *tview.TextView
// These fields should be accessed using their respective Getter
tunnelstoreClient tunnelstore.Client
@ -31,6 +34,20 @@ type subcommandContext struct {
}
func newSubcommandContext(c *cli.Context) (*subcommandContext, error) {
if c.IsSet("launch-ui") {
// Create textView to stream logs to
logTextView := ui.NewDynamicColorTextView()
logger, err := createLoggerConfigured(c, false, logTextView)
if err != nil {
return nil, errors.Wrap(err, "error setting up logger")
}
return &subcommandContext{
c: c,
logger: logger,
uiTextView: logTextView,
}, nil
}
logger, err := createLogger(c, false)
if err != nil {
return nil, errors.Wrap(err, "error setting up logger")
@ -237,7 +254,8 @@ func (sc *subcommandContext) run(tunnelID uuid.UUID) error {
if err != nil {
return err
}
return StartServer(sc.c, version, shutdownC, graceShutdownC, &origin.NamedTunnelConfig{Auth: *credentials, ID: tunnelID}, sc.logger)
return StartServer(sc.c, version, shutdownC, graceShutdownC, &origin.NamedTunnelConfig{Auth: *credentials, ID: tunnelID}, sc.logger, sc.uiTextView)
}
func (sc *subcommandContext) cleanupConnections(tunnelIDs []uuid.UUID) error {

View File

@ -56,7 +56,7 @@ func NewUIModel(version, hostname, metricsURL, proxyURL string, haConnections in
}
}
func (data *uiModel) LaunchUI(ctx context.Context, logger logger.Service, tunnelEventChan <-chan TunnelEvent) {
func (data *uiModel) LaunchUI(ctx context.Context, logger logger.Service, tunnelEventChan <-chan TunnelEvent, logTextView *tview.TextView) {
palette := palette{url: "#4682B4", connected: "#00FF00", defaultText: "white", disconnected: "red", reconnecting: "orange"}
app := tview.NewApplication()
@ -86,12 +86,14 @@ func (data *uiModel) LaunchUI(ctx context.Context, logger logger.Service, tunnel
tunnelHostText := tview.NewTextView().SetText(data.edgeURL)
grid.AddItem(tunnelHostText, 0, 1, 1, 1, 0, 0, false)
grid.AddItem(newDynamicColorTextView().SetText(fmt.Sprintf("[%s]\u2022[%s] Proxying to [%s::b]%s", palette.connected, palette.defaultText, palette.url, data.proxyURL)), 1, 1, 1, 1, 0, 0, false)
grid.AddItem(NewDynamicColorTextView().SetText(fmt.Sprintf("[%s]\u2022[%s] Proxying to [%s::b]%s", palette.connected, palette.defaultText, palette.url, data.proxyURL)), 1, 1, 1, 1, 0, 0, false)
grid.AddItem(connTable, 2, 1, 1, 1, 0, 0, false)
grid.AddItem(newDynamicColorTextView().SetText(fmt.Sprintf("Metrics at [%s::b]%s/metrics", palette.url, data.metricsURL)), 3, 1, 1, 1, 0, 0, false)
grid.AddItem(tview.NewBox(), 4, 0, 1, 2, 0, 0, false)
grid.AddItem(NewDynamicColorTextView().SetText(fmt.Sprintf("Metrics at [%s::b]%s/metrics", palette.url, data.metricsURL)), 3, 1, 1, 1, 0, 0, false)
// Add TextView to stream logs
// LOGS header is displayed in bold
grid.AddItem(logTextView.SetText("[::b]LOGS:[::-]\n\n").SetChangedFunc(handleNewText(app, logTextView)), 4, 0, 5, 2, 0, 0, false)
go func() {
for {
@ -125,10 +127,19 @@ func (data *uiModel) LaunchUI(ctx context.Context, logger logger.Service, tunnel
}()
}
func newDynamicColorTextView() *tview.TextView {
func NewDynamicColorTextView() *tview.TextView {
return tview.NewTextView().SetDynamicColors(true)
}
// Re-draws application when new logs are streamed to UI
func handleNewText(app *tview.Application, logTextView *tview.TextView) func() {
return func() {
app.Draw()
// SetFocus to enable scrolling in textview
app.SetFocus(logTextView)
}
}
func (data *uiModel) changeConnStatus(event TunnelEvent, table *tview.Table, logger logger.Service, palette palette) {
index := int(event.Index)
// Get connection location and state

View File

@ -85,6 +85,14 @@ func LogLevelString(level string) Option {
}
}
func GetSupportedLevels(level string) ([]Level, error) {
supported, err := ParseLevelString(level)
if err != nil {
return nil, err
}
return supported, nil
}
// Parse builds the Options struct so the caller knows what actions should be run
func Parse(opts ...Option) (*Options, error) {
options := &Options{}
@ -98,7 +106,7 @@ func Parse(opts ...Option) (*Options, error) {
// New setups a new logger based on the options.
// The default behavior is to write to standard out
func New(opts ...Option) (Service, error) {
func New(opts ...Option) (*OutputWriter, error) {
config, err := Parse(opts...)
if err != nil {
@ -123,6 +131,7 @@ func New(opts ...Option) (Service, error) {
l.Add(os.Stderr, terminalFormatter, config.supportedTerminalLevels...)
}
}
return l, nil
}

View File

@ -63,6 +63,12 @@ type TerminalFormatter struct {
supportsColor bool
}
// UIFormatter is used for streaming logs to UI
type UIFormatter struct {
format string
supportsColor bool
}
// NewTerminalFormatter creates a Terminal formatter for colored output
// format is the time format to use for timestamp formatting
func NewTerminalFormatter(format string) Formatter {
@ -73,10 +79,39 @@ func NewTerminalFormatter(format string) Formatter {
}
}
func NewUIFormatter(format string) Formatter {
supportsColor := (runtime.GOOS != "windows")
return &UIFormatter{
format: format,
supportsColor: supportsColor,
}
}
// Timestamp uses formatting that is tview-specific for UI
func (f *UIFormatter) Timestamp(l Level, d time.Time) string {
t := ""
dateStr := "[" + d.Format(f.format) + "] "
switch l {
case InfoLevel:
t = "[#00ffff]INFO[white]"
case ErrorLevel:
t = "[red]ERROR[white]"
case DebugLevel:
t = "[yellow]DEBUG[white]"
case FatalLevel:
t = "[red]FATAL[white]"
}
return t + dateStr
}
func (f *UIFormatter) Content(l Level, c string) string {
return c
}
// Timestamp returns the log level with a matching color to the log type
func (f *TerminalFormatter) Timestamp(l Level, d time.Time) string {
t := ""
dateStr := "[" + d.Format(f.format) + "] "
dateStr := "[" + d.Format(f.format) + "] "
switch l {
case InfoLevel:
t = f.output("INFO", skittles.Cyan)