-
Notifications
You must be signed in to change notification settings - Fork 219
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
add structured logging helper #269
Conversation
[APPROVALNOTIFIER] This PR is NOT APPROVED This pull-request has been approved by: pohly The full list of commands accepted by this bot can be found here.
Needs approval from an approver in each of these files:
Approvers can indicate their approval by writing |
/cc @shivanshu1333 @serathius |
The test works with This PR also conflicts with PR #268 (different output in the test). Let's discuss the API and later figure out how to test it. |
klog.go
Outdated
// The same can be done with custom structs. The advantage of LogAs is that the | ||
// formatting code can be written inline in the same function that logs some | ||
// object. | ||
type LogAs struct { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LogAs
is a proposal. Alternative suggestions are welcome.
I find it sounds a bit too much like "kick ass"... 😅
Note that I chose to implement this as a struct. The advantage is that the reader of code which uses LogAs
gets a hint what the functions do, at least when the struct gets initialized with named fields.
The disadvantage is that users might accidentally forget to initialize one of the functions. That compiles, but then will crash when used, at least as it stands now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about just As
? it'd read a bit more like klog.As
more than klog.LogAs
which stutters
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like that proposal.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I changed it.
klog.go
Outdated
} | ||
|
||
func (l LogAs) String() string { return l.Text() } | ||
func (l LogAs) MarshalLog() interface{} { return l.Object() } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here we could check for nil
functions to avoid crashes. If nil
, the helper functions could return "nil".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added.
Structured logging with InfoS is formatted differently than traditional output, so it's worthwhile to have an example that demostrates that. The new example didn't compile initially because an older version of klog was used. This gets avoids by always using the source from the current directory.
3ac7a02
to
11ec4e3
Compare
11ec4e3
to
0b2388e
Compare
Solved by moving the example. The downside is that it will not appear in the API documentation anymore. I had to cherry-pick one commit from my other PR to get
There should be no conflict anymore because the format change will be smaller than originally proposed. This PR is therefore ready for review and merging. |
When passing an object with complex content as a key in a key/value list, good formatting of that object will be different for text and structured output. Custom formatters can be provided by implementing fmt.Stringer and logr.Marshaler, but that implies defining custom types outside of the function which does the logging. The new klog.As helper struct provideds the necessary boilerplate code and allows users to keep all code inside the same function.
0b2388e
to
b6266e9
Compare
cd hack/tools && go test -v -race ./... | ||
- name: Test examples |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could use cd hack/tools && go test -v -race ./... && cd ../..
and keep everything in one step. I find it cleaner to have different steps with different names.
I also tried (cd hack/tools && go test -v -race ./...)
. That worked on Linux, but not on Windows because cd
still had an effect also after the subshell?!
Cannot find path 'D:\a\klog\klog\hack\tools\examples' because it does not exist.
/hold Let's also consider this code here: Let's ignore for a second that this should be I think it would make sense to have a helper which supports writing a
Both are special cases of |
I forgot that @vincepri: are you okay with this:
|
Delayed string formatting is useful also without structured logging. Several places in Kubernetes use if klog.V(2).Enabled() { <format some output as string> klog.Info(<message>) } This can be shortened to klog.V(2).Info(klog.AsString(func() string { <format and return output> }) For the sake of consistency and flexibility, klog.As then also should be a function instead of the underlying struct.
587b288
to
7c3ad19
Compare
@pohly I'd keep them separate, specifying a klog.As(
func() string {
return "something"
}, nil) which could quickly lead into confusion, especially for new users reading the code. We could just offer In most cases, do we expect folks to implement logr.Marshaler interface on an object, or potentially use one of the built-in wrappers, like the object reference one? Is that a correct assumption or am I missing something? |
We cannot drop What we can drop is the comment about the functions being allowed to be nil, i.e. the example you gave then should never be written like that. Should
|
BTW, I am currently working on POC PR against Kubernetes where I am using the new functions. Both |
It is needed in Kubernetes for code like this (from k8s.io/apiserver/pkg/server/httplog/httplog.go): // Log is intended to be called once at the end of your request handler, via defer func (rl *respLogger) Log() { ... klog.V(withLoggingLevel).InfoSDepth(1, "HTTP", keysAndValues...) } Without InfoSDepth as method for Verbose, such code has to use klog.InfoSDepth, which records the message with v=0 in JSON output. This is the wrong verbosity for this debug message. We don't need Verbose.ErrorSDepth (error messages have no verbosity) and also no Verbose.Info[f]Depth (when touching code, it should be rewritten to use Verbose.InfoSDepth instead)
c76b8d9
to
6564c1b
Compare
|
} | ||
klog.InfoS("No formatting", "item", item) | ||
|
||
as := klog.As( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think I understand the intermediate structs. I thought about something like this in th past but let it slip. Why is it more complex than:
// maybe in logr?
func AsString(fn func() string) fmt.Stringer {
return asString{fn}
}
func AsStruct(fn func() logr.PseudoStruct {
return asStruct{fn}
}
// in caller
log.V(3).Info("the message", "arg", logr.AsText(func() string {
// complex or expensive or hyper-local logic
})
I'm not even sure we need different functions? Why not:
type LogFunc func() interface{}
and
log.V(3).Info("the message", "arg", logr.LogFunc(func() interface{}) {
return expensiveProcessing(obj)
}
and handle LogFunc as a parallel to Stringer and LogMarshaller
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
log.V(3).Info("the message", "arg", logr.AsText(func() string
This will always be rendered as text, both in text output and JSON. Such a call can be useful to avoid extra Enabled()
calls. It's not absolutely required.
log.V(3).Info("the message", "arg", logr.LogFunc(func() interface{})
This has two problems:
- only loggers which support
logr.Marshaler
will invoke the function; the klog text format will callfmt.Sprintf("%v", <proxy object>)
, which then renders asobject(0x<address>)
- if it gets called, the function cannot decide whether the caller wants a human-readable string (text format) or machine-readable struct (JSON)
e.g. diff --git a/logr.go b/logr.go
index c05482a..707f7ac 100644
--- a/logr.go
+++ b/logr.go
@@ -499,3 +499,5 @@ type Marshaler interface {
// It may return any value of any type.
MarshalLog() interface{}
}
+
+type Callback func() interface{}
diff --git a/funcr/example_test.go b/funcr/example_test.go
index 53373a2..3800f97 100644
--- a/funcr/example_test.go
+++ b/funcr/example_test.go
@@ -45,6 +45,16 @@ func ExampleNewJSON() {
// Output: {"logger":"MyLogger","level":0,"msg":"the message","savedKey":"savedValue","key":"value"}
}
+func ExampleCallback() {
+ var log logr.Logger = funcr.NewJSON(func(obj string) {
+ fmt.Println(obj)
+ }, funcr.Options{})
+
+ log = log.WithName("MyLogger")
+ log.Info("the message", "key", logr.Callback(func() interface{} { return funcr.PseudoStruct{"v1", 1, "v2", 2} }))
+ // Output: {"logger":"MyLogger","level":0,"msg":"the message","key":{"v1":1,"v2":2}}
+}
+
func ExampleUnderlier() {
var log logr.Logger = funcr.New(func(prefix, args string) {
fmt.Println(prefix, args)
diff --git a/funcr/funcr.go b/funcr/funcr.go
index b23ab96..bef2c34 100644
--- a/funcr/funcr.go
+++ b/funcr/funcr.go
@@ -352,6 +352,11 @@ func (f Formatter) prettyWithFlags(value interface{}, flags uint32, depth int) s
// Replace the value with what the type wants to get logged.
// That then gets handled below via reflection.
value = v.MarshalLog()
+ } else if v, ok := value.(logr.Callback); ok {
+ //FIXME: define the semantics in logr - should this be an else-if or if?
+ // Replace the value with what the callsite wants to get logged.
+ // That then gets handled below via reflection.
+ value = v()
}
// Handle types that want to format themselves. |
This is semantically equivalent to the existing |
Maybe I am missing the point. If I have to define an "as" struct and 2
"methods" why would I not just define a real type? It's just a few LoC
more but is more reusable and understandable.
I proposed Callback as hyper-local thing, but now I am not sure we need
anything...
…On Mon, Dec 13, 2021, 10:17 AM Patrick Ohly ***@***.***> wrote:
logr.go: type Callback func() interface{}
This is semantically equivalent to the existing logr.Marshaler interface
except that it works for stand-alone functions (and only for those). I
don't think we need another, less flexible way of doing the same thing.
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
<#269 (comment)>, or
unsubscribe
<https://github.com/notifications/unsubscribe-auth/ABKWAVH3UHCDJH2SX3LNBILUQY2BZANCNFSM5H5CQL7Q>
.
Triage notifications on the go with GitHub Mobile for iOS
<https://apps.apple.com/app/apple-store/id1477376905?ct=notification-email&mt=8&pt=524675>
or Android
<https://play.google.com/store/apps/details?id=com.github.android&referrer=utm_campaign%3Dnotification-email%26utm_medium%3Demail%26utm_source%3Dgithub>.
|
The type and it's methods have to be defined outside of the function which wants to log values. Then the struct has to be initialized explicitly with all values that are going to be needed by the methods, which makes both the struct and its construction longer. Or is there a way to do that inside a function, with variables inside that function available inside the local type's methods? I can use kubernetes/kubernetes#106978 (review) as example to show how an actual log call can be converted when @thockin: what do you think about |
Let's back up a second. For hyper-local use-cases (e.g. "I have a thing I want to log, but I want to
But if you are going to do that more than one place, it makes sense to go My
In pretty much every case it seems the structured output would be an acceptable replacement for the text output, so I don't think you need the stringer part (it doesn't seem to pay for itself). If you remove that, you are more or less back to But I feel like I am still missing your point |
That's the same goal that I have with
Here I disagree. I think there are several places in Kubernetes were developers use custom code to prepare a text dump with information that they need for debugging. Turning that dump into a structure will make it less readable in text output ( But for such a case the I'll rewrite kubernetes/kubernetes#106978 without these helpers. |
If you have a hammer, the world looks like a nail... and all I did was hit my own finger. After taking another stab at kubernetes/kubernetes#106978 without the helper functions, the result turned out to be quite okay. In one place I managed to convince myself that a struct will be okay also for text output. Let's close this one and continue the review of the resulting code patterns in kubernetes/kubernetes#106978 /close |
@pohly: Closed this PR. In response to this:
Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes/test-infra repository. |
What this PR does / why we need it:
When passing an object with complex content as a key in a key/value list, good
formatting of that object will be different for text and structured
output. Custom formatters can be provided by implementing fmt.Stringer and
logr.Marshaler, but that implies defining custom types outside of the function
which does the logging.
The new LogAs helper struct provideds the necessary boilerplate code and allows
users to keep all code inside the same function.
Which issue(s) this PR fixes:
Fixes kubernetes/kubernetes#106258
Release note: