clog/clog.go (109 lines of code) (raw):
// Copyright 2020 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package clog is a Context logger.
package clog
import (
"context"
"encoding/json"
"fmt"
"sync"
"github.com/GoogleCloudPlatform/guest-logging-go/logger"
"github.com/GoogleCloudPlatform/osconfig/pretty"
"google.golang.org/protobuf/proto"
)
// DebugEnabled will log debug messages.
var DebugEnabled bool
// https://golang.org/pkg/context/#WithValue
type clogKey struct{}
var ctxValueKey = clogKey{}
type log struct {
ctx context.Context
labels map[string]string
sync.Mutex
}
func (l *log) log(structuredPayload any, msg string, sev logger.Severity) {
// Set CallDepth 3, one for logger.Log, one for this function, and one for
// the calling clog function.
logger.Log(logger.LogEntry{Message: msg, StructuredPayload: structuredPayload, Severity: sev, CallDepth: 3, Labels: l.labels})
}
// protoToJSON converts a proto message to a generic JSON object for the purpose
// of passing to Cloud Logging.
//
// Conversion errors are encoded in the JSON object rather than returned,
// because callers of logging functions should not be forced to handle errors.
func protoToJSON(p proto.Message) any {
bytes, err := pretty.MarshalOptions().Marshal(p)
if err != nil {
return fmt.Sprintf("Error converting proto: %s", err)
}
return json.RawMessage(bytes)
}
// DebugRPC logs a completed RPC call.
func DebugRPC(ctx context.Context, method string, req proto.Message, resp proto.Message) {
// Do this here so we don't spend resources building the log message if we don't need to.
if !DebugEnabled || (req == nil && resp == nil) {
return
}
// The Cloud Logging library doesn't handle proto messages nor structures containing generic JSON.
// To work around this we construct map[string]any and fill it with JSON
// resulting from explicit conversion of the proto messages.
payload := map[string]any{}
payload["MethodName"] = method
var msg string
if resp != nil && req != nil {
payload["Response"] = protoToJSON(resp)
payload["Request"] = protoToJSON(req)
msg = fmt.Sprintf("Called: %s with request:\n%s\nresponse:\n%s\n", method, pretty.Format(req), pretty.Format(resp))
} else if resp != nil {
payload["Response"] = protoToJSON(resp)
msg = fmt.Sprintf("Called: %s with response:\n%s\n", method, pretty.Format(resp))
} else {
payload["Request"] = protoToJSON(req)
msg = fmt.Sprintf("Calling: %s with request:\n%s\n", method, pretty.Format(req))
}
fromContext(ctx).log(payload, msg, logger.Debug)
}
// DebugStructured is like Debugf but sends structuredPayload instead of the text message
// to Cloud Logging.
func DebugStructured(ctx context.Context, structuredPayload any, format string, args ...any) {
fromContext(ctx).log(structuredPayload, fmt.Sprintf(format, args...), logger.Debug)
}
// Debugf simulates logger.Debugf and adds context labels.
func Debugf(ctx context.Context, format string, args ...any) {
fromContext(ctx).log(nil, fmt.Sprintf(format, args...), logger.Debug)
}
// Infof simulates logger.Infof and adds context labels.
func Infof(ctx context.Context, format string, args ...any) {
fromContext(ctx).log(nil, fmt.Sprintf(format, args...), logger.Info)
}
// Warningf simulates logger.Warningf and context labels.
func Warningf(ctx context.Context, format string, args ...any) {
fromContext(ctx).log(nil, fmt.Sprintf(format, args...), logger.Warning)
}
// Errorf simulates logger.Errorf and adds context labels.
func Errorf(ctx context.Context, format string, args ...any) {
fromContext(ctx).log(nil, fmt.Sprintf(format, args...), logger.Error)
}
func (l *log) clone() *log {
l.Lock()
defer l.Unlock()
labels := map[string]string{}
for k, v := range l.labels {
labels[k] = v
}
return &log{
labels: labels,
}
}
func forContext(ctx context.Context) (*log, context.Context) {
cv := ctx.Value(ctxValueKey)
l, ok := cv.(*log)
if !ok {
l = &log{labels: map[string]string{}}
} else {
l = l.clone()
}
ctx = context.WithValue(ctx, ctxValueKey, l)
l.ctx = ctx
return l, ctx
}
func fromContext(ctx context.Context) *log {
if ctx == nil {
return &log{}
}
v := ctx.Value(ctxValueKey)
l, ok := v.(*log)
if !ok {
l = &log{}
}
return l
}
// WithLabels makes a log and context and adds the labels (overwriting any with the same key).
func WithLabels(ctx context.Context, labels map[string]string) context.Context {
if len(labels) == 0 {
return ctx
}
l, ctx := forContext(ctx)
l.Lock()
defer l.Unlock()
for k, v := range labels {
l.labels[k] = v
}
return ctx
}