Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

structured logging: support log messages with line breaks #268

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 3 additions & 1 deletion examples/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@ go 1.13

require (
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b
k8s.io/klog/v2 v2.0.0-20200324194303-db919253a3bc
k8s.io/klog/v2 v2.30.0
)

replace k8s.io/klog/v2 => ../
2 changes: 2 additions & 0 deletions examples/go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
github.com/go-logr/logr v0.1.0 h1:M1Tv3VzNlEHg6uyACnRdtrploV2P7wZqH8BoQMtz0cg=
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
github.com/go-logr/logr v1.2.0 h1:QK40JKJyMdUDz+h+xvCsru/bJhvG0UxvePV0ufL/AcE=
github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
k8s.io/klog/v2 v2.0.0-20200324194303-db919253a3bc h1:E/enZ+SqXD3ChluFNvXqlLcUkqMQQDpiyGruRq5pjvY=
Expand Down
74 changes: 74 additions & 0 deletions examples/structured_logging/structured_logging.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package main

import (
"flag"

"k8s.io/klog/v2"
)

// MyStruct will be logged via %+v
type MyStruct struct {
Name string
Data string
internal int
}

// MyStringer will be logged as string, with String providing that string.
type MyString MyStruct

func (m MyString) String() string {
return m.Name + ": " + m.Data
}

func main() {
klog.InitFlags(nil)
flag.Parse()

someData := MyStruct{
Name: "hello",
Data: "world",
}

longData := MyStruct{
Name: "long",
Data: `Multiple
lines
with quite a bit
of text.`,
}

logData := MyStruct{
Name: "log output from some program",
Data: `I0000 12:00:00.000000 123456 main.go:42] Starting
E0000 12:00:01.000000 123456 main.go:43] Failed for some reason
`,
}

stringData := MyString(longData)

klog.Infof("someData printed using InfoF: %v", someData)
klog.Infof("longData printed using InfoF: %v", longData)
klog.Infof(`stringData printed using InfoF,
with the message across multiple lines:
%v`, stringData)
klog.Infof("logData printed using InfoF:\n%v", logData)

klog.Info("=============================================")

klog.InfoS("using InfoS", "someData", someData)
klog.InfoS("using InfoS", "longData", longData)
klog.InfoS(`using InfoS with
the message across multiple lines`,
"int", 1,
"stringData", stringData,
"str", "another value")
klog.InfoS("using InfoS", "logData", logData)
klog.InfoS("using InfoS", "boolean", true, "int", 1, "float", 0.1)

// The Kubernetes recommendation is to start the message with uppercase
// and not end with punctuation. See
// https://github.com/kubernetes/community/blob/HEAD/contributors/devel/sig-instrumentation/migration-to-structured-logging.md
klog.InfoS("Did something", "item", "foobar")
// Not recommended, but also works.
klog.InfoS("This is a full sentence.", "item", "foobar")
}
54 changes: 45 additions & 9 deletions klog.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ import (
"sync"
"sync/atomic"
"time"
"unicode"

"github.com/go-logr/logr"
)
Expand Down Expand Up @@ -802,10 +803,16 @@ func (l *loggingT) infoS(logger *logr.Logger, filter LogFilter, depth int, msg s
// set log severity by s
func (l *loggingT) printS(err error, s severity, depth int, msg string, keysAndValues ...interface{}) {
b := &bytes.Buffer{}
b.WriteString(fmt.Sprintf("%q", msg))
// The message is never quoted. New lines get indented.
writeString(b, msg)
// Enhance readability by inserting : between message and key/value

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't agree with this change, we decided on quoting the string as this gives us predictable and somewhat parsable output (not everyone will imminently switch to JSON). Idea of quoting was related to message being treated as structured metadata. This is based on idea that JSON uses "msg" key to store message, so we though that message should be written as other string values with one difference of msg= being skipped.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't agree with this change,

This PR or the line you commented on, the : separator? I can drop that separator, it's not important.

But quoting is a major problem. I get what was meant to be achieved with it, but IMHO the effect on readability was not considered. it now prevents conversion to structured logging because developers will complain or even go as far as filing bugs and reverting the conversion.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with Marek, JSON should not become a dependency for log parsing, the raw log output should be parsable

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current text format is not machine-readable. If that was the goal, then we have bugs in its implementation around handling of structs.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So as you are both against this proposal, how do you suggest we should deal with cases where developers want multi-line output in a readable format (kubernetes/kubernetes#104868)?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should separate how we consider multi line for different part of logs. In my opinion multiline:

  • doesn't make sense for log messages. They should be a short description of what event is happening in system. Their format should not differ much or at all between different log formats. For example as I user that switched between log formats I want to be able to find log I have seen in old format in the new format just by grepping the message.
  • doesn't make sense for log keys. They should strive to uniquely identify types of metadata attached to log. Strive comes from idea that over time a schema will develop that will have a predictable way to find values under specific keys. Their names should follow a strict convention to avoid diverging.
  • can make sense for complex values when debugging. I assumed handling this case is needed Scheduler logged config prints without line breaks kubernetes#104868. There are multiple cases in k8s logs that multiline logs are used to improve readability. We should allow users to customize that some values (for example dumping whole k8s object yaml) are written in multiline format.

Copy link
Author

@pohly pohly Nov 16, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can make sense for complex values when debugging.

Agreed.

We should allow users to customize that some values (for example dumping whole k8s object yaml) are written in multiline format.

And how to you propose to do that?

We need an API for it and a serialization format that log parsers then will understand.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my opinion multiline doesn't make sense for log messages.

I can buy that argument. So let me propose a solution just for multi-line values in #273

I'm going to close this PR here in favor of that one.

/close

// pairs, but only when the message does not already end with
// punctation.
if len(msg) > 0 && !unicode.IsPunct(rune(msg[len(msg)-1])) && (err != nil || len(keysAndValues) > 0) {
b.WriteString(" |")
}
if err != nil {
b.WriteByte(' ')
b.WriteString(fmt.Sprintf("err=%q", err.Error()))
kvListFormat(b, "err", err)
}
kvListFormat(b, keysAndValues...)
l.printDepth(s, logging.logr, nil, depth+1, b)
Expand All @@ -824,21 +831,50 @@ func kvListFormat(b *bytes.Buffer, keysAndValues ...interface{}) {
}
b.WriteByte(' ')

switch v.(type) {
case string, error:
b.WriteString(fmt.Sprintf("%s=%q", k, v))
switch v := v.(type) {
case string:
writeStringValue(b, k, true, v)
case error:
writeStringValue(b, k, true, v.Error())
case []byte:
b.WriteString(fmt.Sprintf("%s=%+q", k, v))
default:
if _, ok := v.(fmt.Stringer); ok {
b.WriteString(fmt.Sprintf("%s=%q", k, v))
if s, ok := v.(fmt.Stringer); ok {
writeStringValue(b, k, true, s.String())
} else {
b.WriteString(fmt.Sprintf("%s=%+v", k, v))
writeStringValue(b, k, false, fmt.Sprintf("%+v", v))
}
}
}
}

func writeStringValue(b *bytes.Buffer, k interface{}, quoteV bool, v string) {
if !strings.Contains(v, "\n") {
b.WriteString(fmt.Sprintf("%s=", k))
if quoteV {
// Simple string, quote quotation marks and non-printable characters.
b.WriteString(strconv.Quote(v))
return
}
// Non-string with no line breaks.
b.WriteString(v)
return
}
// Complex multi-line string, show as-is with indention.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see any benefit of adding built in markers for logs. They

  • don't make logs more readable as users can klog header to already distinguish start vs end of multi line log
  • adding standard markers makes parsing logs harder, existing kubernetes log parsers already should handle multi line log.
  • Increases log volume

K8s logs already have multi line logs that provide their own markers, thus I think we should leave it up user to provide
them themselves.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there's a misunderstanding. The markers are not delimiters for messages. They are delimiters for values.

With the current InfoS format, the quotation marks provides those delimiters. But they result in output that is basically unreadable for a human, which defeats the purpose of the text output.

Copy link
Author

@pohly pohly Nov 15, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Increases log volume

That's a valid point. We could make them shorter. For example:

I1112 15:01:48.696677  413871 structured_logging.go:52] using InfoS with
 the message across multiple lines: int=1 stringData>>>
 long: Multiple
 lines
 with quite a bit
 of text.
 <<< str="another value"

That's four bytes (>>> plus newline) instead of two (=") and three bytes (<<<) instead of one (") at the end.

Line breaks are the same with new line plus space instead of \n.

If the text contains many quotation marks, this format might actually be shorter because those don't need to be escaped.

b.WriteString(fmt.Sprintf("%s>>>\n ", k))
writeString(b, v)
b.WriteString("\n <<<")
}

func writeString(b *bytes.Buffer, s string) {
for i, line := range strings.Split(s, "\n") {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does strings.Split here needlessly copies the string into slices?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. I'll add benchmarking and make the code more efficient before merging. Right now I am only looking for feedback on the format.

/hold

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#273 has detailed benchmark data.

if i > 0 {
b.WriteString("\n ")
}
b.WriteString(line)
}
}

// redirectBuffer is used to set an alternate destination for the logs
type redirectBuffer struct {
w io.Writer
Expand Down
65 changes: 53 additions & 12 deletions klog_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -894,22 +894,22 @@ func TestInfoS(t *testing.T) {
}{
{
msg: "test",
format: "I0102 15:04:05.067890 1234 klog_test.go:%d] \"test\" pod=\"kubedns\"\n",
format: "I0102 15:04:05.067890 1234 klog_test.go:%d] test | pod=\"kubedns\"\n",
keysValues: []interface{}{"pod", "kubedns"},
},
{
msg: "test",
format: "I0102 15:04:05.067890 1234 klog_test.go:%d] \"test\" replicaNum=20\n",
format: "I0102 15:04:05.067890 1234 klog_test.go:%d] test | replicaNum=20\n",
keysValues: []interface{}{"replicaNum", 20},
},
{
msg: "test",
format: "I0102 15:04:05.067890 1234 klog_test.go:%d] \"test\" err=\"test error\"\n",
format: "I0102 15:04:05.067890 1234 klog_test.go:%d] test | err=\"test error\"\n",
keysValues: []interface{}{"err", errors.New("test error")},
},
{
msg: "test",
format: "I0102 15:04:05.067890 1234 klog_test.go:%d] \"test\" err=\"test error\"\n",
format: "I0102 15:04:05.067890 1234 klog_test.go:%d] test | err=\"test error\"\n",
keysValues: []interface{}{"err", errors.New("test error")},
},
}
Expand Down Expand Up @@ -943,26 +943,53 @@ func TestVInfoS(t *testing.T) {
return time.Date(2006, 1, 2, 15, 4, 5, .067890e9, time.Local)
}
pid = 1234
myData := struct {
Data string
}{
Data: `This is some long text
with a line break.`,
}
var testDataInfo = []struct {
msg string
format string
keysValues []interface{}
}{
{
msg: "test",
format: "I0102 15:04:05.067890 1234 klog_test.go:%d] \"test\" pod=\"kubedns\"\n",
format: "I0102 15:04:05.067890 1234 klog_test.go:%d] test | pod=\"kubedns\"\n",
keysValues: []interface{}{"pod", "kubedns"},
},
{
msg: "test",
format: "I0102 15:04:05.067890 1234 klog_test.go:%d] \"test\" replicaNum=20\n",
format: "I0102 15:04:05.067890 1234 klog_test.go:%d] test | replicaNum=20\n",
keysValues: []interface{}{"replicaNum", 20},
},
{
msg: "test",
format: "I0102 15:04:05.067890 1234 klog_test.go:%d] \"test\" err=\"test error\"\n",
format: "I0102 15:04:05.067890 1234 klog_test.go:%d] test | err=\"test error\"\n",
keysValues: []interface{}{"err", errors.New("test error")},
},
{
msg: `first message line
second message line`,
format: `I0102 15:04:05.067890 1234 klog_test.go:%d] first message line
second message line | multiLine>>>
first value line
second value line
<<<
`,
keysValues: []interface{}{"multiLine", `first value line
second value line`},
},
{
msg: `message`,
format: `I0102 15:04:05.067890 1234 klog_test.go:%d] message | myData>>>
{Data:This is some long text
with a line break.}
<<<
`,
keysValues: []interface{}{"myData", myData},
},
}

logging.verbosity.Set("2")
Expand All @@ -987,7 +1014,7 @@ func TestVInfoS(t *testing.T) {
want = ""
}
if contents(infoLog) != want {
t.Errorf("V(%d).InfoS has unexpected output: \n got:\t%s\nwant:\t%s", l, contents(infoLog), want)
t.Errorf("V(%d).InfoS has unexpected output:\ngot:\n%s\nwant:\n%s\n", l, contents(infoLog), want)
}
}
}
Expand All @@ -1014,11 +1041,11 @@ func TestErrorS(t *testing.T) {
}{
{
err: fmt.Errorf("update status failed"),
format: "E0102 15:04:05.067890 1234 klog_test.go:%d] \"Failed to update pod status\" err=\"update status failed\" pod=\"kubedns\"\n",
format: "E0102 15:04:05.067890 1234 klog_test.go:%d] Failed to update pod status | err=\"update status failed\" pod=\"kubedns\"\n",
},
{
err: nil,
format: "E0102 15:04:05.067890 1234 klog_test.go:%d] \"Failed to update pod status\" pod=\"kubedns\"\n",
format: "E0102 15:04:05.067890 1234 klog_test.go:%d] Failed to update pod status | pod=\"kubedns\"\n",
},
}
for _, e := range errorList {
Expand All @@ -1031,7 +1058,7 @@ func TestErrorS(t *testing.T) {
}
want := fmt.Sprintf(e.format, line)
if contents(errorLog) != want {
t.Errorf("ErrorS has wrong format: \n got:\t%s\nwant:\t%s", contents(errorLog), want)
t.Errorf("ErrorS has wrong format:\ngot:\n%s\nwant:\n%s\n", contents(errorLog), want)
}
}
}
Expand Down Expand Up @@ -1075,6 +1102,20 @@ func TestKvListFormat(t *testing.T) {
keysValues: []interface{}{"pod", "kubedns", "bytes", []byte("��=� ⌘")},
want: " pod=\"kubedns\" bytes=\"\\ufffd\\ufffd=\\ufffd \\u2318\"",
},
{
keysValues: []interface{}{"multiLineString", `Hello world!
Starts with tab.
Starts with spaces.
No whitespace.`,
"pod", "kubedns",
},
want: ` multiLineString>>>
Hello world!
Starts with tab.
Starts with spaces.
No whitespace.
<<< pod="kubedns"`,
},
{
keysValues: []interface{}{"pod", "kubedns", "maps", map[string]int{"three": 4}},
want: " pod=\"kubedns\" maps=map[three:4]",
Expand Down Expand Up @@ -1113,7 +1154,7 @@ func TestKvListFormat(t *testing.T) {
b := &bytes.Buffer{}
kvListFormat(b, d.keysValues...)
if b.String() != d.want {
t.Errorf("kvlist format error:\n got:\n\t%s\nwant:\t%s", b.String(), d.want)
t.Errorf("kvlist format error:\ngot:\n%s\nwant:\n%s\n", b.String(), d.want)
}
}
}
Expand Down