Files
ctrld/cmd/cli/log_header.go
T

226 lines
7.0 KiB
Go

package cli
import (
"bytes"
"net"
"os"
"runtime"
"slices"
"strconv"
"sync"
"time"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"github.com/Control-D-Inc/ctrld"
)
// processStartTime is the time this process started. A reader compares it with
// the time of the header line to tell a restart from a rotation.
var processStartTime = time.Now()
const (
// logHeaderMessage is the message of the header line. Log tools select the
// header by this message.
logHeaderMessage = "Log header"
// interceptModeOff is the mode word for a ctrld that does not intercept.
interceptModeOff = "off"
// logHeaderTriggerSend marks the header that a log request renders. It
// tells a reader that the line came from an upload, not from a file open.
logHeaderTriggerSend = "send"
// upstreamTypeUnspecified names an upstream that has no type in the config.
// ctrld resolves the type at start, so the header must not claim one.
upstreamTypeUnspecified = "unspecified"
)
// headerOSVersion reads the OS version once. The read starts a subprocess on
// macOS, and a header refresh follows every network change.
var headerOSVersion = sync.OnceValue(osVersion)
// logHeaderInput holds every value of a header line. The caller collects the
// values, so the renderer stays free of locks and of system calls.
type logHeaderInput struct {
Version string
Commit string
OS string
Arch string
PID int
StartTime time.Time
InterceptMode string
Listeners []string
UpstreamCount int
UpstreamTypes []string
ResolverUID string
LogFiles []string
Network networkSnapshot
Trigger string
}
// renderLogHeader renders the header as one log line. A file writer stores the
// bytes and writes them itself, so the header reaches a file that the logger
// never fills.
func renderLogHeader(in logHeaderInput) []byte {
var buf bytes.Buffer
// The console sits above info on a default start, and a log file must hold
// its header at every level, so the header takes a core of its own. The
// line is JSON like the journal, because log tools read its fields.
logger := &ctrld.Logger{Logger: zap.New(zapcore.NewCore(newJournalEncoder(), zapcore.AddSync(&buf), zapcore.DebugLevel))}
event := journal(logger.Info()).
Str("version", in.Version).
Str("commit", in.Commit).
Str("os", in.OS).
Str("arch", in.Arch).
Int("pid", in.PID).
Str("start_time", in.StartTime.Format(time.RFC3339)).
Str("intercept_mode", in.InterceptMode).
Strs("listeners", in.Listeners).
Int("upstream_count", in.UpstreamCount).
Strs("upstream_types", in.UpstreamTypes).
Str("resolver_uid", in.ResolverUID).
Strs("log_files", in.LogFiles).
Dict("network", snapshotDict(in.Network))
if in.Trigger != "" {
event = event.Str("trigger", in.Trigger)
}
event.Msg(logHeaderMessage)
// The header is the first line of every file and of every upload, and it
// bypasses the writers, so it passes the journal redaction here.
return redactRetainedLine(buf.Bytes())
}
// logHeaderInput collects every header value of the running program.
func (p *prog) logHeaderInput() logHeaderInput {
return p.logHeaderInputWith(p.networkSnapshot())
}
// logHeaderInputWith collects every header value around one network snapshot.
func (p *prog) logHeaderInputWith(network networkSnapshot) logHeaderInput {
in := logHeaderInput{
Version: curVersion(),
Commit: commit,
OS: headerOSVersion(),
Arch: runtime.GOARCH,
PID: os.Getpid(),
StartTime: processStartTime,
// The resolver UID is a secret that the provisioning code strips from
// every artifact, so the header keeps its first characters only.
ResolverUID: redactToken(cdUID),
LogFiles: p.logHeaderFiles(),
Network: network,
}
p.headerConfigInto(&in)
return in
}
// headerConfigInto reads the config under p.mu, because a reload replaces the
// config under that lock while log view and log send render a header.
func (p *prog) headerConfigInto(in *logHeaderInput) {
p.mu.Lock()
defer p.mu.Unlock()
logHeaderFromConfig(p.cfg, in)
}
// sendLogHeader renders the header that leads one upload. The stored state can
// be older than the request, and support reads this line to learn the network
// of the moment the user asked.
func (p *prog) sendLogHeader() []byte {
in := p.logHeaderInputWith(buildNetworkSnapshot(p.freshSnapshotInputs()))
in.Trigger = logHeaderTriggerSend
return renderLogHeader(in)
}
// logHeaderFiles names the files that this run writes, debug first.
func (p *prog) logHeaderFiles() []string {
files := make([]string, 0, 3)
for _, rf := range p.openLogFiles() {
files = append(files, rf.currentPath())
}
return files
}
// refreshLogHeader renders the header and hands the bytes to every open file.
// The caller must hold no writer lock, because the render reads the config
// and the network.
func (p *prog) refreshLogHeader() {
header := renderLogHeader(p.logHeaderInput())
for _, rf := range p.openLogFiles() {
rf.setHeader(header)
}
}
// writeLogHeaders puts the header at the append point of each open file. A
// header that cannot land leaves the file usable, so the error only earns a
// warning. It returns the rotations that the headers caused, because the
// logger does not reach the files yet and cannot report them.
func (p *prog) writeLogHeaders() []logRotation {
var rotations []logRotation
for _, rf := range p.openLogFiles() {
rotated, err := rf.writeHeaderReporting()
if rotated != nil {
rotations = append(rotations, *rotated)
}
if err != nil {
mainLog.Load().Warn().Err(err).Msg("Could not write log header")
}
}
return rotations
}
// logHeaderFromConfig fills the header fields that the config holds. It keeps
// every upstream endpoint, name, and bootstrap IP out of them, because a token
// can hide in each of them.
func logHeaderFromConfig(cfg *ctrld.Config, in *logHeaderInput) {
if cfg == nil {
in.InterceptMode = interceptModeOff
return
}
in.InterceptMode = headerInterceptMode(cfg)
in.Listeners = listenerAddresses(cfg)
in.UpstreamCount = len(cfg.Upstream)
in.UpstreamTypes = upstreamTypeWords(cfg)
}
// headerInterceptMode names the mode for the header. An unset mode means that
// ctrld does not intercept, which reads as off.
func headerInterceptMode(cfg *ctrld.Config) string {
if mode := listenerInterceptMode(cfg); mode != "" {
return mode
}
return interceptModeOff
}
func listenerAddresses(cfg *ctrld.Config) []string {
addresses := make([]string, 0, len(cfg.Listener))
for _, listener := range cfg.Listener {
if listener == nil {
continue
}
addresses = append(addresses, net.JoinHostPort(listener.IP, strconv.Itoa(listener.Port)))
}
slices.Sort(addresses)
return addresses
}
func upstreamTypeWords(cfg *ctrld.Config) []string {
words := make([]string, 0, len(cfg.Upstream))
for _, upstream := range cfg.Upstream {
if upstream == nil {
continue
}
word := upstream.Type
if word == "" {
word = upstreamTypeUnspecified
}
if !slices.Contains(words, word) {
words = append(words, word)
}
}
slices.Sort(words)
return words
}