mirror of https://github.com/tailscale/tailscale/
health: begin work to use structured health warnings instead of strings, pipe changes into ipn.Notify (#12406)
Updates tailscale/tailscale#4136 This PR is the first round of work to move from encoding health warnings as strings and use structured data instead. The current health package revolves around the idea of Subsystems. Each subsystem can have (or not have) a Go error associated with it. The overall health of the backend is given by the concatenation of all these errors. This PR polishes the concept of Warnable introduced by @bradfitz a few weeks ago. Each Warnable is a component of the backend (for instance, things like 'dns' or 'magicsock' are Warnables). Each Warnable has a unique identifying code. A Warnable is an entity we can warn the user about, by setting (or unsetting) a WarningState for it. Warnables have: - an identifying Code, so that the GUI can track them as their WarningStates come and go - a Title, which the GUIs can use to tell the user what component of the backend is broken - a Text, which is a function that is called with a set of Args to generate a more detailed error message to explain the unhappy state Additionally, this PR also begins to send Warnables and their WarningStates through LocalAPI to the clients, using ipn.Notify messages. An ipn.Notify is only issued when a warning is added or removed from the Tracker. In a next PR, we'll get rid of subsystems entirely, and we'll start using structured warnings for all errors affecting the backend functionality. Signed-off-by: Andrea Gottardo <andrea@gottardo.me>pull/12480/head
parent
e8ca30a5c7
commit
a8ee83e2c5
@ -0,0 +1,30 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package health
|
||||||
|
|
||||||
|
// Arg is a type for the key to be used in the Args of a Warnable.
|
||||||
|
type Arg string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// ArgAvailableVersion provides an update notification Warnable with the available version of the Tailscale client.
|
||||||
|
ArgAvailableVersion Arg = "available-version"
|
||||||
|
|
||||||
|
// ArgCurrentVersion provides an update notification Warnable with the current version of the Tailscale client.
|
||||||
|
ArgCurrentVersion Arg = "current-version"
|
||||||
|
|
||||||
|
// ArgDuration provides a Warnable with how long the Warnable has been in an unhealthy state.
|
||||||
|
ArgDuration Arg = "duration"
|
||||||
|
|
||||||
|
// ArgError provides a Warnable with the underlying error behind an unhealthy state.
|
||||||
|
ArgError Arg = "error"
|
||||||
|
|
||||||
|
// ArgMagicsockFunctionName provides a Warnable with the name of the Magicsock function that caused the unhealthy state.
|
||||||
|
ArgMagicsockFunctionName Arg = "magicsock-function-name"
|
||||||
|
|
||||||
|
// ArgRegionID provides a Warnable with the ID of a DERP server involved in the unhealthy state.
|
||||||
|
ArgRegionID Arg = "region-id"
|
||||||
|
|
||||||
|
// ArgServerName provides a Warnable with the hostname of a server involved in the unhealthy state.
|
||||||
|
ArgServerName Arg = "server-name"
|
||||||
|
)
|
@ -0,0 +1,79 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package health
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// State contains the health status of the backend, and is
|
||||||
|
// provided to the client UI via LocalAPI through ipn.Notify.
|
||||||
|
type State struct {
|
||||||
|
// Each key-value pair in Warnings represents a Warnable that is currently
|
||||||
|
// unhealthy. If a Warnable is healthy, it will not be present in this map.
|
||||||
|
// When a Warnable is unhealthy and becomes healthy, its key-value pair
|
||||||
|
// disappears in the next issued State. Observers should treat the absence of
|
||||||
|
// a WarnableCode in this map as an indication that the Warnable became healthy,
|
||||||
|
// and may use that to clear any notifications that were previously shown to the user.
|
||||||
|
// If Warnings is nil, all Warnables are healthy and the backend is overall healthy.
|
||||||
|
Warnings map[WarnableCode]UnhealthyState
|
||||||
|
}
|
||||||
|
|
||||||
|
// Representation contains information to be shown to the user to inform them
|
||||||
|
// that a Warnable is currently unhealthy.
|
||||||
|
type UnhealthyState struct {
|
||||||
|
WarnableCode WarnableCode
|
||||||
|
Severity Severity
|
||||||
|
Title string
|
||||||
|
Text string
|
||||||
|
BrokenSince *time.Time `json:",omitempty"`
|
||||||
|
Args Args `json:",omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// unhealthyState returns a unhealthyState of the Warnable given its current warningState.
|
||||||
|
func (w *Warnable) unhealthyState(ws *warningState) *UnhealthyState {
|
||||||
|
var text string
|
||||||
|
if ws.Args != nil {
|
||||||
|
text = w.Text(ws.Args)
|
||||||
|
} else {
|
||||||
|
text = w.Text(Args{})
|
||||||
|
}
|
||||||
|
|
||||||
|
return &UnhealthyState{
|
||||||
|
WarnableCode: w.Code,
|
||||||
|
Severity: w.Severity,
|
||||||
|
Title: w.Title,
|
||||||
|
Text: text,
|
||||||
|
BrokenSince: &ws.BrokenSince,
|
||||||
|
Args: ws.Args,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CurrentState returns a snapshot of the current health status of the backend.
|
||||||
|
// It returns a State with nil Warnings if the backend is healthy (all Warnables
|
||||||
|
// have no issues).
|
||||||
|
// The returned State is a snapshot of shared memory, and the caller should not
|
||||||
|
// mutate the returned value.
|
||||||
|
func (t *Tracker) CurrentState() *State {
|
||||||
|
if t.nil() {
|
||||||
|
return &State{}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.mu.Lock()
|
||||||
|
defer t.mu.Unlock()
|
||||||
|
|
||||||
|
if t.warnableVal == nil || len(t.warnableVal) == 0 {
|
||||||
|
return &State{}
|
||||||
|
}
|
||||||
|
|
||||||
|
wm := map[WarnableCode]UnhealthyState{}
|
||||||
|
|
||||||
|
for w, ws := range t.warnableVal {
|
||||||
|
wm[w.Code] = *w.unhealthyState(ws)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &State{
|
||||||
|
Warnings: wm,
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,206 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package health
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
This file contains definitions for the Warnables maintained within this `health` package.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// updateAvailableWarnable is a Warnable that warns the user that an update is available.
|
||||||
|
var updateAvailableWarnable = Register(&Warnable{
|
||||||
|
Code: "update-available",
|
||||||
|
Title: "Update available",
|
||||||
|
Severity: SeverityLow,
|
||||||
|
Text: func(args Args) string {
|
||||||
|
return fmt.Sprintf("An update from version %s to %s is available. Run `tailscale update` or `tailscale set --auto-update` to update.", args[ArgCurrentVersion], args[ArgAvailableVersion])
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// securityUpdateAvailableWarnable is a Warnable that warns the user that an important security update is available.
|
||||||
|
var securityUpdateAvailableWarnable = Register(&Warnable{
|
||||||
|
Code: "security-update-available",
|
||||||
|
Title: "Security update available",
|
||||||
|
Severity: SeverityHigh,
|
||||||
|
Text: func(args Args) string {
|
||||||
|
return fmt.Sprintf("An urgent security update from version %s to %s is available. Run `tailscale update` or `tailscale set --auto-update` to update now.", args[ArgCurrentVersion], args[ArgAvailableVersion])
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// unstableWarnable is a Warnable that warns the user that they are using an unstable version of Tailscale
|
||||||
|
// so they won't be surprised by all the issues that may arise.
|
||||||
|
var unstableWarnable = Register(&Warnable{
|
||||||
|
Code: "is-using-unstable-version",
|
||||||
|
Title: "Using an unstable version",
|
||||||
|
Severity: SeverityLow,
|
||||||
|
Text: StaticMessage("This is an unstable version of Tailscale meant for testing and development purposes: please report any bugs to Tailscale."),
|
||||||
|
})
|
||||||
|
|
||||||
|
// NetworkStatusWarnable is a Warnable that warns the user that the network is down.
|
||||||
|
var NetworkStatusWarnable = Register(&Warnable{
|
||||||
|
Code: "network-status",
|
||||||
|
Title: "Network down",
|
||||||
|
Severity: SeverityHigh,
|
||||||
|
Text: StaticMessage("Tailscale cannot connect because the network is down. (No network interface is up.)"),
|
||||||
|
ImpactsConnectivity: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
// IPNStateWarnable is a Warnable that warns the user that Tailscale is stopped.
|
||||||
|
var IPNStateWarnable = Register(&Warnable{
|
||||||
|
Code: "wantrunning-false",
|
||||||
|
Title: "Not connected to Tailscale",
|
||||||
|
Severity: SeverityLow,
|
||||||
|
Text: StaticMessage("Tailscale is stopped."),
|
||||||
|
})
|
||||||
|
|
||||||
|
// localLogWarnable is a Warnable that warns the user that the local log is misconfigured.
|
||||||
|
var localLogWarnable = Register(&Warnable{
|
||||||
|
Code: "local-log-config-error",
|
||||||
|
Title: "Local log misconfiguration",
|
||||||
|
Severity: SeverityLow,
|
||||||
|
Text: func(args Args) string {
|
||||||
|
return fmt.Sprintf("The local log is misconfigured: %v", args[ArgError])
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// LoginStateWarnable is a Warnable that warns the user that they are logged out,
|
||||||
|
// and provides the last login error if available.
|
||||||
|
var LoginStateWarnable = Register(&Warnable{
|
||||||
|
Code: "login-state",
|
||||||
|
Title: "Logged out",
|
||||||
|
Severity: SeverityMedium,
|
||||||
|
Text: func(args Args) string {
|
||||||
|
if args[ArgError] != "" {
|
||||||
|
return fmt.Sprintf("You are logged out. The last login error was: %v", args[ArgError])
|
||||||
|
} else {
|
||||||
|
return "You are logged out."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// notInMapPollWarnable is a Warnable that warns the user that they cannot connect to the control server.
|
||||||
|
var notInMapPollWarnable = Register(&Warnable{
|
||||||
|
Code: "not-in-map-poll",
|
||||||
|
Title: "Cannot connect to control server",
|
||||||
|
Severity: SeverityMedium,
|
||||||
|
DependsOn: []*Warnable{NetworkStatusWarnable},
|
||||||
|
Text: StaticMessage("Cannot connect to the control server (not in map poll). Check your Internet connection."),
|
||||||
|
})
|
||||||
|
|
||||||
|
// noDERPHomeWarnable is a Warnable that warns the user that Tailscale doesn't have a home DERP.
|
||||||
|
var noDERPHomeWarnable = Register(&Warnable{
|
||||||
|
Code: "no-derp-home",
|
||||||
|
Title: "No home relay server",
|
||||||
|
Severity: SeverityHigh,
|
||||||
|
DependsOn: []*Warnable{NetworkStatusWarnable},
|
||||||
|
Text: StaticMessage("Tailscale could not connect to any relay server. Check your Internet connection."),
|
||||||
|
})
|
||||||
|
|
||||||
|
// noDERPConnectionWarnable is a Warnable that warns the user that Tailscale couldn't connect to a specific DERP server.
|
||||||
|
var noDERPConnectionWarnable = Register(&Warnable{
|
||||||
|
Code: "no-derp-connection",
|
||||||
|
Title: "No relay server connection",
|
||||||
|
Severity: SeverityHigh,
|
||||||
|
DependsOn: []*Warnable{NetworkStatusWarnable},
|
||||||
|
Text: func(args Args) string {
|
||||||
|
return fmt.Sprintf("Tailscale could not connect to the relay server '%s'. The server might be temporarily unavailable, or your Internet connection might be down.", args[ArgRegionID])
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// derpTimeoutWarnable is a Warnable that warns the user that Tailscale hasn't heard from the home DERP region for a while.
|
||||||
|
var derpTimeoutWarnable = Register(&Warnable{
|
||||||
|
Code: "derp-timed-out",
|
||||||
|
Title: "Relay server timed out",
|
||||||
|
Severity: SeverityMedium,
|
||||||
|
DependsOn: []*Warnable{NetworkStatusWarnable},
|
||||||
|
Text: func(args Args) string {
|
||||||
|
return fmt.Sprintf("Tailscale hasn't heard from the home relay server (region %v) in %v. The server might be temporarily unavailable, or your Internet connection might be down.", args[ArgRegionID], args[ArgDuration])
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// derpRegionErrorWarnable is a Warnable that warns the user that a DERP region is reporting an issue.
|
||||||
|
var derpRegionErrorWarnable = Register(&Warnable{
|
||||||
|
Code: "derp-region-error",
|
||||||
|
Title: "Relay server error",
|
||||||
|
Severity: SeverityMedium,
|
||||||
|
DependsOn: []*Warnable{NetworkStatusWarnable},
|
||||||
|
Text: func(args Args) string {
|
||||||
|
return fmt.Sprintf("The relay server #%v is reporting an issue: %v", args[ArgRegionID], args[ArgError])
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// noUDP4BindWarnable is a Warnable that warns the user that Tailscale couldn't listen for incoming UDP connections.
|
||||||
|
var noUDP4BindWarnable = Register(&Warnable{
|
||||||
|
Code: "no-udp4-bind",
|
||||||
|
Title: "Incoming connections may fail",
|
||||||
|
Severity: SeverityHigh,
|
||||||
|
DependsOn: []*Warnable{NetworkStatusWarnable},
|
||||||
|
Text: StaticMessage("Tailscale couldn't listen for incoming UDP connections."),
|
||||||
|
ImpactsConnectivity: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
// mapResponseTimeoutWarnable is a Warnable that warns the user that Tailscale hasn't received a network map from the coordination server in a while.
|
||||||
|
var mapResponseTimeoutWarnable = Register(&Warnable{
|
||||||
|
Code: "mapresponse-timeout",
|
||||||
|
Title: "Network map response timeout",
|
||||||
|
Severity: SeverityMedium,
|
||||||
|
DependsOn: []*Warnable{NetworkStatusWarnable},
|
||||||
|
Text: func(args Args) string {
|
||||||
|
return fmt.Sprintf("Tailscale hasn't received a network map from the coordination server in %s.", args[ArgDuration])
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// tlsConnectionFailedWarnable is a Warnable that warns the user that Tailscale could not establish an encrypted connection with a server.
|
||||||
|
var tlsConnectionFailedWarnable = Register(&Warnable{
|
||||||
|
Code: "tls-connection-failed",
|
||||||
|
Title: "Encrypted connection failed",
|
||||||
|
Severity: SeverityMedium,
|
||||||
|
DependsOn: []*Warnable{NetworkStatusWarnable},
|
||||||
|
Text: func(args Args) string {
|
||||||
|
return fmt.Sprintf("Tailscale could not establish an encrypted connection with '%q': %v", args[ArgServerName], args[ArgError])
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// magicsockReceiveFuncWarnable is a Warnable that warns the user that one of the Magicsock functions is not running.
|
||||||
|
var magicsockReceiveFuncWarnable = Register(&Warnable{
|
||||||
|
Code: "magicsock-receive-func-error",
|
||||||
|
Title: "MagicSock function not running",
|
||||||
|
Severity: SeverityMedium,
|
||||||
|
Text: func(args Args) string {
|
||||||
|
return fmt.Sprintf("The MagicSock function %s is not running. You might experience connectivity issues.", args[ArgMagicsockFunctionName])
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// testWarnable is a Warnable that is used within this package for testing purposes only.
|
||||||
|
var testWarnable = Register(&Warnable{
|
||||||
|
Code: "test-warnable",
|
||||||
|
Title: "Test warnable",
|
||||||
|
Severity: SeverityLow,
|
||||||
|
Text: func(args Args) string {
|
||||||
|
return args[ArgError]
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// applyDiskConfigWarnable is a Warnable that warns the user that there was an error applying the envknob config stored on disk.
|
||||||
|
var applyDiskConfigWarnable = Register(&Warnable{
|
||||||
|
Code: "apply-disk-config",
|
||||||
|
Title: "Could not apply configuration",
|
||||||
|
Severity: SeverityMedium,
|
||||||
|
Text: func(args Args) string {
|
||||||
|
return fmt.Sprintf("An error occurred applying the Tailscale envknob configuration stored on disk: %v", args[ArgError])
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// controlHealthWarnable is a Warnable that warns the user that the coordination server is reporting an health issue.
|
||||||
|
var controlHealthWarnable = Register(&Warnable{
|
||||||
|
Code: "control-health",
|
||||||
|
Title: "Coordination server reports an issue",
|
||||||
|
Severity: SeverityMedium,
|
||||||
|
Text: func(args Args) string {
|
||||||
|
return fmt.Sprintf("The coordination server is reporting an health issue: %v", args[ArgError])
|
||||||
|
},
|
||||||
|
})
|
Loading…
Reference in New Issue