Update dependencies
This commit is contained in:
228
vendor/tailscale.com/log/filelogger/log.go
generated
vendored
Normal file
228
vendor/tailscale.com/log/filelogger/log.go
generated
vendored
Normal file
@@ -0,0 +1,228 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package filelogger provides localdisk log writing & rotation, primarily for Windows
|
||||
// clients. (We get this for free on other platforms.)
|
||||
package filelogger
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
maxSize = 100 << 20
|
||||
maxFiles = 50
|
||||
)
|
||||
|
||||
// New returns a logf wrapper that appends to local disk log
|
||||
// files on Windows, rotating old log files as needed to stay under
|
||||
// file count & byte limits.
|
||||
func New(fileBasePrefix, logID string, logf logger.Logf) logger.Logf {
|
||||
if runtime.GOOS != "windows" {
|
||||
panic("not yet supported on any platform except Windows")
|
||||
}
|
||||
if logf == nil {
|
||||
panic("nil logf")
|
||||
}
|
||||
dir := filepath.Join(os.Getenv("ProgramData"), "Tailscale", "Logs")
|
||||
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
log.Printf("failed to create local log directory; not writing logs to disk: %v", err)
|
||||
return logf
|
||||
}
|
||||
logf("local disk logdir: %v", dir)
|
||||
lfw := &logFileWriter{
|
||||
fileBasePrefix: fileBasePrefix,
|
||||
logID: logID,
|
||||
dir: dir,
|
||||
wrappedLogf: logf,
|
||||
}
|
||||
return lfw.Logf
|
||||
}
|
||||
|
||||
// logFileWriter is the state for the log writer & rotator.
|
||||
type logFileWriter struct {
|
||||
dir string // e.g. `C:\Users\FooBarUser\AppData\Local\Tailscale\Logs`
|
||||
logID string // hex logID
|
||||
fileBasePrefix string // e.g. "tailscale-service" or "tailscale-gui"
|
||||
wrappedLogf logger.Logf // underlying logger to send to
|
||||
|
||||
mu sync.Mutex // guards following
|
||||
buf bytes.Buffer // scratch buffer to avoid allocs
|
||||
fday civilDay // day that f was opened; zero means no file yet open
|
||||
f *os.File // file currently opened for append
|
||||
}
|
||||
|
||||
// civilDay is a year, month, and day in the local timezone.
|
||||
// It's a comparable value type.
|
||||
type civilDay struct {
|
||||
year int
|
||||
month time.Month
|
||||
day int
|
||||
}
|
||||
|
||||
func dayOf(t time.Time) civilDay {
|
||||
return civilDay{t.Year(), t.Month(), t.Day()}
|
||||
}
|
||||
|
||||
func (w *logFileWriter) Logf(format string, a ...any) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
w.buf.Reset()
|
||||
fmt.Fprintf(&w.buf, format, a...)
|
||||
if w.buf.Len() == 0 {
|
||||
return
|
||||
}
|
||||
out := w.buf.Bytes()
|
||||
w.wrappedLogf("%s", out)
|
||||
|
||||
// Make sure there's a final newline before we write to the log file.
|
||||
if out[len(out)-1] != '\n' {
|
||||
w.buf.WriteByte('\n')
|
||||
out = w.buf.Bytes()
|
||||
}
|
||||
|
||||
w.appendToFileLocked(out)
|
||||
}
|
||||
|
||||
// out should end in a newline.
|
||||
// w.mu must be held.
|
||||
func (w *logFileWriter) appendToFileLocked(out []byte) {
|
||||
now := time.Now()
|
||||
day := dayOf(now)
|
||||
if w.fday != day {
|
||||
w.startNewFileLocked()
|
||||
}
|
||||
out = removeDatePrefix(out)
|
||||
if w.f != nil {
|
||||
// RFC3339Nano but with a fixed number (3) of nanosecond digits:
|
||||
const formatPre = "2006-01-02T15:04:05"
|
||||
const formatPost = "Z07:00"
|
||||
fmt.Fprintf(w.f, "%s.%03d%s: %s",
|
||||
now.Format(formatPre),
|
||||
now.Nanosecond()/int(time.Millisecond/time.Nanosecond),
|
||||
now.Format(formatPost),
|
||||
out)
|
||||
}
|
||||
}
|
||||
|
||||
func isNum(b byte) bool { return '0' <= b && b <= '9' }
|
||||
|
||||
// removeDatePrefix returns a subslice of v with the log package's
|
||||
// standard datetime prefix format removed, if present.
|
||||
func removeDatePrefix(v []byte) []byte {
|
||||
const format = "2009/01/23 01:23:23 "
|
||||
if len(v) < len(format) {
|
||||
return v
|
||||
}
|
||||
for i, b := range v[:len(format)] {
|
||||
fb := format[i]
|
||||
if isNum(fb) {
|
||||
if !isNum(b) {
|
||||
return v
|
||||
}
|
||||
continue
|
||||
}
|
||||
if b != fb {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return v[len(format):]
|
||||
}
|
||||
|
||||
// startNewFileLocked opens a new log file for writing
|
||||
// and also cleans up any old files.
|
||||
//
|
||||
// w.mu must be held.
|
||||
func (w *logFileWriter) startNewFileLocked() {
|
||||
var oldName string
|
||||
if w.f != nil {
|
||||
oldName = filepath.Base(w.f.Name())
|
||||
w.f.Close()
|
||||
w.f = nil
|
||||
w.fday = civilDay{}
|
||||
}
|
||||
w.cleanLocked()
|
||||
|
||||
now := time.Now()
|
||||
day := dayOf(now)
|
||||
name := filepath.Join(w.dir, fmt.Sprintf("%s-%04d%02d%02dT%02d%02d%02d-%d.txt",
|
||||
w.fileBasePrefix,
|
||||
day.year,
|
||||
day.month,
|
||||
day.day,
|
||||
now.Hour(),
|
||||
now.Minute(),
|
||||
now.Second(),
|
||||
now.Unix()))
|
||||
var err error
|
||||
w.f, err = os.Create(name)
|
||||
if err != nil {
|
||||
w.wrappedLogf("failed to create log file: %v", err)
|
||||
return
|
||||
}
|
||||
if oldName != "" {
|
||||
fmt.Fprintf(w.f, "(logID %q; continued from log file %s)\n", w.logID, oldName)
|
||||
} else {
|
||||
fmt.Fprintf(w.f, "(logID %q)\n", w.logID)
|
||||
}
|
||||
w.fday = day
|
||||
}
|
||||
|
||||
// cleanLocked cleans up old log files.
|
||||
//
|
||||
// w.mu must be held.
|
||||
func (w *logFileWriter) cleanLocked() {
|
||||
entries, _ := os.ReadDir(w.dir)
|
||||
prefix := w.fileBasePrefix + "-"
|
||||
fileSize := map[string]int64{}
|
||||
var files []string
|
||||
var sumSize int64
|
||||
for _, entry := range entries {
|
||||
fi, err := entry.Info()
|
||||
if err != nil {
|
||||
w.wrappedLogf("error getting log file info: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
baseName := filepath.Base(fi.Name())
|
||||
if !strings.HasPrefix(baseName, prefix) {
|
||||
continue
|
||||
}
|
||||
size := fi.Size()
|
||||
fileSize[baseName] = size
|
||||
sumSize += size
|
||||
files = append(files, baseName)
|
||||
}
|
||||
if sumSize > maxSize {
|
||||
w.wrappedLogf("cleaning log files; sum byte count %d > %d", sumSize, maxSize)
|
||||
}
|
||||
if len(files) > maxFiles {
|
||||
w.wrappedLogf("cleaning log files; number of files %d > %d", len(files), maxFiles)
|
||||
}
|
||||
for (sumSize > maxSize || len(files) > maxFiles) && len(files) > 0 {
|
||||
target := files[0]
|
||||
files = files[1:]
|
||||
|
||||
targetSize := fileSize[target]
|
||||
targetFull := filepath.Join(w.dir, target)
|
||||
err := os.Remove(targetFull)
|
||||
if err != nil {
|
||||
w.wrappedLogf("error cleaning log file: %v", err)
|
||||
} else {
|
||||
sumSize -= targetSize
|
||||
w.wrappedLogf("cleaned log file %s (size %d); new bytes=%v, files=%v", targetFull, targetSize, sumSize, len(files))
|
||||
}
|
||||
}
|
||||
}
|
||||
276
vendor/tailscale.com/log/sockstatlog/logger.go
generated
vendored
Normal file
276
vendor/tailscale.com/log/sockstatlog/logger.go
generated
vendored
Normal file
@@ -0,0 +1,276 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package sockstatlog provides a logger for capturing network socket stats for debugging.
|
||||
// Stats are collected at a frequency of 10 Hz and logged to disk.
|
||||
// Stats are only uploaded to the log server on demand.
|
||||
package sockstatlog
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/logtail"
|
||||
"tailscale.com/logtail/filch"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/sockstats"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/logid"
|
||||
"tailscale.com/util/mak"
|
||||
)
|
||||
|
||||
// pollInterval specifies how often to poll for socket stats.
|
||||
const pollInterval = time.Second / 10
|
||||
|
||||
// logInterval specifies how often to log sockstat events to disk.
|
||||
// This delay is added to prevent an infinite loop when logs are uploaded,
|
||||
// which itself creates additional sockstat events.
|
||||
const logInterval = 10 * time.Second
|
||||
|
||||
// maxLogFileSize specifies the maximum size of a log file before it is rotated.
|
||||
// Our logs are fairly compact, and we are mostly only looking at a few hours of data.
|
||||
// Combined with the fact that these are often uploaded over cellular connections,
|
||||
// we keep this relatively small.
|
||||
const maxLogFileSize = 5 << 20 // 5 MB
|
||||
|
||||
// Logger logs statistics about network sockets.
|
||||
type Logger struct {
|
||||
// enabled identifies whether the logger is enabled.
|
||||
enabled atomic.Bool
|
||||
|
||||
ctx context.Context
|
||||
cancelFn context.CancelFunc
|
||||
|
||||
// eventCh is used to pass events from the poller to the logger.
|
||||
eventCh chan event
|
||||
|
||||
logf logger.Logf
|
||||
|
||||
// logger is the underlying logtail logger than manages log files on disk
|
||||
// and uploading to the log server.
|
||||
logger *logtail.Logger
|
||||
filch *filch.Filch
|
||||
tr http.RoundTripper
|
||||
}
|
||||
|
||||
// deltaStat represents the bytes transferred during a time period.
|
||||
// The first element is transmitted bytes, the second element is received bytes.
|
||||
type deltaStat [2]uint64
|
||||
|
||||
// event represents the socket stats on a specific interface during a time period.
|
||||
type event struct {
|
||||
// Time is when the event started as a Unix timestamp in milliseconds.
|
||||
Time int64 `json:"t"`
|
||||
|
||||
// Duration is the duration of this event in milliseconds.
|
||||
Duration int64 `json:"d"`
|
||||
|
||||
// IsCellularInterface is set to 1 if the traffic was sent over a cellular interface.
|
||||
IsCellularInterface int `json:"c,omitempty"`
|
||||
|
||||
// Stats records the stats for each Label during the time period.
|
||||
Stats map[sockstats.Label]deltaStat `json:"s"`
|
||||
}
|
||||
|
||||
// SockstatLogID reproducibly derives a new logid.PrivateID for sockstat logging from a node's public backend log ID.
|
||||
// The returned PrivateID is the sha256 sum of logID + "sockstat".
|
||||
// If a node's public log ID becomes known, it is trivial to spoof sockstat logs for that node.
|
||||
// Given that this is just for debugging, we're not too concerned about that.
|
||||
func SockstatLogID(logID logid.PublicID) logid.PrivateID {
|
||||
return logid.PrivateID(sha256.Sum256([]byte(logID.String() + "sockstat")))
|
||||
}
|
||||
|
||||
// NewLogger returns a new Logger that will store stats in logdir.
|
||||
// On platforms that do not support sockstat logging, a nil Logger will be returned.
|
||||
// The returned Logger is not yet enabled, and must be shut down with Shutdown when it is no longer needed.
|
||||
// Logs will be uploaded to the log server using a new log ID derived from the provided backend logID.
|
||||
//
|
||||
// The netMon parameter is optional. It should be specified in environments where
|
||||
// Tailscaled is manipulating the routing table.
|
||||
func NewLogger(logdir string, logf logger.Logf, logID logid.PublicID, netMon *netmon.Monitor, health *health.Tracker) (*Logger, error) {
|
||||
if !sockstats.IsAvailable {
|
||||
return nil, nil
|
||||
}
|
||||
if netMon == nil {
|
||||
netMon = netmon.NewStatic()
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(logdir, 0755); err != nil && !os.IsExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
filchPrefix := filepath.Join(logdir, "sockstats")
|
||||
filch, err := filch.New(filchPrefix, filch.Options{
|
||||
MaxFileSize: maxLogFileSize,
|
||||
ReplaceStderr: false,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logger := &Logger{
|
||||
logf: logf,
|
||||
filch: filch,
|
||||
tr: logpolicy.NewLogtailTransport(logtail.DefaultHost, netMon, health, logf),
|
||||
}
|
||||
logger.logger = logtail.NewLogger(logtail.Config{
|
||||
BaseURL: logpolicy.LogURL(),
|
||||
PrivateID: SockstatLogID(logID),
|
||||
Collection: "sockstats.log.tailscale.io",
|
||||
Buffer: filch,
|
||||
CompressLogs: true,
|
||||
FlushDelayFn: func() time.Duration {
|
||||
// set flush delay to 100 years so it never flushes automatically
|
||||
return 100 * 365 * 24 * time.Hour
|
||||
},
|
||||
Stderr: io.Discard, // don't log to stderr
|
||||
|
||||
HTTPC: &http.Client{Transport: logger.tr},
|
||||
}, logf)
|
||||
logger.logger.SetSockstatsLabel(sockstats.LabelSockstatlogLogger)
|
||||
|
||||
return logger, nil
|
||||
}
|
||||
|
||||
// SetLoggingEnabled enables or disables logging.
|
||||
// When disabled, socket stats are not polled and no new logs are written to disk.
|
||||
// Existing logs can still be fetched via the C2N API.
|
||||
func (l *Logger) SetLoggingEnabled(v bool) {
|
||||
old := l.enabled.Load()
|
||||
if old != v && l.enabled.CompareAndSwap(old, v) {
|
||||
if v {
|
||||
if l.eventCh == nil {
|
||||
// eventCh should be large enough for the number of events that will occur within logInterval.
|
||||
// Add an extra second's worth of events to ensure we don't drop any.
|
||||
l.eventCh = make(chan event, (logInterval+time.Second)/pollInterval)
|
||||
}
|
||||
l.ctx, l.cancelFn = context.WithCancel(context.Background())
|
||||
go l.poll()
|
||||
go l.logEvents()
|
||||
} else {
|
||||
l.cancelFn()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Logger) Write(p []byte) (int, error) {
|
||||
return l.logger.Write(p)
|
||||
}
|
||||
|
||||
// poll fetches the current socket stats at the configured time interval,
|
||||
// calculates the delta since the last poll,
|
||||
// and writes any non-zero values to the logger event channel.
|
||||
// This method does not return.
|
||||
func (l *Logger) poll() {
|
||||
// last is the last set of socket stats we saw.
|
||||
var lastStats *sockstats.SockStats
|
||||
var lastTime time.Time
|
||||
|
||||
ticker := time.NewTicker(pollInterval)
|
||||
for {
|
||||
select {
|
||||
case <-l.ctx.Done():
|
||||
ticker.Stop()
|
||||
return
|
||||
case t := <-ticker.C:
|
||||
stats := sockstats.Get()
|
||||
if lastStats != nil {
|
||||
diffstats := delta(lastStats, stats)
|
||||
if len(diffstats) > 0 {
|
||||
e := event{
|
||||
Time: lastTime.UnixMilli(),
|
||||
Duration: t.Sub(lastTime).Milliseconds(),
|
||||
Stats: diffstats,
|
||||
}
|
||||
if stats.CurrentInterfaceCellular {
|
||||
e.IsCellularInterface = 1
|
||||
}
|
||||
l.eventCh <- e
|
||||
}
|
||||
}
|
||||
lastTime = t
|
||||
lastStats = stats
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// logEvents reads events from the event channel at logInterval and logs them to disk.
|
||||
// This method does not return.
|
||||
func (l *Logger) logEvents() {
|
||||
enc := json.NewEncoder(l)
|
||||
flush := func() {
|
||||
for {
|
||||
select {
|
||||
case e := <-l.eventCh:
|
||||
if err := enc.Encode(e); err != nil {
|
||||
l.logf("sockstatlog: error encoding log: %v", err)
|
||||
}
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
ticker := time.NewTicker(logInterval)
|
||||
for {
|
||||
select {
|
||||
case <-l.ctx.Done():
|
||||
ticker.Stop()
|
||||
return
|
||||
case <-ticker.C:
|
||||
flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Logger) LogID() string {
|
||||
if l.logger == nil {
|
||||
return ""
|
||||
}
|
||||
return l.logger.PrivateID().Public().String()
|
||||
}
|
||||
|
||||
// Flush sends pending logs to the log server and flushes them from the local buffer.
|
||||
func (l *Logger) Flush() {
|
||||
l.logger.StartFlush()
|
||||
}
|
||||
|
||||
func (l *Logger) Shutdown(ctx context.Context) {
|
||||
if l.cancelFn != nil {
|
||||
l.cancelFn()
|
||||
}
|
||||
l.filch.Close()
|
||||
l.logger.Shutdown(ctx)
|
||||
|
||||
type closeIdler interface {
|
||||
CloseIdleConnections()
|
||||
}
|
||||
if tr, ok := l.tr.(closeIdler); ok {
|
||||
tr.CloseIdleConnections()
|
||||
}
|
||||
}
|
||||
|
||||
// delta calculates the delta stats between two SockStats snapshots.
|
||||
// b is assumed to have occurred after a.
|
||||
// Zero values are omitted from the returned map, and an empty map is returned if no bytes were transferred.
|
||||
func delta(a, b *sockstats.SockStats) (stats map[sockstats.Label]deltaStat) {
|
||||
if a == nil || b == nil {
|
||||
return nil
|
||||
}
|
||||
for label, bs := range b.Stats {
|
||||
as := a.Stats[label]
|
||||
if as.TxBytes == bs.TxBytes && as.RxBytes == bs.RxBytes {
|
||||
// fast path for unchanged stats
|
||||
continue
|
||||
}
|
||||
mak.Set(&stats, label, deltaStat{bs.TxBytes - as.TxBytes, bs.RxBytes - as.RxBytes})
|
||||
}
|
||||
return stats
|
||||
}
|
||||
Reference in New Issue
Block a user