-
Notifications
You must be signed in to change notification settings - Fork 168
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
contextual logging #2121
contextual logging #2121
Changes from 13 commits
7b4dea0
0d5e0a8
a8dd1ff
fd34f5a
546895c
0a50dd1
007dc08
72e392d
a8f6c59
1cffa55
3cfa185
e22b1cf
5046c59
335553e
0578ddd
5f5e23b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,185 @@ | ||
/* | ||
Package clog provides Context with logging information. | ||
*/ | ||
package clog | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"strconv" | ||
"strings" | ||
"sync" | ||
|
||
"github.com/golang/glog" | ||
) | ||
|
||
// unique type to prevent assignment. | ||
type clogContextKeyT struct{} | ||
|
||
var clogContextKey = clogContextKeyT{} | ||
|
||
const ( | ||
// standard keys | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the meaning of a standard key? Why There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
These constants exists only for one reason - to be able to print them in sorted order.
It is. I just forgot to add it to the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ahh, I see, then I think you can remove this comment You can just call them There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. They are 'standard' because we define them in the package, and have separate |
||
manifestID = "manifestID" | ||
sessionID = "sessionID" | ||
nonce = "nonce" | ||
seqNo = "seqNo" | ||
orchSessionID = "orchSessionID" // session id generated on orchestrator for broadcaster | ||
) | ||
|
||
// Verbose is a boolean type that implements Infof (like Printf) etc. | ||
type Verbose bool | ||
|
||
var stdKeys map[string]bool | ||
var stdKeysOrder = []string{manifestID, sessionID, nonce, seqNo, orchSessionID} | ||
|
||
func init() { | ||
stdKeys = make(map[string]bool) | ||
for _, key := range stdKeysOrder { | ||
stdKeys[key] = true | ||
} | ||
} | ||
|
||
type values struct { | ||
mu sync.RWMutex | ||
vals map[string]string | ||
} | ||
|
||
func newValues() *values { | ||
return &values{ | ||
vals: make(map[string]string), | ||
} | ||
} | ||
|
||
// Clone creates new context with parentCtx as parent and | ||
// logging details from logCtx | ||
func Clone(parentCtx, logCtx context.Context) context.Context { | ||
cmap, _ := logCtx.Value(clogContextKey).(*values) | ||
newCmap := newValues() | ||
if cmap != nil { | ||
cmap.mu.RLock() | ||
for k, v := range cmap.vals { | ||
newCmap.vals[k] = v | ||
} | ||
cmap.mu.RUnlock() | ||
} | ||
return context.WithValue(parentCtx, clogContextKey, newCmap) | ||
} | ||
|
||
func AddManifestID(ctx context.Context, val string) context.Context { | ||
return AddVal(ctx, manifestID, val) | ||
} | ||
|
||
func AddSessionID(ctx context.Context, val string) context.Context { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see this function is never used, why don't we set session ID anywhere? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point. We have vague definition of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
return AddVal(ctx, sessionID, val) | ||
} | ||
|
||
func AddNonce(ctx context.Context, val uint64) context.Context { | ||
return AddVal(ctx, nonce, strconv.FormatUint(val, 10)) | ||
} | ||
|
||
func AddSeqNo(ctx context.Context, val uint64) context.Context { | ||
return AddVal(ctx, seqNo, strconv.FormatUint(val, 10)) | ||
victorges marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
func AddOrchSessionID(ctx context.Context, val string) context.Context { | ||
return AddVal(ctx, orchSessionID, val) | ||
} | ||
|
||
func AddVal(ctx context.Context, key, val string) context.Context { | ||
cmap, _ := ctx.Value(clogContextKey).(*values) | ||
if cmap == nil { | ||
cmap = newValues() | ||
ctx = context.WithValue(ctx, clogContextKey, cmap) | ||
} | ||
Comment on lines
+89
to
+94
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One thing that bothers me about the "lazy init" approach is that every interface we give needs to receive & return a ctx = clog.InitFields(ctx) and then it can just be used to add fields without worrying about re-setting the clog.AddVal(ctx, "a", "b") or clog.Fields(ctx).Add("a", "b") or even ⛓️ clog.Fields(ctx)
.Add("a", "b")
.Add("c", "d") In which Note that we could also allow both interfaces to be used, with the disclaimer that, if you use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right now, after context is initialised, you can just do |
||
cmap.mu.Lock() | ||
cmap.vals[key] = val | ||
cmap.mu.Unlock() | ||
Comment on lines
+95
to
+97
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would be great if we could avoid this lock somehow, as logging ideally shouldn't be something you need to be too cautious about performance issues. I wonder if no lock at all wouldn't be sufficient here, since that also considering the current interface which returns a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I thought so, but was forced to add lock after panics 😄 Turned out we do use same context from different go-routines. |
||
return ctx | ||
} | ||
|
||
func Warningf(ctx context.Context, format string, args ...interface{}) { | ||
glog.WarningDepth(1, formatMessage(ctx, false, format, args...)) | ||
} | ||
|
||
func Errorf(ctx context.Context, format string, args ...interface{}) { | ||
glog.ErrorDepth(1, formatMessage(ctx, false, format, args...)) | ||
} | ||
|
||
func Fatalf(ctx context.Context, format string, args ...interface{}) { | ||
glog.FatalDepth(1, formatMessage(ctx, false, format, args...)) | ||
} | ||
|
||
func Infof(ctx context.Context, format string, args ...interface{}) { | ||
infof(ctx, false, format, args...) | ||
} | ||
|
||
// Infofe if last argument is not nil it will be printed as " err=%q" | ||
func Infofe(ctx context.Context, format string, args ...interface{}) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there any known convention with this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is convention that I've just invented 😄 if err != nil {
glog.Infof("message var1=%s err=%v", var1, err)
} else {
glog.Infof("message var1=%s", var1)
}`
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok, I see. I'd probably prefer having There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You would prefer not to have There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This will be so much less insane now that we have an actual tool for querying logs by their actual fields and not just text search 😄
Since it is a new convetion and thus not something that is self-explanatory, WDYT of using a more explicit name for the function? I like And another approach is providing an ctx = clog.AddErr(ctx, err)
clog.Infof(ctx, "message var1=%s", var1) or with the chaining idea in the other comment: clog.Fields(ctx).AddErr(err)
.Infof("message var1=%s", var1) |
||
infof(ctx, true, format, args...) | ||
} | ||
|
||
func V(level glog.Level) Verbose { | ||
return Verbose(glog.V(level)) | ||
} | ||
|
||
// Infof is equivalent to the global Infof function, guarded by the value of v. | ||
// See the documentation of V for usage. | ||
darkdarkdragon marked this conversation as resolved.
Show resolved
Hide resolved
|
||
func (v Verbose) Infof(ctx context.Context, format string, args ...interface{}) { | ||
if v { | ||
infof(ctx, false, format, args...) | ||
} | ||
} | ||
|
||
func (v Verbose) Infofe(ctx context.Context, format string, args ...interface{}) { | ||
if v { | ||
infof(ctx, true, format, args...) | ||
} | ||
} | ||
|
||
func infof(ctx context.Context, lastErr bool, format string, args ...interface{}) { | ||
glog.InfoDepth(2, formatMessage(ctx, lastErr, format, args...)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I already asked about it, but it gets lost somewhere during the refactor ;) Why is the depth here There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ahh, I see. Consider adding a comment to the code to explain it. |
||
} | ||
|
||
func messageFromContext(ctx context.Context, sb *strings.Builder) { | ||
if ctx == nil { | ||
return | ||
} | ||
cmap, _ := ctx.Value(clogContextKey).(*values) | ||
if cmap == nil { | ||
return | ||
} | ||
cmap.mu.RLock() | ||
for _, key := range stdKeysOrder { | ||
if val, ok := cmap.vals[key]; ok { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see you print the standard keys in some predefined order. However, not standard keys are printed in random order. Is that intended? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
It is consequence of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we prepare the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Do we use it anywhere? Why do we need it? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it will be quite annoying to go through logs where the keys have a random order on every line. Especially when for example going through a stream of the same log type, I normally get used to find the data I want on the same position on the line (cause fields generally have have the same length) and just move around. Totally something that we can change in implementation later without breaking compatibility, so not necessary now, but giving my +1 here that we probably want to do some deterministic key order here soon. We could create an auxiliary |
||
sb.WriteString(key) | ||
sb.WriteString("=") | ||
sb.WriteString(val) | ||
sb.WriteString(" ") | ||
} | ||
} | ||
for key, val := range cmap.vals { | ||
if _, ok := stdKeys[key]; !ok { | ||
sb.WriteString(key) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: Consider extracting to a function, these 4 lines are the same as in line 166. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I could, but I don't see much value in it. |
||
sb.WriteString("=") | ||
sb.WriteString(val) | ||
sb.WriteString(" ") | ||
} | ||
} | ||
cmap.mu.RUnlock() | ||
} | ||
|
||
func formatMessage(ctx context.Context, lastErr bool, format string, args ...interface{}) string { | ||
var sb strings.Builder | ||
messageFromContext(ctx, &sb) | ||
var err interface{} | ||
if lastErr && len(args) > 0 { | ||
err = args[len(args)-1] | ||
args = args[:len(args)-1] | ||
} | ||
sb.WriteString(fmt.Sprintf(format, args...)) | ||
if err != nil { | ||
sb.WriteString(fmt.Sprintf(" err=%q", err)) | ||
} | ||
return sb.String() | ||
} | ||
victorges marked this conversation as resolved.
Show resolved
Hide resolved
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
package clog | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestStdKeys(t *testing.T) { | ||
assert := assert.New(t) | ||
ctx := AddManifestID(context.Background(), "manID") | ||
ctx = AddSessionID(ctx, "sessionID") | ||
ctx = AddNonce(ctx, 1038) | ||
ctx = AddOrchSessionID(ctx, "orchID") | ||
ctx = AddSeqNo(ctx, 9427) | ||
ctx = AddVal(ctx, "customKey", "customVal") | ||
msg := formatMessage(ctx, false, "testing message num=%d", 452) | ||
assert.Equal("manifestID=manID sessionID=sessionID nonce=1038 seqNo=9427 orchSessionID=orchID customKey=customVal testing message num=452", msg) | ||
ctxCloned := Clone(context.Background(), ctx) | ||
ctxCloned = AddManifestID(ctxCloned, "newManifest") | ||
msgCloned := formatMessage(ctxCloned, false, "testing message num=%d", 4521) | ||
assert.Equal("manifestID=newManifest sessionID=sessionID nonce=1038 seqNo=9427 orchSessionID=orchID customKey=customVal testing message num=4521", msgCloned) | ||
// old context shouldn't change | ||
msg = formatMessage(ctx, false, "testing message num=%d", 452) | ||
assert.Equal("manifestID=manID sessionID=sessionID nonce=1038 seqNo=9427 orchSessionID=orchID customKey=customVal testing message num=452", msg) | ||
} | ||
|
||
func TestLastErr(t *testing.T) { | ||
assert := assert.New(t) | ||
ctx := AddManifestID(context.Background(), "manID") | ||
var err error | ||
msg := formatMessage(ctx, true, "testing message num=%d", 452, err) | ||
assert.Equal("manifestID=manID testing message num=452", msg) | ||
err = errors.New("test error") | ||
msg = formatMessage(ctx, true, "testing message num=%d", 452, err) | ||
assert.Equal("manifestID=manID testing message num=452 err=\"test error\"", msg) | ||
} |
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.
How about creating a dedicated unit test for this file, like
clog_test.go
?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 it will test?
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 think it should at least:
AddManifestID()
,AddOrchSessionID()
)messageFromContext()
and assert the resultYou can think if something else can be tested.
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 tests
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.
Great, thanks!