internal.go

  1// Copyright 2024, Google Inc.
  2// All rights reserved.
  3//
  4// Redistribution and use in source and binary forms, with or without
  5// modification, are permitted provided that the following conditions are
  6// met:
  7//
  8//     * Redistributions of source code must retain the above copyright
  9// notice, this list of conditions and the following disclaimer.
 10//     * Redistributions in binary form must reproduce the above
 11// copyright notice, this list of conditions and the following disclaimer
 12// in the documentation and/or other materials provided with the
 13// distribution.
 14//     * Neither the name of Google Inc. nor the names of its
 15// contributors may be used to endorse or promote products derived from
 16// this software without specific prior written permission.
 17//
 18// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 19// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 20// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 21// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 22// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 23// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 24// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 25// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 26// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 27// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 28// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 29
 30// Package internal provides some common logic and types to other logging
 31// sub-packages.
 32package internal
 33
 34import (
 35	"context"
 36	"io"
 37	"log/slog"
 38	"os"
 39	"strings"
 40	"time"
 41)
 42
 43const (
 44	// LoggingLevelEnvVar is the environment variable used to enable logging
 45	// at a particular level.
 46	LoggingLevelEnvVar = "GOOGLE_SDK_GO_LOGGING_LEVEL"
 47
 48	googLvlKey    = "severity"
 49	googMsgKey    = "message"
 50	googSourceKey = "sourceLocation"
 51	googTimeKey   = "timestamp"
 52)
 53
 54// NewLoggerWithWriter is exposed for testing.
 55func NewLoggerWithWriter(w io.Writer) *slog.Logger {
 56	lvl, loggingEnabled := checkLoggingLevel()
 57	if !loggingEnabled {
 58		return slog.New(noOpHandler{})
 59	}
 60	return slog.New(newGCPSlogHandler(lvl, w))
 61}
 62
 63// checkLoggingLevel returned the configured logging level and whether or not
 64// logging is enabled.
 65func checkLoggingLevel() (slog.Leveler, bool) {
 66	sLevel := strings.ToLower(os.Getenv(LoggingLevelEnvVar))
 67	var level slog.Level
 68	switch sLevel {
 69	case "debug":
 70		level = slog.LevelDebug
 71	case "info":
 72		level = slog.LevelInfo
 73	case "warn":
 74		level = slog.LevelWarn
 75	case "error":
 76		level = slog.LevelError
 77	default:
 78		return nil, false
 79	}
 80	return level, true
 81}
 82
 83// newGCPSlogHandler returns a Handler that is configured to output in a JSON
 84// format with well-known keys. For more information on this format see
 85// https://cloud.google.com/logging/docs/agent/logging/configuration#special-fields.
 86func newGCPSlogHandler(lvl slog.Leveler, w io.Writer) slog.Handler {
 87	return slog.NewJSONHandler(w, &slog.HandlerOptions{
 88		Level:       lvl,
 89		ReplaceAttr: replaceAttr,
 90	})
 91}
 92
 93// replaceAttr remaps default Go logging keys to match what is expected in
 94// cloud logging.
 95func replaceAttr(groups []string, a slog.Attr) slog.Attr {
 96	if groups == nil {
 97		if a.Key == slog.LevelKey {
 98			a.Key = googLvlKey
 99			return a
100		} else if a.Key == slog.MessageKey {
101			a.Key = googMsgKey
102			return a
103		} else if a.Key == slog.SourceKey {
104			a.Key = googSourceKey
105			return a
106		} else if a.Key == slog.TimeKey {
107			a.Key = googTimeKey
108			if a.Value.Kind() == slog.KindTime {
109				a.Value = slog.StringValue(a.Value.Time().Format(time.RFC3339))
110			}
111			return a
112		}
113	}
114	return a
115}
116
117// The handler returned if logging is not enabled.
118type noOpHandler struct{}
119
120func (h noOpHandler) Enabled(_ context.Context, _ slog.Level) bool {
121	return false
122}
123
124func (h noOpHandler) Handle(_ context.Context, _ slog.Record) error {
125	return nil
126}
127
128func (h noOpHandler) WithAttrs(_ []slog.Attr) slog.Handler {
129	return h
130}
131
132func (h noOpHandler) WithGroup(_ string) slog.Handler {
133	return h
134}