TUN-3198: Handle errors while running tunnel UI
This commit is contained in:
parent
8a829b773a
commit
26fc20d406
|
@ -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,
|
||||
}),
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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,6 +79,35 @@ 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 := ""
|
||||
|
|
Loading…
Reference in New Issue