Skip to content

Commit 3fd5710

Browse files
committed
TUN-7128: Categorize logs from public hostname locations
Updates the HTTP ingress request log events to have more structured fields to adapt to streaming logs reporting.
1 parent 5d0bb25 commit 3fd5710

File tree

7 files changed

+199
-79
lines changed

7 files changed

+199
-79
lines changed

cmd/cloudflared/tail/cmd.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,12 @@ func Run(c *cli.Context) error {
187187
}
188188
// Output all the logs received to stdout
189189
for _, l := range logs.Logs {
190-
fmt.Printf("%s %s %s %s\n", l.Timestamp, l.Level, l.Event, l.Message)
190+
fields, err := json.Marshal(l.Fields)
191+
if err != nil {
192+
fields = []byte("unable to parse fields")
193+
log.Debug().Msgf("unable to parse fields from event %+v", l)
194+
}
195+
fmt.Printf("%s %s %s %s %s\n", l.Time, l.Level, l.Event, l.Message, fields)
191196
}
192197
case management.UnknownServerEventType:
193198
fallthrough

management/events.go

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ type EventStopStreaming struct {
5959
// EventLog is the event that the server sends to the client with the log events.
6060
type EventLog struct {
6161
ServerEvent
62-
Logs []Log `json:"logs"`
62+
Logs []*Log `json:"logs"`
6363
}
6464

6565
// LogEventType is the way that logging messages are able to be filtered.
@@ -68,10 +68,12 @@ type EventLog struct {
6868
type LogEventType int
6969

7070
const (
71-
Cloudflared LogEventType = 0
72-
HTTP LogEventType = 1
73-
TCP LogEventType = 2
74-
UDP LogEventType = 3
71+
// Cloudflared events are signficant to cloudflared operations like connection state changes.
72+
// Cloudflared is also the default event type for any events that haven't been separated into a proper event type.
73+
Cloudflared LogEventType = iota
74+
HTTP
75+
TCP
76+
UDP
7577
)
7678

7779
func (l LogEventType) String() string {
@@ -101,12 +103,26 @@ const (
101103
Error LogLevel = "error"
102104
)
103105

106+
const (
107+
// TimeKey aligns with the zerolog.TimeFieldName
108+
TimeKey = "time"
109+
// LevelKey aligns with the zerolog.LevelFieldName
110+
LevelKey = "level"
111+
// LevelKey aligns with the zerolog.MessageFieldName
112+
MessageKey = "message"
113+
// EventTypeKey is the custom JSON key of the LogEventType in ZeroLogEvent
114+
EventTypeKey = "event"
115+
// FieldsKey is a custom JSON key to match and store every other key for a zerolog event
116+
FieldsKey = "fields"
117+
)
118+
104119
// Log is the basic structure of the events that are sent to the client.
105120
type Log struct {
106-
Event LogEventType `json:"event"`
107-
Timestamp string `json:"timestamp"`
108-
Level LogLevel `json:"level"`
109-
Message string `json:"message"`
121+
Time string `json:"time,omitempty"`
122+
Level LogLevel `json:"level,omitempty"`
123+
Message string `json:"message,omitempty"`
124+
Event LogEventType `json:"event,omitempty"`
125+
Fields map[string]interface{} `json:"fields,omitempty"`
110126
}
111127

112128
// IntoClientEvent unmarshals the provided ClientEvent into the proper type.

management/events_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,12 @@ func TestIntoServerEvent_Invalid(t *testing.T) {
6262
func TestReadServerEvent(t *testing.T) {
6363
sentEvent := EventLog{
6464
ServerEvent: ServerEvent{Type: Logs},
65-
Logs: []Log{
65+
Logs: []*Log{
6666
{
67-
Timestamp: time.Now().UTC().Format(time.RFC3339),
68-
Event: HTTP,
69-
Level: Info,
70-
Message: "test",
67+
Time: time.Now().UTC().Format(time.RFC3339),
68+
Event: HTTP,
69+
Level: Info,
70+
Message: "test",
7171
},
7272
},
7373
}

management/logger.go

Lines changed: 59 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,6 @@ const (
1818
logWindow = 30
1919
)
2020

21-
// ZeroLogEvent is the json structure that zerolog stores it's events as
22-
type ZeroLogEvent struct {
23-
Time string `json:"time,omitempty"`
24-
Level LogLevel `json:"level,omitempty"`
25-
Type LogEventType `json:"type,omitempty"`
26-
Message string `json:"message,omitempty"`
27-
}
28-
2921
// Logger manages the number of management streaming log sessions
3022
type Logger struct {
3123
sessions []*Session
@@ -54,14 +46,14 @@ type LoggerListener interface {
5446

5547
type Session struct {
5648
// Buffered channel that holds the recent log events
57-
listener chan *ZeroLogEvent
49+
listener chan *Log
5850
// Types of log events that this session will provide through the listener
5951
filters []LogEventType
6052
}
6153

6254
func newListener(size int) *Session {
6355
return &Session{
64-
listener: make(chan *ZeroLogEvent, size),
56+
listener: make(chan *Log, size),
6557
filters: []LogEventType{},
6658
}
6759
}
@@ -104,12 +96,10 @@ func (l *Logger) Write(p []byte) (int, error) {
10496
if len(l.sessions) == 0 {
10597
return len(p), nil
10698
}
107-
var event ZeroLogEvent
108-
iter := json.BorrowIterator(p)
109-
defer json.ReturnIterator(iter)
110-
iter.ReadVal(&event)
111-
if iter.Error != nil {
112-
l.Log.Debug().Msg("unable to unmarshal log event")
99+
event, err := parseZerologEvent(p)
100+
// drop event if unable to parse properly
101+
if err != nil {
102+
l.Log.Debug().Msg("unable to parse log event")
113103
return len(p), nil
114104
}
115105
for _, listener := range l.sessions {
@@ -118,7 +108,7 @@ func (l *Logger) Write(p []byte) (int, error) {
118108
valid := false
119109
// make sure listener is subscribed to this event type
120110
for _, t := range listener.filters {
121-
if t == event.Type {
111+
if t == event.Event {
122112
valid = true
123113
break
124114
}
@@ -129,7 +119,7 @@ func (l *Logger) Write(p []byte) (int, error) {
129119
}
130120

131121
select {
132-
case listener.listener <- &event:
122+
case listener.listener <- event:
133123
default:
134124
// buffer is full, discard
135125
}
@@ -140,3 +130,54 @@ func (l *Logger) Write(p []byte) (int, error) {
140130
func (l *Logger) WriteLevel(level zerolog.Level, p []byte) (n int, err error) {
141131
return l.Write(p)
142132
}
133+
134+
func parseZerologEvent(p []byte) (*Log, error) {
135+
var fields map[string]interface{}
136+
iter := json.BorrowIterator(p)
137+
defer json.ReturnIterator(iter)
138+
iter.ReadVal(&fields)
139+
if iter.Error != nil {
140+
return nil, iter.Error
141+
}
142+
logTime := time.Now().UTC().Format(zerolog.TimeFieldFormat)
143+
if t, ok := fields[TimeKey]; ok {
144+
if t, ok := t.(string); ok {
145+
logTime = t
146+
}
147+
}
148+
logLevel := Debug
149+
if level, ok := fields[LevelKey]; ok {
150+
if level, ok := level.(string); ok {
151+
logLevel = LogLevel(level)
152+
}
153+
}
154+
// Assume the event type is Cloudflared if unable to parse/find. This could be from log events that haven't
155+
// yet been tagged with the appropriate EventType yet.
156+
logEvent := Cloudflared
157+
e := fields[EventTypeKey]
158+
if e != nil {
159+
if eventNumber, ok := e.(float64); ok {
160+
logEvent = LogEventType(eventNumber)
161+
}
162+
}
163+
logMessage := ""
164+
if m, ok := fields[MessageKey]; ok {
165+
if m, ok := m.(string); ok {
166+
logMessage = m
167+
}
168+
}
169+
event := Log{
170+
Time: logTime,
171+
Level: logLevel,
172+
Event: logEvent,
173+
Message: logMessage,
174+
}
175+
// Remove the keys that have top level keys on Log
176+
delete(fields, TimeKey)
177+
delete(fields, LevelKey)
178+
delete(fields, EventTypeKey)
179+
delete(fields, MessageKey)
180+
// The rest of the keys go into the Fields
181+
event.Fields = fields
182+
return &event, nil
183+
}

management/logger_test.go

Lines changed: 72 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55

66
"github.com/rs/zerolog"
77
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
89
)
910

1011
// No listening sessions will not write to the channel
@@ -22,12 +23,13 @@ func TestLoggerWrite_OneSession(t *testing.T) {
2223

2324
session := logger.Listen()
2425
defer logger.Close(session)
25-
zlog.Info().Int("type", int(HTTP)).Msg("hello")
26+
zlog.Info().Int(EventTypeKey, int(HTTP)).Msg("hello")
2627
select {
2728
case event := <-session.listener:
29+
assert.NotEmpty(t, event.Time)
2830
assert.Equal(t, "hello", event.Message)
29-
assert.Equal(t, LogLevel("info"), event.Level)
30-
assert.Equal(t, HTTP, event.Type)
31+
assert.Equal(t, Info, event.Level)
32+
assert.Equal(t, HTTP, event.Event)
3133
default:
3234
assert.Fail(t, "expected an event to be in the listener")
3335
}
@@ -41,26 +43,28 @@ func TestLoggerWrite_MultipleSessions(t *testing.T) {
4143
session1 := logger.Listen()
4244
defer logger.Close(session1)
4345
session2 := logger.Listen()
44-
zlog.Info().Int("type", int(HTTP)).Msg("hello")
46+
zlog.Info().Int(EventTypeKey, int(HTTP)).Msg("hello")
4547
for _, session := range []*Session{session1, session2} {
4648
select {
4749
case event := <-session.listener:
50+
assert.NotEmpty(t, event.Time)
4851
assert.Equal(t, "hello", event.Message)
49-
assert.Equal(t, LogLevel("info"), event.Level)
50-
assert.Equal(t, HTTP, event.Type)
52+
assert.Equal(t, Info, event.Level)
53+
assert.Equal(t, HTTP, event.Event)
5154
default:
5255
assert.Fail(t, "expected an event to be in the listener")
5356
}
5457
}
5558

5659
// Close session2 and make sure session1 still receives events
5760
logger.Close(session2)
58-
zlog.Info().Int("type", int(HTTP)).Msg("hello2")
61+
zlog.Info().Int(EventTypeKey, int(HTTP)).Msg("hello2")
5962
select {
6063
case event := <-session1.listener:
64+
assert.NotEmpty(t, event.Time)
6165
assert.Equal(t, "hello2", event.Message)
62-
assert.Equal(t, LogLevel("info"), event.Level)
63-
assert.Equal(t, HTTP, event.Type)
66+
assert.Equal(t, Info, event.Level)
67+
assert.Equal(t, HTTP, event.Event)
6468
default:
6569
assert.Fail(t, "expected an event to be in the listener")
6670
}
@@ -73,3 +77,62 @@ func TestLoggerWrite_MultipleSessions(t *testing.T) {
7377
// pass
7478
}
7579
}
80+
81+
type mockWriter struct {
82+
event *Log
83+
err error
84+
}
85+
86+
func (m *mockWriter) Write(p []byte) (int, error) {
87+
m.event, m.err = parseZerologEvent(p)
88+
return len(p), nil
89+
}
90+
91+
// Validate all event types are set properly
92+
func TestParseZerologEvent_EventTypes(t *testing.T) {
93+
writer := mockWriter{}
94+
zlog := zerolog.New(&writer).With().Timestamp().Logger().Level(zerolog.InfoLevel)
95+
96+
for _, test := range []LogEventType{
97+
Cloudflared,
98+
HTTP,
99+
TCP,
100+
UDP,
101+
} {
102+
t.Run(test.String(), func(t *testing.T) {
103+
defer func() { writer.err = nil }()
104+
zlog.Info().Int(EventTypeKey, int(test)).Msg("test")
105+
require.NoError(t, writer.err)
106+
require.Equal(t, test, writer.event.Event)
107+
})
108+
}
109+
110+
// Invalid defaults to Cloudflared LogEventType
111+
t.Run("Invalid", func(t *testing.T) {
112+
defer func() { writer.err = nil }()
113+
zlog.Info().Str(EventTypeKey, "unknown").Msg("test")
114+
require.NoError(t, writer.err)
115+
require.Equal(t, Cloudflared, writer.event.Event)
116+
})
117+
}
118+
119+
// Validate top-level keys are removed from Fields
120+
func TestParseZerologEvent_Fields(t *testing.T) {
121+
writer := mockWriter{}
122+
zlog := zerolog.New(&writer).With().Timestamp().Logger().Level(zerolog.InfoLevel)
123+
zlog.Info().Int(EventTypeKey, int(Cloudflared)).Str("test", "test").Msg("test message")
124+
require.NoError(t, writer.err)
125+
event := writer.event
126+
require.NotEmpty(t, event.Time)
127+
require.Equal(t, Cloudflared, event.Event)
128+
require.Equal(t, Info, event.Level)
129+
require.Equal(t, "test message", event.Message)
130+
131+
// Make sure Fields doesn't have other set keys used in the Log struct
132+
require.NotEmpty(t, event.Fields)
133+
require.Equal(t, "test", event.Fields["test"])
134+
require.NotContains(t, event.Fields, EventTypeKey)
135+
require.NotContains(t, event.Fields, LevelKey)
136+
require.NotContains(t, event.Fields, MessageKey)
137+
require.NotContains(t, event.Fields, TimeKey)
138+
}

management/service.go

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -96,12 +96,7 @@ func (m *ManagementService) streamLogs(c *websocket.Conn, ctx context.Context, s
9696
case event := <-session.listener:
9797
err := WriteEvent(c, ctx, &EventLog{
9898
ServerEvent: ServerEvent{Type: Logs},
99-
Logs: []Log{{
100-
Event: Cloudflared,
101-
Timestamp: event.Time,
102-
Level: event.Level,
103-
Message: event.Message,
104-
}},
99+
Logs: []*Log{event},
105100
})
106101
if err != nil {
107102
// If the client (or the server) already closed the connection, don't attempt to close it again

0 commit comments

Comments
 (0)