mirror of
https://github.com/cloudflare/cloudflared.git
synced 2025-05-10 18:56:34 +00:00

To help accommodate web browser interactions with websockets, when a streaming logs session is requested for the same actor while already serving a session for that user in a separate request, the original request will be closed and the new request start streaming logs instead. This should help with rogue sessions holding on for too long with no client on the other side (before idle timeout or connection close).
178 lines
4.3 KiB
Go
178 lines
4.3 KiB
Go
package management
|
|
|
|
import (
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
|
|
jsoniter "github.com/json-iterator/go"
|
|
"github.com/rs/zerolog"
|
|
)
|
|
|
|
var json = jsoniter.ConfigFastest
|
|
|
|
// 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 {
|
|
// ActiveSession returns the first active session for the requested actor.
|
|
ActiveSession(actor) *session
|
|
// ActiveSession returns the count of active sessions.
|
|
ActiveSessions() int
|
|
// Listen appends the session to the list of sessions that receive log events.
|
|
Listen(*session)
|
|
// Remove a session from the available sessions that were receiving log events.
|
|
Remove(*session)
|
|
}
|
|
|
|
func (l *Logger) ActiveSession(actor actor) *session {
|
|
l.mu.RLock()
|
|
defer l.mu.RUnlock()
|
|
for _, session := range l.sessions {
|
|
if session.actor.ID == actor.ID && session.active.Load() {
|
|
return session
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (l *Logger) ActiveSessions() int {
|
|
l.mu.RLock()
|
|
defer l.mu.RUnlock()
|
|
count := 0
|
|
for _, session := range l.sessions {
|
|
if session.active.Load() {
|
|
count += 1
|
|
}
|
|
}
|
|
return count
|
|
}
|
|
|
|
func (l *Logger) Listen(session *session) {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
session.active.Store(true)
|
|
l.sessions = append(l.sessions, session)
|
|
}
|
|
|
|
func (l *Logger) Remove(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
|
|
}
|
|
event, err := parseZerologEvent(p)
|
|
// drop event if unable to parse properly
|
|
if err != nil {
|
|
l.Log.Debug().Msg("unable to parse log event")
|
|
return len(p), nil
|
|
}
|
|
for _, session := range l.sessions {
|
|
session.Insert(event)
|
|
}
|
|
return len(p), nil
|
|
}
|
|
|
|
func (l *Logger) WriteLevel(level zerolog.Level, p []byte) (n int, err error) {
|
|
return l.Write(p)
|
|
}
|
|
|
|
func parseZerologEvent(p []byte) (*Log, error) {
|
|
var fields map[string]interface{}
|
|
iter := json.BorrowIterator(p)
|
|
defer json.ReturnIterator(iter)
|
|
iter.ReadVal(&fields)
|
|
if iter.Error != nil {
|
|
return nil, iter.Error
|
|
}
|
|
logTime := time.Now().UTC().Format(zerolog.TimeFieldFormat)
|
|
if t, ok := fields[TimeKey]; ok {
|
|
if t, ok := t.(string); ok {
|
|
logTime = t
|
|
}
|
|
}
|
|
logLevel := Debug
|
|
// A zerolog Debug event can be created and then an error can be added
|
|
// via .Err(error), if so, we upgrade the level to error.
|
|
if _, hasError := fields["error"]; hasError {
|
|
logLevel = Error
|
|
} else {
|
|
if level, ok := fields[LevelKey]; ok {
|
|
if level, ok := level.(string); ok {
|
|
if logLevel, ok = ParseLogLevel(level); !ok {
|
|
logLevel = Debug
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Assume the event type is Cloudflared if unable to parse/find. This could be from log events that haven't
|
|
// yet been tagged with the appropriate EventType yet.
|
|
logEvent := Cloudflared
|
|
e := fields[EventTypeKey]
|
|
if e != nil {
|
|
if eventNumber, ok := e.(float64); ok {
|
|
logEvent = LogEventType(eventNumber)
|
|
}
|
|
}
|
|
logMessage := ""
|
|
if m, ok := fields[MessageKey]; ok {
|
|
if m, ok := m.(string); ok {
|
|
logMessage = m
|
|
}
|
|
}
|
|
event := Log{
|
|
Time: logTime,
|
|
Level: logLevel,
|
|
Event: logEvent,
|
|
Message: logMessage,
|
|
}
|
|
// Remove the keys that have top level keys on Log
|
|
delete(fields, TimeKey)
|
|
delete(fields, LevelKey)
|
|
delete(fields, EventTypeKey)
|
|
delete(fields, MessageKey)
|
|
// The rest of the keys go into the Fields
|
|
event.Fields = fields
|
|
return &event, nil
|
|
}
|