Update
This commit is contained in:
80
vendor/tailscale.com/logtail/backoff/backoff.go
generated
vendored
80
vendor/tailscale.com/logtail/backoff/backoff.go
generated
vendored
@@ -1,80 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package backoff provides a back-off timer type.
|
||||
package backoff
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand/v2"
|
||||
"time"
|
||||
|
||||
"tailscale.com/tstime"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// Backoff tracks state the history of consecutive failures and sleeps
|
||||
// an increasing amount of time, up to a provided limit.
|
||||
type Backoff struct {
|
||||
n int // number of consecutive failures
|
||||
maxBackoff time.Duration
|
||||
|
||||
// Name is the name of this backoff timer, for logging purposes.
|
||||
name string
|
||||
// logf is the function used for log messages when backing off.
|
||||
logf logger.Logf
|
||||
|
||||
// tstime.Clock.NewTimer is used instead time.NewTimer.
|
||||
Clock tstime.Clock
|
||||
|
||||
// LogLongerThan sets the minimum time of a single backoff interval
|
||||
// before we mention it in the log.
|
||||
LogLongerThan time.Duration
|
||||
}
|
||||
|
||||
// NewBackoff returns a new Backoff timer with the provided name (for logging), logger,
|
||||
// and max backoff time. By default, all failures (calls to BackOff with a non-nil err)
|
||||
// are logged unless the returned Backoff.LogLongerThan is adjusted.
|
||||
func NewBackoff(name string, logf logger.Logf, maxBackoff time.Duration) *Backoff {
|
||||
return &Backoff{
|
||||
name: name,
|
||||
logf: logf,
|
||||
maxBackoff: maxBackoff,
|
||||
Clock: tstime.StdClock{},
|
||||
}
|
||||
}
|
||||
|
||||
// BackOff sleeps an increasing amount of time if err is non-nil while the
|
||||
// context is active. It resets the backoff schedule once err is nil.
|
||||
func (b *Backoff) BackOff(ctx context.Context, err error) {
|
||||
if err == nil {
|
||||
// No error. Reset number of consecutive failures.
|
||||
b.n = 0
|
||||
return
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
// Fast path.
|
||||
return
|
||||
}
|
||||
|
||||
b.n++
|
||||
// n^2 backoff timer is a little smoother than the
|
||||
// common choice of 2^n.
|
||||
d := time.Duration(b.n*b.n) * 10 * time.Millisecond
|
||||
if d > b.maxBackoff {
|
||||
d = b.maxBackoff
|
||||
}
|
||||
// Randomize the delay between 0.5-1.5 x msec, in order
|
||||
// to prevent accidental "thundering herd" problems.
|
||||
d = time.Duration(float64(d) * (rand.Float64() + 0.5))
|
||||
|
||||
if d >= b.LogLongerThan {
|
||||
b.logf("%s: [v1] backoff: %d msec", b.name, d.Milliseconds())
|
||||
}
|
||||
t, tChannel := b.Clock.NewTimer(d)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Stop()
|
||||
case <-tChannel:
|
||||
}
|
||||
}
|
||||
50
vendor/tailscale.com/logtail/buffer.go
generated
vendored
50
vendor/tailscale.com/logtail/buffer.go
generated
vendored
@@ -1,13 +1,18 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_logtail
|
||||
|
||||
package logtail
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"expvar"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"tailscale.com/metrics"
|
||||
"tailscale.com/syncs"
|
||||
)
|
||||
|
||||
type Buffer interface {
|
||||
@@ -34,14 +39,44 @@ type memBuffer struct {
|
||||
next []byte
|
||||
pending chan qentry
|
||||
|
||||
dropMu sync.Mutex
|
||||
dropMu syncs.Mutex
|
||||
dropCount int
|
||||
|
||||
// Metrics (see [memBuffer.ExpVar] for details).
|
||||
writeCalls expvar.Int
|
||||
readCalls expvar.Int
|
||||
writeBytes expvar.Int
|
||||
readBytes expvar.Int
|
||||
droppedBytes expvar.Int
|
||||
storedBytes expvar.Int
|
||||
}
|
||||
|
||||
// ExpVar returns a [metrics.Set] with metrics about the buffer.
|
||||
//
|
||||
// - counter_write_calls: Total number of write calls.
|
||||
// - counter_read_calls: Total number of read calls.
|
||||
// - counter_write_bytes: Total number of bytes written.
|
||||
// - counter_read_bytes: Total number of bytes read.
|
||||
// - counter_dropped_bytes: Total number of bytes dropped.
|
||||
// - gauge_stored_bytes: Current number of bytes stored in memory.
|
||||
func (b *memBuffer) ExpVar() expvar.Var {
|
||||
m := new(metrics.Set)
|
||||
m.Set("counter_write_calls", &b.writeCalls)
|
||||
m.Set("counter_read_calls", &b.readCalls)
|
||||
m.Set("counter_write_bytes", &b.writeBytes)
|
||||
m.Set("counter_read_bytes", &b.readBytes)
|
||||
m.Set("counter_dropped_bytes", &b.droppedBytes)
|
||||
m.Set("gauge_stored_bytes", &b.storedBytes)
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *memBuffer) TryReadLine() ([]byte, error) {
|
||||
m.readCalls.Add(1)
|
||||
if m.next != nil {
|
||||
msg := m.next
|
||||
m.next = nil
|
||||
m.readBytes.Add(int64(len(msg)))
|
||||
m.storedBytes.Add(-int64(len(msg)))
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
@@ -49,8 +84,13 @@ func (m *memBuffer) TryReadLine() ([]byte, error) {
|
||||
case ent := <-m.pending:
|
||||
if ent.dropCount > 0 {
|
||||
m.next = ent.msg
|
||||
return fmt.Appendf(nil, "----------- %d logs dropped ----------", ent.dropCount), nil
|
||||
b := fmt.Appendf(nil, "----------- %d logs dropped ----------", ent.dropCount)
|
||||
m.writeBytes.Add(int64(len(b))) // indicate pseudo-injected log message
|
||||
m.readBytes.Add(int64(len(b)))
|
||||
return b, nil
|
||||
}
|
||||
m.readBytes.Add(int64(len(ent.msg)))
|
||||
m.storedBytes.Add(-int64(len(ent.msg)))
|
||||
return ent.msg, nil
|
||||
default:
|
||||
return nil, nil
|
||||
@@ -58,6 +98,7 @@ func (m *memBuffer) TryReadLine() ([]byte, error) {
|
||||
}
|
||||
|
||||
func (m *memBuffer) Write(b []byte) (int, error) {
|
||||
m.writeCalls.Add(1)
|
||||
m.dropMu.Lock()
|
||||
defer m.dropMu.Unlock()
|
||||
|
||||
@@ -67,10 +108,13 @@ func (m *memBuffer) Write(b []byte) (int, error) {
|
||||
}
|
||||
select {
|
||||
case m.pending <- ent:
|
||||
m.writeBytes.Add(int64(len(b)))
|
||||
m.storedBytes.Add(+int64(len(b)))
|
||||
m.dropCount = 0
|
||||
return len(b), nil
|
||||
default:
|
||||
m.dropCount++
|
||||
m.droppedBytes.Add(int64(len(b)))
|
||||
return 0, errBufferFull
|
||||
}
|
||||
}
|
||||
|
||||
67
vendor/tailscale.com/logtail/config.go
generated
vendored
Normal file
67
vendor/tailscale.com/logtail/config.go
generated
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package logtail
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"tailscale.com/tstime"
|
||||
"tailscale.com/types/logid"
|
||||
"tailscale.com/util/eventbus"
|
||||
)
|
||||
|
||||
// DefaultHost is the default host name to upload logs to when
|
||||
// Config.BaseURL isn't provided.
|
||||
const DefaultHost = "log.tailscale.com"
|
||||
|
||||
const defaultFlushDelay = 2 * time.Second
|
||||
|
||||
const (
|
||||
// CollectionNode is the name of a logtail Config.Collection
|
||||
// for tailscaled (or equivalent: IPNExtension, Android app).
|
||||
CollectionNode = "tailnode.log.tailscale.io"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Collection string // collection name, a domain name
|
||||
PrivateID logid.PrivateID // private ID for the primary log stream
|
||||
CopyPrivateID logid.PrivateID // private ID for a log stream that is a superset of this log stream
|
||||
BaseURL string // if empty defaults to "https://log.tailscale.com"
|
||||
HTTPC *http.Client // if empty defaults to http.DefaultClient
|
||||
SkipClientTime bool // if true, client_time is not written to logs
|
||||
LowMemory bool // if true, logtail minimizes memory use
|
||||
Clock tstime.Clock // if set, Clock.Now substitutes uses of time.Now
|
||||
Stderr io.Writer // if set, logs are sent here instead of os.Stderr
|
||||
Bus *eventbus.Bus // if set, uses the eventbus for awaitInternetUp instead of callback
|
||||
StderrLevel int // max verbosity level to write to stderr; 0 means the non-verbose messages only
|
||||
Buffer Buffer // temp storage, if nil a MemoryBuffer
|
||||
CompressLogs bool // whether to compress the log uploads
|
||||
MaxUploadSize int // maximum upload size; 0 means using the default
|
||||
|
||||
// MetricsDelta, if non-nil, is a func that returns an encoding
|
||||
// delta in clientmetrics to upload alongside existing logs.
|
||||
// It can return either an empty string (for nothing) or a string
|
||||
// that's safe to embed in a JSON string literal without further escaping.
|
||||
MetricsDelta func() string
|
||||
|
||||
// FlushDelayFn, if non-nil is a func that returns how long to wait to
|
||||
// accumulate logs before uploading them. 0 or negative means to upload
|
||||
// immediately.
|
||||
//
|
||||
// If nil, a default value is used. (currently 2 seconds)
|
||||
FlushDelayFn func() time.Duration
|
||||
|
||||
// IncludeProcID, if true, results in an ephemeral process identifier being
|
||||
// included in logs. The ID is random and not guaranteed to be globally
|
||||
// unique, but it can be used to distinguish between different instances
|
||||
// running with same PrivateID.
|
||||
IncludeProcID bool
|
||||
|
||||
// IncludeProcSequence, if true, results in an ephemeral sequence number
|
||||
// being included in the logs. The sequence number is incremented for each
|
||||
// log message sent, but is not persisted across process restarts.
|
||||
IncludeProcSequence bool
|
||||
}
|
||||
530
vendor/tailscale.com/logtail/filch/filch.go
generated
vendored
530
vendor/tailscale.com/logtail/filch/filch.go
generated
vendored
@@ -1,148 +1,421 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_logtail
|
||||
|
||||
// Package filch is a file system queue that pilfers your stderr.
|
||||
// (A FILe CHannel that filches.)
|
||||
package filch
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"cmp"
|
||||
"errors"
|
||||
"expvar"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"tailscale.com/metrics"
|
||||
"tailscale.com/util/must"
|
||||
)
|
||||
|
||||
var stderrFD = 2 // a variable for testing
|
||||
|
||||
const defaultMaxFileSize = 50 << 20
|
||||
var errTooLong = errors.New("filch: line too long")
|
||||
var errClosed = errors.New("filch: buffer is closed")
|
||||
|
||||
const DefaultMaxLineSize = 64 << 10
|
||||
const DefaultMaxFileSize = 50 << 20
|
||||
|
||||
type Options struct {
|
||||
ReplaceStderr bool // dup over fd 2 so everything written to stderr comes here
|
||||
MaxFileSize int
|
||||
// ReplaceStderr specifies whether to filch [os.Stderr] such that
|
||||
// everything written there appears in the [Filch] buffer instead.
|
||||
// In order to write to stderr instead of writing to [Filch],
|
||||
// then use [Filch.OrigStderr].
|
||||
ReplaceStderr bool
|
||||
|
||||
// MaxLineSize is the maximum line size that could be encountered,
|
||||
// including the trailing newline. This is enforced as a hard limit.
|
||||
// Writes larger than this will be rejected. Reads larger than this
|
||||
// will report an error and skip over the long line.
|
||||
// If zero, the [DefaultMaxLineSize] is used.
|
||||
MaxLineSize int
|
||||
|
||||
// MaxFileSize specifies the maximum space on disk to use for logs.
|
||||
// This is not enforced as a hard limit, but rather a soft limit.
|
||||
// If zero, then [DefaultMaxFileSize] is used.
|
||||
MaxFileSize int
|
||||
}
|
||||
|
||||
// A Filch uses two alternating files as a simplistic ring buffer.
|
||||
type Filch struct {
|
||||
// OrigStderr is the original [os.Stderr] if [Options.ReplaceStderr] is specified.
|
||||
// Writing directly to this avoids writing into the Filch buffer.
|
||||
// Otherwise, it is nil.
|
||||
OrigStderr *os.File
|
||||
|
||||
mu sync.Mutex
|
||||
cur *os.File
|
||||
alt *os.File
|
||||
altscan *bufio.Scanner
|
||||
recovered int64
|
||||
// maxLineSize specifies the maximum line size to use.
|
||||
maxLineSize int // immutable once set
|
||||
|
||||
maxFileSize int64
|
||||
writeCounter int
|
||||
// maxFileSize specifies the max space either newer and older should use.
|
||||
maxFileSize int64 // immutable once set
|
||||
|
||||
// buf is an initial buffer for altscan.
|
||||
// As of August 2021, 99.96% of all log lines
|
||||
// are below 4096 bytes in length.
|
||||
// Since this cutoff is arbitrary, instead of using 4096,
|
||||
// we subtract off the size of the rest of the struct
|
||||
// so that the whole struct takes 4096 bytes
|
||||
// (less on 32 bit platforms).
|
||||
// This reduces allocation waste.
|
||||
buf [4096 - 64]byte
|
||||
mu sync.Mutex
|
||||
newer *os.File // newer logs data; writes are appended to the end
|
||||
older *os.File // older logs data; reads are consumed from the start
|
||||
|
||||
newlyWrittenBytes int64 // bytes written directly to newer; reset upon rotation
|
||||
newlyFilchedBytes int64 // bytes filched indirectly to newer; reset upon rotation
|
||||
|
||||
wrBuf []byte // temporary buffer for writing; only used for writes without trailing newline
|
||||
wrBufMaxLen int // maximum length of wrBuf; reduced upon every rotation
|
||||
|
||||
rdBufIdx int // index into rdBuf for the next unread bytes
|
||||
rdBuf []byte // temporary buffer for reading
|
||||
rdBufMaxLen int // maximum length of rdBuf; reduced upon every rotation
|
||||
|
||||
// Metrics (see [Filch.ExpVar] for details).
|
||||
writeCalls expvar.Int
|
||||
readCalls expvar.Int
|
||||
rotateCalls expvar.Int
|
||||
callErrors expvar.Int
|
||||
writeBytes expvar.Int
|
||||
readBytes expvar.Int
|
||||
filchedBytes expvar.Int
|
||||
droppedBytes expvar.Int
|
||||
storedBytes expvar.Int
|
||||
}
|
||||
|
||||
// ExpVar returns a [metrics.Set] with metrics about the buffer.
|
||||
//
|
||||
// - counter_write_calls: Total number of calls to [Filch.Write]
|
||||
// (excludes calls when file is closed).
|
||||
//
|
||||
// - counter_read_calls: Total number of calls to [Filch.TryReadLine]
|
||||
// (excludes calls when file is closed or no bytes).
|
||||
//
|
||||
// - counter_rotate_calls: Total number of calls to rotate the log files
|
||||
// (excludes calls when there is nothing to rotate to).
|
||||
//
|
||||
// - counter_call_errors: Total number of calls returning errors.
|
||||
//
|
||||
// - counter_write_bytes: Total number of bytes written
|
||||
// (includes bytes filched from stderr).
|
||||
//
|
||||
// - counter_read_bytes: Total number of bytes read
|
||||
// (includes bytes filched from stderr).
|
||||
//
|
||||
// - counter_filched_bytes: Total number of bytes filched from stderr.
|
||||
//
|
||||
// - counter_dropped_bytes: Total number of bytes dropped
|
||||
// (includes bytes filched from stderr and lines too long to read).
|
||||
//
|
||||
// - gauge_stored_bytes: Current number of bytes stored on disk.
|
||||
func (f *Filch) ExpVar() expvar.Var {
|
||||
m := new(metrics.Set)
|
||||
m.Set("counter_write_calls", &f.writeCalls)
|
||||
m.Set("counter_read_calls", &f.readCalls)
|
||||
m.Set("counter_rotate_calls", &f.rotateCalls)
|
||||
m.Set("counter_call_errors", &f.callErrors)
|
||||
m.Set("counter_write_bytes", &f.writeBytes)
|
||||
m.Set("counter_read_bytes", &f.readBytes)
|
||||
m.Set("counter_filched_bytes", &f.filchedBytes)
|
||||
m.Set("counter_dropped_bytes", &f.droppedBytes)
|
||||
m.Set("gauge_stored_bytes", &f.storedBytes)
|
||||
return m
|
||||
}
|
||||
|
||||
func (f *Filch) unreadReadBuffer() []byte {
|
||||
return f.rdBuf[f.rdBufIdx:]
|
||||
}
|
||||
func (f *Filch) availReadBuffer() []byte {
|
||||
return f.rdBuf[len(f.rdBuf):cap(f.rdBuf)]
|
||||
}
|
||||
func (f *Filch) resetReadBuffer() {
|
||||
f.rdBufIdx, f.rdBuf = 0, f.rdBuf[:0]
|
||||
}
|
||||
func (f *Filch) moveReadBufferToFront() {
|
||||
f.rdBufIdx, f.rdBuf = 0, f.rdBuf[:copy(f.rdBuf, f.rdBuf[f.rdBufIdx:])]
|
||||
}
|
||||
func (f *Filch) growReadBuffer() {
|
||||
f.rdBuf = slices.Grow(f.rdBuf, cap(f.rdBuf)+1)
|
||||
}
|
||||
func (f *Filch) consumeReadBuffer(n int) {
|
||||
f.rdBufIdx += n
|
||||
}
|
||||
func (f *Filch) appendReadBuffer(n int) {
|
||||
f.rdBuf = f.rdBuf[:len(f.rdBuf)+n]
|
||||
f.rdBufMaxLen = max(f.rdBufMaxLen, len(f.rdBuf))
|
||||
}
|
||||
|
||||
// TryReadline implements the logtail.Buffer interface.
|
||||
func (f *Filch) TryReadLine() ([]byte, error) {
|
||||
func (f *Filch) TryReadLine() (b []byte, err error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
|
||||
if f.altscan != nil {
|
||||
if b, err := f.scan(); b != nil || err != nil {
|
||||
return b, err
|
||||
}
|
||||
if f.older == nil {
|
||||
return nil, io.EOF
|
||||
}
|
||||
|
||||
f.cur, f.alt = f.alt, f.cur
|
||||
if f.OrigStderr != nil {
|
||||
if err := dup2Stderr(f.cur); err != nil {
|
||||
var tooLong bool // whether we are in a line that is too long
|
||||
defer func() {
|
||||
f.consumeReadBuffer(len(b))
|
||||
if tooLong || len(b) > f.maxLineSize {
|
||||
f.droppedBytes.Add(int64(len(b)))
|
||||
b, err = nil, cmp.Or(err, errTooLong)
|
||||
} else {
|
||||
f.readBytes.Add(int64(len(b)))
|
||||
}
|
||||
if len(b) != 0 || err != nil {
|
||||
f.readCalls.Add(1)
|
||||
}
|
||||
if err != nil {
|
||||
f.callErrors.Add(1)
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
// Check if unread buffer already has the next line.
|
||||
unread := f.unreadReadBuffer()
|
||||
if i := bytes.IndexByte(unread, '\n') + len("\n"); i > 0 {
|
||||
return unread[:i], nil
|
||||
}
|
||||
|
||||
// Check whether to make space for more data to read.
|
||||
avail := f.availReadBuffer()
|
||||
if len(avail) == 0 {
|
||||
switch {
|
||||
case len(unread) > f.maxLineSize:
|
||||
tooLong = true
|
||||
f.droppedBytes.Add(int64(len(unread)))
|
||||
f.resetReadBuffer()
|
||||
case len(unread) < cap(f.rdBuf)/10:
|
||||
f.moveReadBufferToFront()
|
||||
default:
|
||||
f.growReadBuffer()
|
||||
}
|
||||
avail = f.availReadBuffer() // invariant: len(avail) > 0
|
||||
}
|
||||
|
||||
// Read data into the available buffer.
|
||||
n, err := f.older.Read(avail)
|
||||
f.appendReadBuffer(n)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
unread = f.unreadReadBuffer()
|
||||
if len(unread) == 0 {
|
||||
if err := f.rotateLocked(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if f.storedBytes.Value() == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
return unread, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if _, err := f.alt.Seek(0, io.SeekStart); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f.altscan = bufio.NewScanner(f.alt)
|
||||
f.altscan.Buffer(f.buf[:], bufio.MaxScanTokenSize)
|
||||
f.altscan.Split(splitLines)
|
||||
return f.scan()
|
||||
}
|
||||
|
||||
func (f *Filch) scan() ([]byte, error) {
|
||||
if f.altscan.Scan() {
|
||||
return f.altscan.Bytes(), nil
|
||||
}
|
||||
err := f.altscan.Err()
|
||||
err2 := f.alt.Truncate(0)
|
||||
_, err3 := f.alt.Seek(0, io.SeekStart)
|
||||
f.altscan = nil
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err2 != nil {
|
||||
return nil, err2
|
||||
}
|
||||
if err3 != nil {
|
||||
return nil, err3
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
var alwaysStatForTests bool
|
||||
|
||||
// Write implements the logtail.Buffer interface.
|
||||
func (f *Filch) Write(b []byte) (int, error) {
|
||||
func (f *Filch) Write(b []byte) (n int, err error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
if f.writeCounter == 100 {
|
||||
// Check the file size every 100 writes.
|
||||
f.writeCounter = 0
|
||||
fi, err := f.cur.Stat()
|
||||
if f.newer == nil {
|
||||
return 0, errClosed
|
||||
}
|
||||
|
||||
defer func() {
|
||||
f.writeCalls.Add(1)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
f.callErrors.Add(1)
|
||||
}
|
||||
if fi.Size() >= f.maxFileSize {
|
||||
// This most likely means we are not draining.
|
||||
// To limit the amount of space we use, throw away the old logs.
|
||||
if err := moveContents(f.alt, f.cur); err != nil {
|
||||
}()
|
||||
|
||||
// To make sure we do not write data to disk unbounded
|
||||
// (in the event that we are not draining fast enough)
|
||||
// check whether we exceeded maxFileSize.
|
||||
// If so, then force a file rotation.
|
||||
if f.newlyWrittenBytes+f.newlyFilchedBytes > f.maxFileSize || f.writeCalls.Value()%100 == 0 || alwaysStatForTests {
|
||||
f.statAndUpdateBytes()
|
||||
if f.newlyWrittenBytes+f.newlyFilchedBytes > f.maxFileSize {
|
||||
if err := f.rotateLocked(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
}
|
||||
f.writeCounter++
|
||||
|
||||
// Write the log entry (appending a newline character if needed).
|
||||
var newline string
|
||||
if len(b) == 0 || b[len(b)-1] != '\n' {
|
||||
bnl := make([]byte, len(b)+1)
|
||||
copy(bnl, b)
|
||||
bnl[len(bnl)-1] = '\n'
|
||||
return f.cur.Write(bnl)
|
||||
newline = "\n"
|
||||
f.wrBuf = append(append(f.wrBuf[:0], b...), newline...)
|
||||
f.wrBufMaxLen = max(f.wrBufMaxLen, len(f.wrBuf))
|
||||
b = f.wrBuf
|
||||
}
|
||||
return f.cur.Write(b)
|
||||
if len(b) > f.maxLineSize {
|
||||
for line := range bytes.Lines(b) {
|
||||
if len(line) > f.maxLineSize {
|
||||
return 0, errTooLong
|
||||
}
|
||||
}
|
||||
}
|
||||
n, err = f.newer.Write(b)
|
||||
f.writeBytes.Add(int64(n))
|
||||
f.storedBytes.Add(int64(n))
|
||||
f.newlyWrittenBytes += int64(n)
|
||||
return n - len(newline), err // subtract possibly appended newline
|
||||
}
|
||||
|
||||
// Close closes the Filch, releasing all os resources.
|
||||
func (f *Filch) Close() (err error) {
|
||||
func (f *Filch) statAndUpdateBytes() {
|
||||
if fi, err := f.newer.Stat(); err == nil {
|
||||
prevSize := f.newlyWrittenBytes + f.newlyFilchedBytes
|
||||
filchedBytes := max(0, fi.Size()-prevSize)
|
||||
f.writeBytes.Add(filchedBytes)
|
||||
f.filchedBytes.Add(filchedBytes)
|
||||
f.storedBytes.Add(filchedBytes)
|
||||
f.newlyFilchedBytes += filchedBytes
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Filch) storedBytesForTest() int64 {
|
||||
return must.Get(f.newer.Stat()).Size() + must.Get(f.older.Stat()).Size()
|
||||
}
|
||||
|
||||
var activeStderrWriteForTest sync.RWMutex
|
||||
|
||||
// stderrWriteForTest calls [os.Stderr.Write], but respects calls to [waitIdleStderrForTest].
|
||||
func stderrWriteForTest(b []byte) int {
|
||||
activeStderrWriteForTest.RLock()
|
||||
defer activeStderrWriteForTest.RUnlock()
|
||||
return must.Get(os.Stderr.Write(b))
|
||||
}
|
||||
|
||||
// waitIdleStderrForTest waits until there are no active stderrWriteForTest calls.
|
||||
func waitIdleStderrForTest() {
|
||||
activeStderrWriteForTest.Lock()
|
||||
defer activeStderrWriteForTest.Unlock()
|
||||
}
|
||||
|
||||
// rotateLocked swaps f.newer and f.older such that:
|
||||
//
|
||||
// - f.newer will be truncated and future writes will be appended to the end.
|
||||
// - if [Options.ReplaceStderr], then stderr writes will redirect to f.newer
|
||||
// - f.older will contain historical data, reads will consume from the start.
|
||||
// - f.older is guaranteed to be immutable.
|
||||
//
|
||||
// There are two reasons for rotating:
|
||||
//
|
||||
// - The reader finished reading f.older.
|
||||
// No data should be lost under this condition.
|
||||
//
|
||||
// - The writer exceeded a limit for f.newer.
|
||||
// Data may be lost under this cxondition.
|
||||
func (f *Filch) rotateLocked() error {
|
||||
f.rotateCalls.Add(1)
|
||||
|
||||
// Truncate the older file.
|
||||
if fi, err := f.older.Stat(); err != nil {
|
||||
return err
|
||||
} else if fi.Size() > 0 {
|
||||
// Update dropped bytes.
|
||||
if pos, err := f.older.Seek(0, io.SeekCurrent); err == nil {
|
||||
rdPos := pos - int64(len(f.unreadReadBuffer())) // adjust for data already read into the read buffer
|
||||
f.droppedBytes.Add(max(0, fi.Size()-rdPos))
|
||||
}
|
||||
f.resetReadBuffer()
|
||||
|
||||
// Truncate the older file and write relative to the start.
|
||||
if err := f.older.Truncate(0); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := f.older.Seek(0, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Swap newer and older.
|
||||
f.newer, f.older = f.older, f.newer
|
||||
|
||||
// If necessary, filch stderr into newer instead of older.
|
||||
// This must be done after truncation otherwise
|
||||
// we might lose some stderr data asynchronously written
|
||||
// right in the middle of a rotation.
|
||||
// Note that mutex does not prevent stderr writes.
|
||||
prevSize := f.newlyWrittenBytes + f.newlyFilchedBytes
|
||||
f.newlyWrittenBytes, f.newlyFilchedBytes = 0, 0
|
||||
if f.OrigStderr != nil {
|
||||
if err := dup2Stderr(f.newer); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Update filched bytes and stored bytes metrics.
|
||||
// This must be done after filching to newer
|
||||
// so that f.older.Stat is *mostly* stable.
|
||||
//
|
||||
// NOTE: Unfortunately, an asynchronous os.Stderr.Write call
|
||||
// that is already in progress when we called dup2Stderr
|
||||
// will still write to the previous FD and
|
||||
// may not be immediately observable by this Stat call.
|
||||
// This is fundamentally unsolvable with the current design
|
||||
// as we cannot synchronize all other os.Stderr.Write calls.
|
||||
// In rare cases, it is possible that [Filch.TryReadLine] consumes
|
||||
// the entire older file before the write commits,
|
||||
// leading to dropped stderr lines.
|
||||
waitIdleStderrForTest()
|
||||
if fi, err := f.older.Stat(); err != nil {
|
||||
return err
|
||||
} else {
|
||||
filchedBytes := max(0, fi.Size()-prevSize)
|
||||
f.writeBytes.Add(filchedBytes)
|
||||
f.filchedBytes.Add(filchedBytes)
|
||||
f.storedBytes.Set(fi.Size()) // newer has been truncated, so only older matters
|
||||
}
|
||||
|
||||
// Start reading from the start of older.
|
||||
if _, err := f.older.Seek(0, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Garbage collect unnecessarily large buffers.
|
||||
mayGarbageCollect := func(b []byte, maxLen int) ([]byte, int) {
|
||||
if cap(b)/4 > maxLen { // if less than 25% utilized
|
||||
b = slices.Grow([]byte(nil), 2*maxLen)
|
||||
}
|
||||
maxLen = 3 * (maxLen / 4) // reduce by 25%
|
||||
return b, maxLen
|
||||
}
|
||||
f.wrBuf, f.wrBufMaxLen = mayGarbageCollect(f.wrBuf, f.wrBufMaxLen)
|
||||
f.rdBuf, f.rdBufMaxLen = mayGarbageCollect(f.rdBuf, f.rdBufMaxLen)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close closes the Filch, releasing all resources.
|
||||
func (f *Filch) Close() error {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
|
||||
var errUnsave, errCloseNew, errCloseOld error
|
||||
if f.OrigStderr != nil {
|
||||
if err2 := unsaveStderr(f.OrigStderr); err == nil {
|
||||
err = err2
|
||||
}
|
||||
errUnsave = unsaveStderr(f.OrigStderr)
|
||||
f.OrigStderr = nil
|
||||
}
|
||||
|
||||
if err2 := f.cur.Close(); err == nil {
|
||||
err = err2
|
||||
if f.newer != nil {
|
||||
errCloseNew = f.newer.Close()
|
||||
f.newer = nil
|
||||
}
|
||||
if err2 := f.alt.Close(); err == nil {
|
||||
err = err2
|
||||
if f.older != nil {
|
||||
errCloseOld = f.older.Close()
|
||||
f.older = nil
|
||||
}
|
||||
|
||||
return err
|
||||
return errors.Join(errUnsave, errCloseNew, errCloseOld)
|
||||
}
|
||||
|
||||
// New creates a new filch around two log files, each starting with filePrefix.
|
||||
@@ -181,14 +454,10 @@ func New(filePrefix string, opts Options) (f *Filch, err error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mfs := defaultMaxFileSize
|
||||
if opts.MaxFileSize > 0 {
|
||||
mfs = opts.MaxFileSize
|
||||
}
|
||||
f = &Filch{
|
||||
OrigStderr: os.Stderr, // temporary, for past logs recovery
|
||||
maxFileSize: int64(mfs),
|
||||
}
|
||||
f = new(Filch)
|
||||
f.maxLineSize = int(cmp.Or(max(0, opts.MaxLineSize), DefaultMaxLineSize))
|
||||
f.maxFileSize = int64(cmp.Or(max(0, opts.MaxFileSize), DefaultMaxFileSize))
|
||||
f.maxFileSize /= 2 // since there are two log files that combine to equal MaxFileSize
|
||||
|
||||
// Neither, either, or both files may exist and contain logs from
|
||||
// the last time the process ran. The three cases are:
|
||||
@@ -198,35 +467,22 @@ func New(filePrefix string, opts Options) (f *Filch, err error) {
|
||||
// - both: the files were swapped and were starting to be
|
||||
// read out, while new logs streamed into the other
|
||||
// file, but the read out did not complete
|
||||
if n := fi1.Size() + fi2.Size(); n > 0 {
|
||||
f.recovered = n
|
||||
}
|
||||
switch {
|
||||
case fi1.Size() > 0 && fi2.Size() == 0:
|
||||
f.cur, f.alt = f2, f1
|
||||
f.newer, f.older = f2, f1 // use empty file as newer
|
||||
case fi2.Size() > 0 && fi1.Size() == 0:
|
||||
f.cur, f.alt = f1, f2
|
||||
case fi1.Size() > 0 && fi2.Size() > 0: // both
|
||||
// We need to pick one of the files to be the elder,
|
||||
// which we do using the mtime.
|
||||
var older, newer *os.File
|
||||
if fi1.ModTime().Before(fi2.ModTime()) {
|
||||
older, newer = f1, f2
|
||||
} else {
|
||||
older, newer = f2, f1
|
||||
}
|
||||
if err := moveContents(older, newer); err != nil {
|
||||
fmt.Fprintf(f.OrigStderr, "filch: recover move failed: %v\n", err)
|
||||
fmt.Fprintf(older, "filch: recover move failed: %v\n", err)
|
||||
}
|
||||
f.cur, f.alt = newer, older
|
||||
f.newer, f.older = f1, f2 // use empty file as newer
|
||||
case fi1.ModTime().Before(fi2.ModTime()):
|
||||
f.newer, f.older = f2, f1 // use older file as older
|
||||
case fi2.ModTime().Before(fi1.ModTime()):
|
||||
f.newer, f.older = f1, f2 // use newer file as newer
|
||||
default:
|
||||
f.cur, f.alt = f1, f2 // does not matter
|
||||
f.newer, f.older = f1, f2 // does not matter
|
||||
}
|
||||
if f.recovered > 0 {
|
||||
f.altscan = bufio.NewScanner(f.alt)
|
||||
f.altscan.Buffer(f.buf[:], bufio.MaxScanTokenSize)
|
||||
f.altscan.Split(splitLines)
|
||||
f.writeBytes.Set(fi1.Size() + fi2.Size())
|
||||
f.storedBytes.Set(fi1.Size() + fi2.Size())
|
||||
if fi, err := f.newer.Stat(); err == nil {
|
||||
f.newlyWrittenBytes = fi.Size()
|
||||
}
|
||||
|
||||
f.OrigStderr = nil
|
||||
@@ -235,50 +491,10 @@ func New(filePrefix string, opts Options) (f *Filch, err error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := dup2Stderr(f.cur); err != nil {
|
||||
if err := dup2Stderr(f.newer); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func moveContents(dst, src *os.File) (err error) {
|
||||
defer func() {
|
||||
_, err2 := src.Seek(0, io.SeekStart)
|
||||
err3 := src.Truncate(0)
|
||||
_, err4 := dst.Seek(0, io.SeekStart)
|
||||
if err == nil {
|
||||
err = err2
|
||||
}
|
||||
if err == nil {
|
||||
err = err3
|
||||
}
|
||||
if err == nil {
|
||||
err = err4
|
||||
}
|
||||
}()
|
||||
if _, err := src.Seek(0, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := dst.Seek(0, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(dst, src); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func splitLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||
if atEOF && len(data) == 0 {
|
||||
return 0, nil, nil
|
||||
}
|
||||
if i := bytes.IndexByte(data, '\n'); i >= 0 {
|
||||
return i + 1, data[0 : i+1], nil
|
||||
}
|
||||
if atEOF {
|
||||
return len(data), data, nil
|
||||
}
|
||||
return 0, nil, nil
|
||||
}
|
||||
|
||||
34
vendor/tailscale.com/logtail/filch/filch_omit.go
generated
vendored
Normal file
34
vendor/tailscale.com/logtail/filch/filch_omit.go
generated
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build ts_omit_logtail
|
||||
|
||||
package filch
|
||||
|
||||
import "os"
|
||||
|
||||
type Options struct {
|
||||
ReplaceStderr bool
|
||||
MaxLineSize int
|
||||
MaxFileSize int
|
||||
}
|
||||
|
||||
type Filch struct {
|
||||
OrigStderr *os.File
|
||||
}
|
||||
|
||||
func (*Filch) TryReadLine() ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (*Filch) Write(b []byte) (int, error) {
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
func (f *Filch) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func New(string, Options) (*Filch, error) {
|
||||
return new(Filch), nil
|
||||
}
|
||||
8
vendor/tailscale.com/logtail/filch/filch_stub.go
generated
vendored
8
vendor/tailscale.com/logtail/filch/filch_stub.go
generated
vendored
@@ -1,13 +1,13 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build wasm || plan9 || tamago
|
||||
//go:build !ts_omit_logtail && (wasm || plan9 || tamago)
|
||||
|
||||
package filch
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
import "os"
|
||||
|
||||
const replaceStderrSupportedForTest = false
|
||||
|
||||
func saveStderr() (*os.File, error) {
|
||||
return os.Stderr, nil
|
||||
|
||||
4
vendor/tailscale.com/logtail/filch/filch_unix.go
generated
vendored
4
vendor/tailscale.com/logtail/filch/filch_unix.go
generated
vendored
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !windows && !wasm && !plan9 && !tamago
|
||||
//go:build !ts_omit_logtail && !windows && !wasm && !plan9 && !tamago
|
||||
|
||||
package filch
|
||||
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
const replaceStderrSupportedForTest = true
|
||||
|
||||
func saveStderr() (*os.File, error) {
|
||||
fd, err := unix.Dup(stderrFD)
|
||||
if err != nil {
|
||||
|
||||
4
vendor/tailscale.com/logtail/filch/filch_windows.go
generated
vendored
4
vendor/tailscale.com/logtail/filch/filch_windows.go
generated
vendored
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_logtail && windows
|
||||
|
||||
package filch
|
||||
|
||||
import (
|
||||
@@ -9,6 +11,8 @@ import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
const replaceStderrSupportedForTest = true
|
||||
|
||||
var kernel32 = syscall.MustLoadDLL("kernel32.dll")
|
||||
var procSetStdHandle = kernel32.MustFindProc("SetStdHandle")
|
||||
|
||||
|
||||
417
vendor/tailscale.com/logtail/logtail.go
generated
vendored
417
vendor/tailscale.com/logtail/logtail.go
generated
vendored
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_logtail
|
||||
|
||||
// Package logtail sends logs to log.tailscale.com.
|
||||
package logtail
|
||||
|
||||
@@ -10,14 +12,13 @@ import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"expvar"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
mrand "math/rand/v2"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"os"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
@@ -25,14 +26,16 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/creachadair/msync/trigger"
|
||||
"github.com/go-json-experiment/json/jsontext"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/metrics"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/sockstats"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/tstime"
|
||||
tslogger "tailscale.com/types/logger"
|
||||
"tailscale.com/types/logid"
|
||||
"tailscale.com/util/eventbus"
|
||||
"tailscale.com/util/set"
|
||||
"tailscale.com/util/truncate"
|
||||
"tailscale.com/util/zstdframe"
|
||||
@@ -54,58 +57,6 @@ const lowMemRatio = 4
|
||||
// but not too large to be a notable waste of memory if retained forever.
|
||||
const bufferSize = 4 << 10
|
||||
|
||||
// DefaultHost is the default host name to upload logs to when
|
||||
// Config.BaseURL isn't provided.
|
||||
const DefaultHost = "log.tailscale.com"
|
||||
|
||||
const defaultFlushDelay = 2 * time.Second
|
||||
|
||||
const (
|
||||
// CollectionNode is the name of a logtail Config.Collection
|
||||
// for tailscaled (or equivalent: IPNExtension, Android app).
|
||||
CollectionNode = "tailnode.log.tailscale.io"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Collection string // collection name, a domain name
|
||||
PrivateID logid.PrivateID // private ID for the primary log stream
|
||||
CopyPrivateID logid.PrivateID // private ID for a log stream that is a superset of this log stream
|
||||
BaseURL string // if empty defaults to "https://log.tailscale.com"
|
||||
HTTPC *http.Client // if empty defaults to http.DefaultClient
|
||||
SkipClientTime bool // if true, client_time is not written to logs
|
||||
LowMemory bool // if true, logtail minimizes memory use
|
||||
Clock tstime.Clock // if set, Clock.Now substitutes uses of time.Now
|
||||
Stderr io.Writer // if set, logs are sent here instead of os.Stderr
|
||||
StderrLevel int // max verbosity level to write to stderr; 0 means the non-verbose messages only
|
||||
Buffer Buffer // temp storage, if nil a MemoryBuffer
|
||||
CompressLogs bool // whether to compress the log uploads
|
||||
MaxUploadSize int // maximum upload size; 0 means using the default
|
||||
|
||||
// MetricsDelta, if non-nil, is a func that returns an encoding
|
||||
// delta in clientmetrics to upload alongside existing logs.
|
||||
// It can return either an empty string (for nothing) or a string
|
||||
// that's safe to embed in a JSON string literal without further escaping.
|
||||
MetricsDelta func() string
|
||||
|
||||
// FlushDelayFn, if non-nil is a func that returns how long to wait to
|
||||
// accumulate logs before uploading them. 0 or negative means to upload
|
||||
// immediately.
|
||||
//
|
||||
// If nil, a default value is used. (currently 2 seconds)
|
||||
FlushDelayFn func() time.Duration
|
||||
|
||||
// IncludeProcID, if true, results in an ephemeral process identifier being
|
||||
// included in logs. The ID is random and not guaranteed to be globally
|
||||
// unique, but it can be used to distinguish between different instances
|
||||
// running with same PrivateID.
|
||||
IncludeProcID bool
|
||||
|
||||
// IncludeProcSequence, if true, results in an ephemeral sequence number
|
||||
// being included in the logs. The sequence number is incremented for each
|
||||
// log message sent, but is not persisted across process restarts.
|
||||
IncludeProcSequence bool
|
||||
}
|
||||
|
||||
func NewLogger(cfg Config, logf tslogger.Logf) *Logger {
|
||||
if cfg.BaseURL == "" {
|
||||
cfg.BaseURL = "https://" + DefaultHost
|
||||
@@ -151,7 +102,7 @@ func NewLogger(cfg Config, logf tslogger.Logf) *Logger {
|
||||
if !cfg.CopyPrivateID.IsZero() {
|
||||
urlSuffix = "?copyId=" + cfg.CopyPrivateID.String()
|
||||
}
|
||||
l := &Logger{
|
||||
logger := &Logger{
|
||||
privateID: cfg.PrivateID,
|
||||
stderr: cfg.Stderr,
|
||||
stderrLevel: int64(cfg.StderrLevel),
|
||||
@@ -173,15 +124,21 @@ func NewLogger(cfg Config, logf tslogger.Logf) *Logger {
|
||||
shutdownStart: make(chan struct{}),
|
||||
shutdownDone: make(chan struct{}),
|
||||
}
|
||||
l.SetSockstatsLabel(sockstats.LabelLogtailLogger)
|
||||
l.compressLogs = cfg.CompressLogs
|
||||
|
||||
if cfg.Bus != nil {
|
||||
logger.eventClient = cfg.Bus.Client("logtail.Logger")
|
||||
// Subscribe to change deltas from NetMon to detect when the network comes up.
|
||||
eventbus.SubscribeFunc(logger.eventClient, logger.onChangeDelta)
|
||||
}
|
||||
logger.SetSockstatsLabel(sockstats.LabelLogtailLogger)
|
||||
logger.compressLogs = cfg.CompressLogs
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
l.uploadCancel = cancel
|
||||
logger.uploadCancel = cancel
|
||||
|
||||
go l.uploading(ctx)
|
||||
l.Write([]byte("logtail started"))
|
||||
return l
|
||||
go logger.uploading(ctx)
|
||||
logger.Write([]byte("logtail started"))
|
||||
return logger
|
||||
}
|
||||
|
||||
// Logger writes logs, splitting them as configured between local
|
||||
@@ -209,6 +166,8 @@ type Logger struct {
|
||||
privateID logid.PrivateID
|
||||
httpDoCalls atomic.Int32
|
||||
sockstatsLabel atomicSocktatsLabel
|
||||
eventClient *eventbus.Client
|
||||
networkIsUp trigger.Cond // set/reset by netmon.ChangeDelta events
|
||||
|
||||
procID uint32
|
||||
includeProcSequence bool
|
||||
@@ -223,6 +182,12 @@ type Logger struct {
|
||||
shutdownStartMu sync.Mutex // guards the closing of shutdownStart
|
||||
shutdownStart chan struct{} // closed when shutdown begins
|
||||
shutdownDone chan struct{} // closed when shutdown complete
|
||||
|
||||
// Metrics (see [Logger.ExpVar] for details).
|
||||
uploadCalls expvar.Int
|
||||
failedCalls expvar.Int
|
||||
uploadedBytes expvar.Int
|
||||
uploadingTime expvar.Int
|
||||
}
|
||||
|
||||
type atomicSocktatsLabel struct{ p atomic.Uint32 }
|
||||
@@ -233,27 +198,27 @@ func (p *atomicSocktatsLabel) Store(label sockstats.Label) { p.p.Store(uint32(la
|
||||
// SetVerbosityLevel controls the verbosity level that should be
|
||||
// written to stderr. 0 is the default (not verbose). Levels 1 or higher
|
||||
// are increasingly verbose.
|
||||
func (l *Logger) SetVerbosityLevel(level int) {
|
||||
atomic.StoreInt64(&l.stderrLevel, int64(level))
|
||||
func (lg *Logger) SetVerbosityLevel(level int) {
|
||||
atomic.StoreInt64(&lg.stderrLevel, int64(level))
|
||||
}
|
||||
|
||||
// SetNetMon sets the network monitor.
|
||||
//
|
||||
// It should not be changed concurrently with log writes and should
|
||||
// only be set once.
|
||||
func (l *Logger) SetNetMon(lm *netmon.Monitor) {
|
||||
l.netMonitor = lm
|
||||
func (lg *Logger) SetNetMon(lm *netmon.Monitor) {
|
||||
lg.netMonitor = lm
|
||||
}
|
||||
|
||||
// SetSockstatsLabel sets the label used in sockstat logs to identify network traffic from this logger.
|
||||
func (l *Logger) SetSockstatsLabel(label sockstats.Label) {
|
||||
l.sockstatsLabel.Store(label)
|
||||
func (lg *Logger) SetSockstatsLabel(label sockstats.Label) {
|
||||
lg.sockstatsLabel.Store(label)
|
||||
}
|
||||
|
||||
// PrivateID returns the logger's private log ID.
|
||||
//
|
||||
// It exists for internal use only.
|
||||
func (l *Logger) PrivateID() logid.PrivateID { return l.privateID }
|
||||
func (lg *Logger) PrivateID() logid.PrivateID { return lg.privateID }
|
||||
|
||||
// Shutdown gracefully shuts down the logger while completing any
|
||||
// remaining uploads.
|
||||
@@ -261,30 +226,33 @@ func (l *Logger) PrivateID() logid.PrivateID { return l.privateID }
|
||||
// It will block, continuing to try and upload unless the passed
|
||||
// context object interrupts it by being done.
|
||||
// If the shutdown is interrupted, an error is returned.
|
||||
func (l *Logger) Shutdown(ctx context.Context) error {
|
||||
func (lg *Logger) Shutdown(ctx context.Context) error {
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
l.uploadCancel()
|
||||
<-l.shutdownDone
|
||||
case <-l.shutdownDone:
|
||||
lg.uploadCancel()
|
||||
<-lg.shutdownDone
|
||||
case <-lg.shutdownDone:
|
||||
}
|
||||
close(done)
|
||||
l.httpc.CloseIdleConnections()
|
||||
lg.httpc.CloseIdleConnections()
|
||||
}()
|
||||
|
||||
l.shutdownStartMu.Lock()
|
||||
if lg.eventClient != nil {
|
||||
lg.eventClient.Close()
|
||||
}
|
||||
lg.shutdownStartMu.Lock()
|
||||
select {
|
||||
case <-l.shutdownStart:
|
||||
l.shutdownStartMu.Unlock()
|
||||
case <-lg.shutdownStart:
|
||||
lg.shutdownStartMu.Unlock()
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
close(l.shutdownStart)
|
||||
l.shutdownStartMu.Unlock()
|
||||
close(lg.shutdownStart)
|
||||
lg.shutdownStartMu.Unlock()
|
||||
|
||||
io.WriteString(l, "logger closing down\n")
|
||||
io.WriteString(lg, "logger closing down\n")
|
||||
<-done
|
||||
|
||||
return nil
|
||||
@@ -294,8 +262,8 @@ func (l *Logger) Shutdown(ctx context.Context) error {
|
||||
// process, and any associated goroutines.
|
||||
//
|
||||
// Deprecated: use Shutdown
|
||||
func (l *Logger) Close() {
|
||||
l.Shutdown(context.Background())
|
||||
func (lg *Logger) Close() {
|
||||
lg.Shutdown(context.Background())
|
||||
}
|
||||
|
||||
// drainBlock is called by drainPending when there are no logs to drain.
|
||||
@@ -305,11 +273,11 @@ func (l *Logger) Close() {
|
||||
//
|
||||
// If the caller specified FlushInterface, drainWake is only sent to
|
||||
// periodically.
|
||||
func (l *Logger) drainBlock() (shuttingDown bool) {
|
||||
func (lg *Logger) drainBlock() (shuttingDown bool) {
|
||||
select {
|
||||
case <-l.shutdownStart:
|
||||
case <-lg.shutdownStart:
|
||||
return true
|
||||
case <-l.drainWake:
|
||||
case <-lg.drainWake:
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -317,20 +285,20 @@ func (l *Logger) drainBlock() (shuttingDown bool) {
|
||||
// drainPending drains and encodes a batch of logs from the buffer for upload.
|
||||
// If no logs are available, drainPending blocks until logs are available.
|
||||
// The returned buffer is only valid until the next call to drainPending.
|
||||
func (l *Logger) drainPending() (b []byte) {
|
||||
b = l.drainBuf[:0]
|
||||
func (lg *Logger) drainPending() (b []byte) {
|
||||
b = lg.drainBuf[:0]
|
||||
b = append(b, '[')
|
||||
defer func() {
|
||||
b = bytes.TrimRight(b, ",")
|
||||
b = append(b, ']')
|
||||
l.drainBuf = b
|
||||
lg.drainBuf = b
|
||||
if len(b) <= len("[]") {
|
||||
b = nil
|
||||
}
|
||||
}()
|
||||
|
||||
maxLen := cmp.Or(l.maxUploadSize, maxSize)
|
||||
if l.lowMem {
|
||||
maxLen := cmp.Or(lg.maxUploadSize, maxSize)
|
||||
if lg.lowMem {
|
||||
// When operating in a low memory environment, it is better to upload
|
||||
// in multiple operations than it is to allocate a large body and OOM.
|
||||
// Even if maxLen is less than maxSize, we can still upload an entry
|
||||
@@ -338,13 +306,13 @@ func (l *Logger) drainPending() (b []byte) {
|
||||
maxLen /= lowMemRatio
|
||||
}
|
||||
for len(b) < maxLen {
|
||||
line, err := l.buffer.TryReadLine()
|
||||
line, err := lg.buffer.TryReadLine()
|
||||
switch {
|
||||
case err == io.EOF:
|
||||
return b
|
||||
case err != nil:
|
||||
b = append(b, '{')
|
||||
b = l.appendMetadata(b, false, true, 0, 0, "reading ringbuffer: "+err.Error(), nil, 0)
|
||||
b = lg.appendMetadata(b, false, true, 0, 0, "reading ringbuffer: "+err.Error(), nil, 0)
|
||||
b = bytes.TrimRight(b, ",")
|
||||
b = append(b, '}')
|
||||
return b
|
||||
@@ -358,10 +326,10 @@ func (l *Logger) drainPending() (b []byte) {
|
||||
// in our buffer from a previous large write, let it go.
|
||||
if cap(b) > bufferSize {
|
||||
b = bytes.Clone(b)
|
||||
l.drainBuf = b
|
||||
lg.drainBuf = b
|
||||
}
|
||||
|
||||
if shuttingDown := l.drainBlock(); shuttingDown {
|
||||
if shuttingDown := lg.drainBlock(); shuttingDown {
|
||||
return b
|
||||
}
|
||||
continue
|
||||
@@ -378,18 +346,18 @@ func (l *Logger) drainPending() (b []byte) {
|
||||
default:
|
||||
// This is probably a log added to stderr by filch
|
||||
// outside of the logtail logger. Encode it.
|
||||
if !l.explainedRaw {
|
||||
fmt.Fprintf(l.stderr, "RAW-STDERR: ***\n")
|
||||
fmt.Fprintf(l.stderr, "RAW-STDERR: *** Lines prefixed with RAW-STDERR below bypassed logtail and probably come from a previous run of the program\n")
|
||||
fmt.Fprintf(l.stderr, "RAW-STDERR: ***\n")
|
||||
fmt.Fprintf(l.stderr, "RAW-STDERR:\n")
|
||||
l.explainedRaw = true
|
||||
if !lg.explainedRaw {
|
||||
fmt.Fprintf(lg.stderr, "RAW-STDERR: ***\n")
|
||||
fmt.Fprintf(lg.stderr, "RAW-STDERR: *** Lines prefixed with RAW-STDERR below bypassed logtail and probably come from a previous run of the program\n")
|
||||
fmt.Fprintf(lg.stderr, "RAW-STDERR: ***\n")
|
||||
fmt.Fprintf(lg.stderr, "RAW-STDERR:\n")
|
||||
lg.explainedRaw = true
|
||||
}
|
||||
fmt.Fprintf(l.stderr, "RAW-STDERR: %s", b)
|
||||
fmt.Fprintf(lg.stderr, "RAW-STDERR: %s", b)
|
||||
// Do not add a client time, as it could be really old.
|
||||
// Do not include instance key or ID either,
|
||||
// since this came from a different instance.
|
||||
b = l.appendText(b, line, true, 0, 0, 0)
|
||||
b = lg.appendText(b, line, true, 0, 0, 0)
|
||||
}
|
||||
b = append(b, ',')
|
||||
}
|
||||
@@ -397,14 +365,14 @@ func (l *Logger) drainPending() (b []byte) {
|
||||
}
|
||||
|
||||
// This is the goroutine that repeatedly uploads logs in the background.
|
||||
func (l *Logger) uploading(ctx context.Context) {
|
||||
defer close(l.shutdownDone)
|
||||
func (lg *Logger) uploading(ctx context.Context) {
|
||||
defer close(lg.shutdownDone)
|
||||
|
||||
for {
|
||||
body := l.drainPending()
|
||||
body := lg.drainPending()
|
||||
origlen := -1 // sentinel value: uncompressed
|
||||
// Don't attempt to compress tiny bodies; not worth the CPU cycles.
|
||||
if l.compressLogs && len(body) > 256 {
|
||||
if lg.compressLogs && len(body) > 256 {
|
||||
zbody := zstdframe.AppendEncode(nil, body,
|
||||
zstdframe.FastestCompression, zstdframe.LowMemory(true))
|
||||
|
||||
@@ -421,20 +389,20 @@ func (l *Logger) uploading(ctx context.Context) {
|
||||
var numFailures int
|
||||
var firstFailure time.Time
|
||||
for len(body) > 0 && ctx.Err() == nil {
|
||||
retryAfter, err := l.upload(ctx, body, origlen)
|
||||
retryAfter, err := lg.upload(ctx, body, origlen)
|
||||
if err != nil {
|
||||
numFailures++
|
||||
firstFailure = l.clock.Now()
|
||||
firstFailure = lg.clock.Now()
|
||||
|
||||
if !l.internetUp() {
|
||||
fmt.Fprintf(l.stderr, "logtail: internet down; waiting\n")
|
||||
l.awaitInternetUp(ctx)
|
||||
if !lg.internetUp() {
|
||||
fmt.Fprintf(lg.stderr, "logtail: internet down; waiting\n")
|
||||
lg.awaitInternetUp(ctx)
|
||||
continue
|
||||
}
|
||||
|
||||
// Only print the same message once.
|
||||
if currError := err.Error(); lastError != currError {
|
||||
fmt.Fprintf(l.stderr, "logtail: upload: %v\n", err)
|
||||
fmt.Fprintf(lg.stderr, "logtail: upload: %v\n", err)
|
||||
lastError = currError
|
||||
}
|
||||
|
||||
@@ -447,44 +415,68 @@ func (l *Logger) uploading(ctx context.Context) {
|
||||
} else {
|
||||
// Only print a success message after recovery.
|
||||
if numFailures > 0 {
|
||||
fmt.Fprintf(l.stderr, "logtail: upload succeeded after %d failures and %s\n", numFailures, l.clock.Since(firstFailure).Round(time.Second))
|
||||
fmt.Fprintf(lg.stderr, "logtail: upload succeeded after %d failures and %s\n", numFailures, lg.clock.Since(firstFailure).Round(time.Second))
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
case <-l.shutdownStart:
|
||||
case <-lg.shutdownStart:
|
||||
return
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Logger) internetUp() bool {
|
||||
if l.netMonitor == nil {
|
||||
// No way to tell, so assume it is.
|
||||
func (lg *Logger) internetUp() bool {
|
||||
select {
|
||||
case <-lg.networkIsUp.Ready():
|
||||
return true
|
||||
default:
|
||||
if lg.netMonitor == nil {
|
||||
return true // No way to tell, so assume it is.
|
||||
}
|
||||
return lg.netMonitor.InterfaceState().AnyInterfaceUp()
|
||||
}
|
||||
return l.netMonitor.InterfaceState().AnyInterfaceUp()
|
||||
}
|
||||
|
||||
func (l *Logger) awaitInternetUp(ctx context.Context) {
|
||||
// onChangeDelta is an eventbus subscriber function that handles
|
||||
// [netmon.ChangeDelta] events to detect whether the Internet is expected to be
|
||||
// reachable.
|
||||
func (lg *Logger) onChangeDelta(delta *netmon.ChangeDelta) {
|
||||
if delta.AnyInterfaceUp() {
|
||||
fmt.Fprintf(lg.stderr, "logtail: internet back up\n")
|
||||
lg.networkIsUp.Set()
|
||||
} else {
|
||||
fmt.Fprintf(lg.stderr, "logtail: network changed, but is not up\n")
|
||||
lg.networkIsUp.Reset()
|
||||
}
|
||||
}
|
||||
|
||||
func (lg *Logger) awaitInternetUp(ctx context.Context) {
|
||||
if lg.eventClient != nil {
|
||||
select {
|
||||
case <-lg.networkIsUp.Ready():
|
||||
case <-ctx.Done():
|
||||
}
|
||||
return
|
||||
}
|
||||
upc := make(chan bool, 1)
|
||||
defer l.netMonitor.RegisterChangeCallback(func(delta *netmon.ChangeDelta) {
|
||||
if delta.New.AnyInterfaceUp() {
|
||||
defer lg.netMonitor.RegisterChangeCallback(func(delta *netmon.ChangeDelta) {
|
||||
if delta.AnyInterfaceUp() {
|
||||
select {
|
||||
case upc <- true:
|
||||
default:
|
||||
}
|
||||
}
|
||||
})()
|
||||
if l.internetUp() {
|
||||
if lg.internetUp() {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-upc:
|
||||
fmt.Fprintf(l.stderr, "logtail: internet back up\n")
|
||||
fmt.Fprintf(lg.stderr, "logtail: internet back up\n")
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}
|
||||
@@ -492,13 +484,16 @@ func (l *Logger) awaitInternetUp(ctx context.Context) {
|
||||
// upload uploads body to the log server.
|
||||
// origlen indicates the pre-compression body length.
|
||||
// origlen of -1 indicates that the body is not compressed.
|
||||
func (l *Logger) upload(ctx context.Context, body []byte, origlen int) (retryAfter time.Duration, err error) {
|
||||
func (lg *Logger) upload(ctx context.Context, body []byte, origlen int) (retryAfter time.Duration, err error) {
|
||||
lg.uploadCalls.Add(1)
|
||||
startUpload := time.Now()
|
||||
|
||||
const maxUploadTime = 45 * time.Second
|
||||
ctx = sockstats.WithSockStats(ctx, l.sockstatsLabel.Load(), l.Logf)
|
||||
ctx = sockstats.WithSockStats(ctx, lg.sockstatsLabel.Load(), lg.Logf)
|
||||
ctx, cancel := context.WithTimeout(ctx, maxUploadTime)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", l.url, bytes.NewReader(body))
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", lg.url, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
// I know of no conditions under which this could fail.
|
||||
// Report it very loudly.
|
||||
@@ -529,18 +524,23 @@ func (l *Logger) upload(ctx context.Context, body []byte, origlen int) (retryAft
|
||||
compressedNote = "compressed"
|
||||
}
|
||||
|
||||
l.httpDoCalls.Add(1)
|
||||
resp, err := l.httpc.Do(req)
|
||||
lg.httpDoCalls.Add(1)
|
||||
resp, err := lg.httpc.Do(req)
|
||||
if err != nil {
|
||||
lg.failedCalls.Add(1)
|
||||
return 0, fmt.Errorf("log upload of %d bytes %s failed: %v", len(body), compressedNote, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
lg.failedCalls.Add(1)
|
||||
n, _ := strconv.Atoi(resp.Header.Get("Retry-After"))
|
||||
b, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<10))
|
||||
return time.Duration(n) * time.Second, fmt.Errorf("log upload of %d bytes %s failed %d: %s", len(body), compressedNote, resp.StatusCode, bytes.TrimSpace(b))
|
||||
}
|
||||
|
||||
lg.uploadedBytes.Add(int64(len(body)))
|
||||
lg.uploadingTime.Add(int64(time.Since(startUpload)))
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
@@ -549,19 +549,43 @@ func (l *Logger) upload(ctx context.Context, body []byte, origlen int) (retryAft
|
||||
//
|
||||
// TODO(bradfitz): this apparently just returns nil, as of tailscale/corp@9c2ec35.
|
||||
// Finish cleaning this up.
|
||||
func (l *Logger) Flush() error {
|
||||
func (lg *Logger) Flush() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartFlush starts a log upload, if anything is pending.
|
||||
//
|
||||
// If l is nil, StartFlush is a no-op.
|
||||
func (l *Logger) StartFlush() {
|
||||
if l != nil {
|
||||
l.tryDrainWake()
|
||||
func (lg *Logger) StartFlush() {
|
||||
if lg != nil {
|
||||
lg.tryDrainWake()
|
||||
}
|
||||
}
|
||||
|
||||
// ExpVar report metrics about the logger.
|
||||
//
|
||||
// - counter_upload_calls: Total number of upload attempts.
|
||||
//
|
||||
// - counter_upload_errors: Total number of upload attempts that failed.
|
||||
//
|
||||
// - counter_uploaded_bytes: Total number of bytes successfully uploaded
|
||||
// (which is calculated after compression is applied).
|
||||
//
|
||||
// - counter_uploading_nsecs: Total number of nanoseconds spent uploading.
|
||||
//
|
||||
// - buffer: An optional [metrics.Set] with metrics for the [Buffer].
|
||||
func (lg *Logger) ExpVar() expvar.Var {
|
||||
m := new(metrics.Set)
|
||||
m.Set("counter_upload_calls", &lg.uploadCalls)
|
||||
m.Set("counter_upload_errors", &lg.failedCalls)
|
||||
m.Set("counter_uploaded_bytes", &lg.uploadedBytes)
|
||||
m.Set("counter_uploading_nsecs", &lg.uploadingTime)
|
||||
if v, ok := lg.buffer.(interface{ ExpVar() expvar.Var }); ok {
|
||||
m.Set("buffer", v.ExpVar())
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// logtailDisabled is whether logtail uploads to logcatcher are disabled.
|
||||
var logtailDisabled atomic.Bool
|
||||
|
||||
@@ -574,41 +598,41 @@ var debugWakesAndUploads = envknob.RegisterBool("TS_DEBUG_LOGTAIL_WAKES")
|
||||
|
||||
// tryDrainWake tries to send to lg.drainWake, to cause an uploading wakeup.
|
||||
// It does not block.
|
||||
func (l *Logger) tryDrainWake() {
|
||||
l.flushPending.Store(false)
|
||||
func (lg *Logger) tryDrainWake() {
|
||||
lg.flushPending.Store(false)
|
||||
if debugWakesAndUploads() {
|
||||
// Using println instead of log.Printf here to avoid recursing back into
|
||||
// ourselves.
|
||||
println("logtail: try drain wake, numHTTP:", l.httpDoCalls.Load())
|
||||
println("logtail: try drain wake, numHTTP:", lg.httpDoCalls.Load())
|
||||
}
|
||||
select {
|
||||
case l.drainWake <- struct{}{}:
|
||||
case lg.drainWake <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Logger) sendLocked(jsonBlob []byte) (int, error) {
|
||||
func (lg *Logger) sendLocked(jsonBlob []byte) (int, error) {
|
||||
tapSend(jsonBlob)
|
||||
if logtailDisabled.Load() {
|
||||
return len(jsonBlob), nil
|
||||
}
|
||||
|
||||
n, err := l.buffer.Write(jsonBlob)
|
||||
n, err := lg.buffer.Write(jsonBlob)
|
||||
|
||||
flushDelay := defaultFlushDelay
|
||||
if l.flushDelayFn != nil {
|
||||
flushDelay = l.flushDelayFn()
|
||||
if lg.flushDelayFn != nil {
|
||||
flushDelay = lg.flushDelayFn()
|
||||
}
|
||||
if flushDelay > 0 {
|
||||
if l.flushPending.CompareAndSwap(false, true) {
|
||||
if l.flushTimer == nil {
|
||||
l.flushTimer = l.clock.AfterFunc(flushDelay, l.tryDrainWake)
|
||||
if lg.flushPending.CompareAndSwap(false, true) {
|
||||
if lg.flushTimer == nil {
|
||||
lg.flushTimer = lg.clock.AfterFunc(flushDelay, lg.tryDrainWake)
|
||||
} else {
|
||||
l.flushTimer.Reset(flushDelay)
|
||||
lg.flushTimer.Reset(flushDelay)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
l.tryDrainWake()
|
||||
lg.tryDrainWake()
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
@@ -616,13 +640,13 @@ func (l *Logger) sendLocked(jsonBlob []byte) (int, error) {
|
||||
// appendMetadata appends optional "logtail", "metrics", and "v" JSON members.
|
||||
// This assumes dst is already within a JSON object.
|
||||
// Each member is comma-terminated.
|
||||
func (l *Logger) appendMetadata(dst []byte, skipClientTime, skipMetrics bool, procID uint32, procSequence uint64, errDetail string, errData jsontext.Value, level int) []byte {
|
||||
func (lg *Logger) appendMetadata(dst []byte, skipClientTime, skipMetrics bool, procID uint32, procSequence uint64, errDetail string, errData jsontext.Value, level int) []byte {
|
||||
// Append optional logtail metadata.
|
||||
if !skipClientTime || procID != 0 || procSequence != 0 || errDetail != "" || errData != nil {
|
||||
dst = append(dst, `"logtail":{`...)
|
||||
if !skipClientTime {
|
||||
dst = append(dst, `"client_time":"`...)
|
||||
dst = l.clock.Now().UTC().AppendFormat(dst, time.RFC3339Nano)
|
||||
dst = lg.clock.Now().UTC().AppendFormat(dst, time.RFC3339Nano)
|
||||
dst = append(dst, '"', ',')
|
||||
}
|
||||
if procID != 0 {
|
||||
@@ -655,8 +679,8 @@ func (l *Logger) appendMetadata(dst []byte, skipClientTime, skipMetrics bool, pr
|
||||
}
|
||||
|
||||
// Append optional metrics metadata.
|
||||
if !skipMetrics && l.metricsDelta != nil {
|
||||
if d := l.metricsDelta(); d != "" {
|
||||
if !skipMetrics && lg.metricsDelta != nil {
|
||||
if d := lg.metricsDelta(); d != "" {
|
||||
dst = append(dst, `"metrics":"`...)
|
||||
dst = append(dst, d...)
|
||||
dst = append(dst, '"', ',')
|
||||
@@ -676,10 +700,10 @@ func (l *Logger) appendMetadata(dst []byte, skipClientTime, skipMetrics bool, pr
|
||||
}
|
||||
|
||||
// appendText appends a raw text message in the Tailscale JSON log entry format.
|
||||
func (l *Logger) appendText(dst, src []byte, skipClientTime bool, procID uint32, procSequence uint64, level int) []byte {
|
||||
func (lg *Logger) appendText(dst, src []byte, skipClientTime bool, procID uint32, procSequence uint64, level int) []byte {
|
||||
dst = slices.Grow(dst, len(src))
|
||||
dst = append(dst, '{')
|
||||
dst = l.appendMetadata(dst, skipClientTime, false, procID, procSequence, "", nil, level)
|
||||
dst = lg.appendMetadata(dst, skipClientTime, false, procID, procSequence, "", nil, level)
|
||||
if len(src) == 0 {
|
||||
dst = bytes.TrimRight(dst, ",")
|
||||
return append(dst, "}\n"...)
|
||||
@@ -688,7 +712,7 @@ func (l *Logger) appendText(dst, src []byte, skipClientTime bool, procID uint32,
|
||||
// Append the text string, which may be truncated.
|
||||
// Invalid UTF-8 will be mangled with the Unicode replacement character.
|
||||
max := maxTextSize
|
||||
if l.lowMem {
|
||||
if lg.lowMem {
|
||||
max /= lowMemRatio
|
||||
}
|
||||
dst = append(dst, `"text":`...)
|
||||
@@ -711,19 +735,14 @@ func appendTruncatedString(dst, src []byte, n int) []byte {
|
||||
return dst
|
||||
}
|
||||
|
||||
func (l *Logger) AppendTextOrJSONLocked(dst, src []byte) []byte {
|
||||
l.clock = tstime.StdClock{}
|
||||
return l.appendTextOrJSONLocked(dst, src, 0)
|
||||
}
|
||||
|
||||
// appendTextOrJSONLocked appends a raw text message or a raw JSON object
|
||||
// in the Tailscale JSON log format.
|
||||
func (l *Logger) appendTextOrJSONLocked(dst, src []byte, level int) []byte {
|
||||
if l.includeProcSequence {
|
||||
l.procSequence++
|
||||
func (lg *Logger) appendTextOrJSONLocked(dst, src []byte, level int) []byte {
|
||||
if lg.includeProcSequence {
|
||||
lg.procSequence++
|
||||
}
|
||||
if len(src) == 0 || src[0] != '{' {
|
||||
return l.appendText(dst, src, l.skipClientTime, l.procID, l.procSequence, level)
|
||||
return lg.appendText(dst, src, lg.skipClientTime, lg.procID, lg.procSequence, level)
|
||||
}
|
||||
|
||||
// Check whether the input is a valid JSON object and
|
||||
@@ -735,11 +754,11 @@ func (l *Logger) appendTextOrJSONLocked(dst, src []byte, level int) []byte {
|
||||
// However, bytes.NewBuffer normally allocates unless
|
||||
// we immediately shallow copy it into a pre-allocated Buffer struct.
|
||||
// See https://go.dev/issue/67004.
|
||||
l.bytesBuf = *bytes.NewBuffer(src)
|
||||
defer func() { l.bytesBuf = bytes.Buffer{} }() // avoid pinning src
|
||||
lg.bytesBuf = *bytes.NewBuffer(src)
|
||||
defer func() { lg.bytesBuf = bytes.Buffer{} }() // avoid pinning src
|
||||
|
||||
dec := &l.jsonDec
|
||||
dec.Reset(&l.bytesBuf)
|
||||
dec := &lg.jsonDec
|
||||
dec.Reset(&lg.bytesBuf)
|
||||
if tok, err := dec.ReadToken(); tok.Kind() != '{' || err != nil {
|
||||
return false
|
||||
}
|
||||
@@ -771,7 +790,7 @@ func (l *Logger) appendTextOrJSONLocked(dst, src []byte, level int) []byte {
|
||||
|
||||
// Treat invalid JSON as a raw text message.
|
||||
if !validJSON {
|
||||
return l.appendText(dst, src, l.skipClientTime, l.procID, l.procSequence, level)
|
||||
return lg.appendText(dst, src, lg.skipClientTime, lg.procID, lg.procSequence, level)
|
||||
}
|
||||
|
||||
// Check whether the JSON payload is too large.
|
||||
@@ -779,13 +798,13 @@ func (l *Logger) appendTextOrJSONLocked(dst, src []byte, level int) []byte {
|
||||
// That's okay as the Tailscale log service limit is actually 2*maxSize.
|
||||
// However, so long as logging applications aim to target the maxSize limit,
|
||||
// there should be no trouble eventually uploading logs.
|
||||
maxLen := cmp.Or(l.maxUploadSize, maxSize)
|
||||
maxLen := cmp.Or(lg.maxUploadSize, maxSize)
|
||||
if len(src) > maxLen {
|
||||
errDetail := fmt.Sprintf("entry too large: %d bytes", len(src))
|
||||
errData := appendTruncatedString(nil, src, maxLen/len(`\uffff`)) // escaping could increase size
|
||||
|
||||
dst = append(dst, '{')
|
||||
dst = l.appendMetadata(dst, l.skipClientTime, true, l.procID, l.procSequence, errDetail, errData, level)
|
||||
dst = lg.appendMetadata(dst, lg.skipClientTime, true, lg.procID, lg.procSequence, errDetail, errData, level)
|
||||
dst = bytes.TrimRight(dst, ",")
|
||||
return append(dst, "}\n"...)
|
||||
}
|
||||
@@ -802,7 +821,7 @@ func (l *Logger) appendTextOrJSONLocked(dst, src []byte, level int) []byte {
|
||||
}
|
||||
dst = slices.Grow(dst, len(src))
|
||||
dst = append(dst, '{')
|
||||
dst = l.appendMetadata(dst, l.skipClientTime, true, l.procID, l.procSequence, errDetail, errData, level)
|
||||
dst = lg.appendMetadata(dst, lg.skipClientTime, true, lg.procID, lg.procSequence, errDetail, errData, level)
|
||||
if logtailValLength > 0 {
|
||||
// Exclude original logtail member from the message.
|
||||
dst = appendWithoutNewline(dst, src[len("{"):logtailKeyOffset])
|
||||
@@ -829,82 +848,42 @@ func appendWithoutNewline(dst, src []byte) []byte {
|
||||
}
|
||||
|
||||
// Logf logs to l using the provided fmt-style format and optional arguments.
|
||||
func (l *Logger) Logf(format string, args ...any) {
|
||||
fmt.Fprintf(l, format, args...)
|
||||
func (lg *Logger) Logf(format string, args ...any) {
|
||||
fmt.Fprintf(lg, format, args...)
|
||||
}
|
||||
|
||||
var obscureIPs = envknob.RegisterBool("TS_OBSCURE_LOGGED_IPS")
|
||||
|
||||
// Write logs an encoded JSON blob.
|
||||
//
|
||||
// If the []byte passed to Write is not an encoded JSON blob,
|
||||
// then contents is fit into a JSON blob and written.
|
||||
//
|
||||
// This is intended as an interface for the stdlib "log" package.
|
||||
func (l *Logger) Write(buf []byte) (int, error) {
|
||||
func (lg *Logger) Write(buf []byte) (int, error) {
|
||||
if len(buf) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
inLen := len(buf) // length as provided to us, before modifications to downstream writers
|
||||
|
||||
level, buf := parseAndRemoveLogLevel(buf)
|
||||
if l.stderr != nil && l.stderr != io.Discard && int64(level) <= atomic.LoadInt64(&l.stderrLevel) {
|
||||
if lg.stderr != nil && lg.stderr != io.Discard && int64(level) <= atomic.LoadInt64(&lg.stderrLevel) {
|
||||
if buf[len(buf)-1] == '\n' {
|
||||
l.stderr.Write(buf)
|
||||
lg.stderr.Write(buf)
|
||||
} else {
|
||||
// The log package always line-terminates logs,
|
||||
// so this is an uncommon path.
|
||||
withNL := append(buf[:len(buf):len(buf)], '\n')
|
||||
l.stderr.Write(withNL)
|
||||
lg.stderr.Write(withNL)
|
||||
}
|
||||
}
|
||||
|
||||
if obscureIPs() {
|
||||
buf = redactIPs(buf)
|
||||
}
|
||||
lg.writeLock.Lock()
|
||||
defer lg.writeLock.Unlock()
|
||||
|
||||
l.writeLock.Lock()
|
||||
defer l.writeLock.Unlock()
|
||||
|
||||
b := l.appendTextOrJSONLocked(l.writeBuf[:0], buf, level)
|
||||
_, err := l.sendLocked(b)
|
||||
b := lg.appendTextOrJSONLocked(lg.writeBuf[:0], buf, level)
|
||||
_, err := lg.sendLocked(b)
|
||||
return inLen, err
|
||||
}
|
||||
|
||||
var (
|
||||
regexMatchesIPv6 = regexp.MustCompile(`([0-9a-fA-F]{1,4}):([0-9a-fA-F]{1,4}):([0-9a-fA-F:]{1,4})*`)
|
||||
regexMatchesIPv4 = regexp.MustCompile(`(\d{1,3})\.(\d{1,3})\.\d{1,3}\.\d{1,3}`)
|
||||
)
|
||||
|
||||
// redactIPs is a helper function used in Write() to redact IPs (other than tailscale IPs).
|
||||
// This function takes a log line as a byte slice and
|
||||
// uses regex matching to parse and find IP addresses. Based on if the IP address is IPv4 or
|
||||
// IPv6, it parses and replaces the end of the addresses with an "x". This function returns the
|
||||
// log line with the IPs redacted.
|
||||
func redactIPs(buf []byte) []byte {
|
||||
out := regexMatchesIPv6.ReplaceAllFunc(buf, func(b []byte) []byte {
|
||||
ip, err := netip.ParseAddr(string(b))
|
||||
if err != nil || tsaddr.IsTailscaleIP(ip) {
|
||||
return b // don't change this one
|
||||
}
|
||||
|
||||
prefix := bytes.Split(b, []byte(":"))
|
||||
return bytes.Join(append(prefix[:2], []byte("x")), []byte(":"))
|
||||
})
|
||||
|
||||
out = regexMatchesIPv4.ReplaceAllFunc(out, func(b []byte) []byte {
|
||||
ip, err := netip.ParseAddr(string(b))
|
||||
if err != nil || tsaddr.IsTailscaleIP(ip) {
|
||||
return b // don't change this one
|
||||
}
|
||||
|
||||
prefix := bytes.Split(b, []byte("."))
|
||||
return bytes.Join(append(prefix[:2], []byte("x.x")), []byte("."))
|
||||
})
|
||||
|
||||
return []byte(out)
|
||||
}
|
||||
|
||||
var (
|
||||
openBracketV = []byte("[v")
|
||||
v1 = []byte("[v1] ")
|
||||
|
||||
44
vendor/tailscale.com/logtail/logtail_omit.go
generated
vendored
Normal file
44
vendor/tailscale.com/logtail/logtail_omit.go
generated
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build ts_omit_logtail
|
||||
|
||||
package logtail
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
tslogger "tailscale.com/types/logger"
|
||||
"tailscale.com/types/logid"
|
||||
)
|
||||
|
||||
// Noop implementations of everything when ts_omit_logtail is set.
|
||||
|
||||
type Logger struct{}
|
||||
|
||||
type Buffer any
|
||||
|
||||
func Disable() {}
|
||||
|
||||
func NewLogger(cfg Config, logf tslogger.Logf) *Logger {
|
||||
return &Logger{}
|
||||
}
|
||||
|
||||
func (*Logger) Write(p []byte) (n int, err error) {
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (*Logger) Logf(format string, args ...any) {}
|
||||
func (*Logger) Shutdown(ctx context.Context) error { return nil }
|
||||
func (*Logger) SetVerbosityLevel(level int) {}
|
||||
|
||||
func (l *Logger) SetSockstatsLabel(label any) {}
|
||||
|
||||
func (l *Logger) PrivateID() logid.PrivateID { return logid.PrivateID{} }
|
||||
func (l *Logger) StartFlush() {}
|
||||
|
||||
func RegisterLogTap(dst chan<- string) (unregister func()) {
|
||||
return func() {}
|
||||
}
|
||||
|
||||
func (*Logger) SetNetMon(any) {}
|
||||
Reference in New Issue
Block a user