Skip to content

Commit a65da54

Browse files
committed
TUN-9371: Add logging format as JSON
Closes TUN-9371
1 parent 43a3ba3 commit a65da54

File tree

7 files changed

+126
-18
lines changed

7 files changed

+126
-18
lines changed

cmd/cloudflared/cliutil/logger.go

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,48 +4,56 @@ import (
44
"github.com/urfave/cli/v2"
55
"github.com/urfave/cli/v2/altsrc"
66

7-
cfdflags "github.com/cloudflare/cloudflared/cmd/cloudflared/flags"
7+
"github.com/cloudflare/cloudflared/cmd/cloudflared/flags"
88
)
99

1010
var (
1111
debugLevelWarning = "At debug level cloudflared will log request URL, method, protocol, content length, as well as, all request and response headers. " +
1212
"This can expose sensitive information in your logs."
13+
14+
FlagLogOutput = &cli.StringFlag{
15+
Name: flags.LogFormatOutput,
16+
Usage: "Output format for the logs (default, json)",
17+
Value: flags.LogFormatOutputValueDefault,
18+
EnvVars: []string{"TUNNEL_MANAGEMENT_OUTPUT", "TUNNEL_LOG_OUTPUT"},
19+
}
1320
)
1421

1522
func ConfigureLoggingFlags(shouldHide bool) []cli.Flag {
1623
return []cli.Flag{
1724
altsrc.NewStringFlag(&cli.StringFlag{
18-
Name: cfdflags.LogLevel,
25+
Name: flags.LogLevel,
1926
Value: "info",
2027
Usage: "Application logging level {debug, info, warn, error, fatal}. " + debugLevelWarning,
2128
EnvVars: []string{"TUNNEL_LOGLEVEL"},
2229
Hidden: shouldHide,
2330
}),
2431
altsrc.NewStringFlag(&cli.StringFlag{
25-
Name: cfdflags.TransportLogLevel,
32+
Name: flags.TransportLogLevel,
2633
Aliases: []string{"proto-loglevel"}, // This flag used to be called proto-loglevel
2734
Value: "info",
2835
Usage: "Transport logging level(previously called protocol logging level) {debug, info, warn, error, fatal}",
2936
EnvVars: []string{"TUNNEL_PROTO_LOGLEVEL", "TUNNEL_TRANSPORT_LOGLEVEL"},
3037
Hidden: shouldHide,
3138
}),
3239
altsrc.NewStringFlag(&cli.StringFlag{
33-
Name: cfdflags.LogFile,
40+
Name: flags.LogFile,
3441
Usage: "Save application log to this file for reporting issues.",
3542
EnvVars: []string{"TUNNEL_LOGFILE"},
3643
Hidden: shouldHide,
3744
}),
3845
altsrc.NewStringFlag(&cli.StringFlag{
39-
Name: cfdflags.LogDirectory,
46+
Name: flags.LogDirectory,
4047
Usage: "Save application log to this directory for reporting issues.",
4148
EnvVars: []string{"TUNNEL_LOGDIRECTORY"},
4249
Hidden: shouldHide,
4350
}),
4451
altsrc.NewStringFlag(&cli.StringFlag{
45-
Name: cfdflags.TraceOutput,
52+
Name: flags.TraceOutput,
4653
Usage: "Name of trace output file, generated when cloudflared stops.",
4754
EnvVars: []string{"TUNNEL_TRACE_OUTPUT"},
4855
Hidden: shouldHide,
4956
}),
57+
FlagLogOutput,
5058
}
5159
}

cmd/cloudflared/flags/flags.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,11 @@ const (
138138
// LogDirectory is the command line flag to define the directory where application logs will be stored.
139139
LogDirectory = "log-directory"
140140

141+
// LogFormatOutput allows the command line logs to be output as JSON.
142+
LogFormatOutput = "output"
143+
LogFormatOutputValueDefault = "default"
144+
LogFormatOutputValueJSON = "json"
145+
141146
// TraceOutput is the command line flag to set the name of trace output file
142147
TraceOutput = "trace-output"
143148

cmd/cloudflared/tail/cmd.go

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"encoding/json"
55
"errors"
66
"fmt"
7+
"io"
78
"net/http"
89
"net/url"
910
"os"
@@ -97,12 +98,6 @@ func buildTailCommand(subcommands []*cli.Command) *cli.Command {
9798
Value: "",
9899
EnvVars: []string{"TUNNEL_MANAGEMENT_TOKEN"},
99100
},
100-
&cli.StringFlag{
101-
Name: "output",
102-
Usage: "Output format for the logs (default, json)",
103-
Value: "default",
104-
EnvVars: []string{"TUNNEL_MANAGEMENT_OUTPUT"},
105-
},
106101
&cli.StringFlag{
107102
Name: "management-hostname",
108103
Usage: "Management hostname to signify incoming management requests",
@@ -128,6 +123,7 @@ func buildTailCommand(subcommands []*cli.Command) *cli.Command {
128123
EnvVars: []string{"TUNNEL_ORIGIN_CERT"},
129124
Value: credentials.FindDefaultOriginCertPath(),
130125
},
126+
cliutil.FlagLogOutput,
131127
},
132128
Subcommands: subcommands,
133129
}
@@ -171,10 +167,21 @@ func createLogger(c *cli.Context) *zerolog.Logger {
171167
if levelErr != nil {
172168
level = zerolog.InfoLevel
173169
}
174-
log := zerolog.New(zerolog.ConsoleWriter{
175-
Out: colorable.NewColorable(os.Stderr),
176-
TimeFormat: time.RFC3339,
177-
}).With().Timestamp().Logger().Level(level)
170+
var writer io.Writer
171+
switch c.String(cfdflags.LogFormatOutput) {
172+
case cfdflags.LogFormatOutputValueJSON:
173+
// zerolog by default outputs as JSON
174+
writer = os.Stderr
175+
case cfdflags.LogFormatOutputValueDefault:
176+
// "default" and unset use the same logger output format
177+
fallthrough
178+
default:
179+
writer = zerolog.ConsoleWriter{
180+
Out: colorable.NewColorable(os.Stderr),
181+
TimeFormat: time.RFC3339,
182+
}
183+
}
184+
log := zerolog.New(writer).With().Timestamp().Logger().Level(level)
178185
return &log
179186
}
180187

logger/configuration.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ type Config struct {
1717

1818
type ConsoleConfig struct {
1919
noColor bool
20+
asJSON bool
2021
}
2122

2223
type FileConfig struct {
@@ -48,6 +49,7 @@ func createDefaultConfig() Config {
4849
return Config{
4950
ConsoleConfig: &ConsoleConfig{
5051
noColor: false,
52+
asJSON: false,
5153
},
5254
FileConfig: &FileConfig{
5355
Dirname: "",
@@ -67,11 +69,12 @@ func createDefaultConfig() Config {
6769
func CreateConfig(
6870
minLevel string,
6971
disableTerminal bool,
72+
formatJSON bool,
7073
rollingLogPath, nonRollingLogFilePath string,
7174
) *Config {
7275
var console *ConsoleConfig
7376
if !disableTerminal {
74-
console = createConsoleConfig()
77+
console = createConsoleConfig(formatJSON)
7578
}
7679

7780
var file *FileConfig
@@ -95,9 +98,10 @@ func CreateConfig(
9598
}
9699
}
97100

98-
func createConsoleConfig() *ConsoleConfig {
101+
func createConsoleConfig(formatJSON bool) *ConsoleConfig {
99102
return &ConsoleConfig{
100103
noColor: false,
104+
asJSON: formatJSON,
101105
}
102106
}
103107

logger/console.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package logger
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"io"
7+
8+
jsoniter "github.com/json-iterator/go"
9+
)
10+
11+
var json = jsoniter.ConfigCompatibleWithStandardLibrary
12+
13+
// consoleWriter allows us the simplicity to prevent duplicate json keys in the logger events reported.
14+
//
15+
// By default zerolog constructs the json event in parts by appending each additional key after the first. It
16+
// doesn't have any internal state or struct of the json message representation so duplicate keys can be
17+
// inserted without notice and no pruning will occur before writing the log event out to the io.Writer.
18+
//
19+
// To help prevent these duplicate keys, we will decode the json log event and then immediately re-encode it
20+
// again as writing it to the output io.Writer. Since we encode it to a map[string]any, duplicate keys
21+
// are pruned. We pay the cost of decoding and encoding the log event for each time, but helps prevent
22+
// us from needing to worry about adding duplicate keys in the log event from different areas of code.
23+
type consoleWriter struct {
24+
out io.Writer
25+
}
26+
27+
func (c *consoleWriter) Write(p []byte) (n int, err error) {
28+
var evt map[string]any
29+
d := json.NewDecoder(bytes.NewReader(p))
30+
d.UseNumber()
31+
err = d.Decode(&evt)
32+
if err != nil {
33+
return n, fmt.Errorf("cannot decode event: %s", err)
34+
}
35+
e := json.NewEncoder(c.out)
36+
return len(p), e.Encode(evt)
37+
}

logger/console_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package logger
2+
3+
import (
4+
"bytes"
5+
"strings"
6+
"testing"
7+
8+
"github.com/rs/zerolog"
9+
)
10+
11+
func TestConsoleLoggerDuplicateKeys(t *testing.T) {
12+
r := bytes.NewBuffer(make([]byte, 500))
13+
logger := zerolog.New(&consoleWriter{out: r}).With().Timestamp().Logger()
14+
logger.Debug().Str("test", "1234").Int("number", 45).Str("test", "5678").Msg("log message")
15+
16+
event, err := r.ReadString('\n')
17+
if err != nil {
18+
t.Error(err)
19+
}
20+
21+
if !strings.Contains(event, "\"test\":\"5678\"") {
22+
t.Errorf("log event missing key 'test': %s", event)
23+
}
24+
if !strings.Contains(event, "\"number\":45") {
25+
t.Errorf("log event missing key 'number': %s", event)
26+
}
27+
if !strings.Contains(event, "\"time\":") {
28+
t.Errorf("log event missing key 'time': %s", event)
29+
}
30+
if !strings.Contains(event, "\"level\":\"debug\"") {
31+
t.Errorf("log event missing key 'level': %s", event)
32+
}
33+
}

logger/create.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,10 +149,21 @@ func createFromContext(
149149
logLevel := c.String(logLevelFlagName)
150150
logFile := c.String(cfdflags.LogFile)
151151
logDirectory := c.String(logDirectoryFlagName)
152+
var logFormatJSON bool
153+
switch c.String(cfdflags.LogFormatOutput) {
154+
case cfdflags.LogFormatOutputValueJSON:
155+
logFormatJSON = true
156+
case cfdflags.LogFormatOutputValueDefault:
157+
// "default" and unset use the same logger output format
158+
fallthrough
159+
default:
160+
logFormatJSON = false
161+
}
152162

153163
loggerConfig := CreateConfig(
154164
logLevel,
155165
disableTerminal,
166+
logFormatJSON,
156167
logDirectory,
157168
logFile,
158169
)
@@ -177,6 +188,9 @@ func Create(loggerConfig *Config) *zerolog.Logger {
177188
}
178189

179190
func createConsoleLogger(config ConsoleConfig) io.Writer {
191+
if config.asJSON {
192+
return &consoleWriter{out: os.Stderr}
193+
}
180194
consoleOut := os.Stderr
181195
return zerolog.ConsoleWriter{
182196
Out: colorable.NewColorable(consoleOut),

0 commit comments

Comments
 (0)