cloudflared/management/logger.go
Devin Carr 55ed995bf0 TUN-7127: Disconnect logger level requirement for management
By default, we want streaming logs to be able to stream debug logs
from cloudflared without needing to update the remote cloudflared's
configuration. This disconnects the provided local log level sent
to console, file, etc. from the level that management tunnel will
utilize via requested filters.
2023-04-06 11:31:47 -07:00

143 lines
3.5 KiB
Go

package management
import (
"os"
"sync"
"time"
jsoniter "github.com/json-iterator/go"
"github.com/rs/zerolog"
)
var json = jsoniter.ConfigFastest
const (
// Indicates how many log messages the listener will hold before dropping.
// Provides a throttling mechanism to drop latest messages if the sender
// can't keep up with the influx of log messages.
logWindow = 30
)
// ZeroLogEvent is the json structure that zerolog stores it's events as
type ZeroLogEvent struct {
Time string `json:"time,omitempty"`
Level LogLevel `json:"level,omitempty"`
Type LogEventType `json:"type,omitempty"`
Message string `json:"message,omitempty"`
}
// Logger manages the number of management streaming log sessions
type Logger struct {
sessions []*Session
mu sync.RWMutex
// Unique logger that isn't a io.Writer of the list of zerolog writers. This helps prevent management log
// statements from creating infinite recursion to export messages to a session and allows basic debugging and
// error statements to be issued in the management code itself.
Log *zerolog.Logger
}
func NewLogger() *Logger {
log := zerolog.New(zerolog.ConsoleWriter{
Out: os.Stdout,
TimeFormat: time.RFC3339,
}).With().Timestamp().Logger().Level(zerolog.InfoLevel)
return &Logger{
Log: &log,
}
}
type LoggerListener interface {
Listen() *Session
Close(*Session)
}
type Session struct {
// Buffered channel that holds the recent log events
listener chan *ZeroLogEvent
// Types of log events that this session will provide through the listener
filters []LogEventType
}
func newListener(size int) *Session {
return &Session{
listener: make(chan *ZeroLogEvent, size),
filters: []LogEventType{},
}
}
// Listen creates a new Session that will append filtered log events as they are created.
func (l *Logger) Listen() *Session {
l.mu.Lock()
defer l.mu.Unlock()
listener := newListener(logWindow)
l.sessions = append(l.sessions, listener)
return listener
}
// Close will remove a Session from the available sessions that were receiving log events.
func (l *Logger) Close(session *Session) {
l.mu.Lock()
defer l.mu.Unlock()
index := -1
for i, v := range l.sessions {
if v == session {
index = i
break
}
}
if index == -1 {
// Not found
return
}
copy(l.sessions[index:], l.sessions[index+1:])
l.sessions = l.sessions[:len(l.sessions)-1]
}
// Write will write the log event to all sessions that have available capacity. For those that are full, the message
// will be dropped.
// This function is the interface that zerolog expects to call when a log event is to be written out.
func (l *Logger) Write(p []byte) (int, error) {
l.mu.RLock()
defer l.mu.RUnlock()
// return early if no active sessions
if len(l.sessions) == 0 {
return len(p), nil
}
var event ZeroLogEvent
iter := json.BorrowIterator(p)
defer json.ReturnIterator(iter)
iter.ReadVal(&event)
if iter.Error != nil {
l.Log.Debug().Msg("unable to unmarshal log event")
return len(p), nil
}
for _, listener := range l.sessions {
// no filters means all types are allowed
if len(listener.filters) != 0 {
valid := false
// make sure listener is subscribed to this event type
for _, t := range listener.filters {
if t == event.Type {
valid = true
break
}
}
if !valid {
continue
}
}
select {
case listener.listener <- &event:
default:
// buffer is full, discard
}
}
return len(p), nil
}
func (l *Logger) WriteLevel(level zerolog.Level, p []byte) (n int, err error) {
return l.Write(p)
}