blob: 53cead40b0d8aa9747d7fb8543b27c88314bc6b9 [file]
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to You 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 logging performs structured output of log entries.
package logging
import (
"context"
"encoding/json"
"io"
"log/slog"
"os"
"path"
"runtime"
"sync"
"cloud.google.com/go/logging"
"cloud.google.com/go/logging/apiv2/loggingpb"
)
// Options for the slog.Logger
type Options struct {
*slog.HandlerOptions
Name string
Writer io.Writer
Client *logging.Client
}
// New instantiates a slog.Logger to output using Google Cloud logging entries.
// When running locally, output is JSON strings of Cloud logging entries and
// does not make any API calls to the service. When running in Google Cloud,
// logging entries are submitted to the Cloud logging service.
func New(opts *Options) *slog.Logger {
if opts.HandlerOptions == nil {
opts.HandlerOptions = &slog.HandlerOptions{}
}
opts.AddSource = true
if opts.Writer == nil {
opts.Writer = os.Stdout
}
handler := &gcpHandler{
name: opts.Name,
mu: &sync.Mutex{},
out: opts.Writer,
JSONHandler: slog.NewJSONHandler(opts.Writer, opts.HandlerOptions),
}
if opts.Client != nil {
handler.logger = opts.Client.Logger(path.Base(opts.Name))
}
return slog.New(handler)
}
var _ slog.Handler = &gcpHandler{}
type gcpHandler struct {
name string
*slog.JSONHandler
mu *sync.Mutex
out io.Writer
logger *logging.Logger
}
func (g *gcpHandler) Enabled(ctx context.Context, level slog.Level) bool {
return g.JSONHandler.Enabled(ctx, level)
}
func severity(lvl slog.Level) logging.Severity {
switch lvl {
case slog.LevelDebug:
return logging.Debug
case slog.LevelInfo:
return logging.Info
case slog.LevelWarn:
return logging.Warning
case slog.LevelError:
return logging.Error
}
return logging.Default
}
func (g *gcpHandler) Handle(_ context.Context, record slog.Record) error {
payload := map[string]any{
"message": record.Message,
}
record.Attrs(func(attr slog.Attr) bool {
payload[attr.Key] = attr.Value.Any()
return true
})
fs := runtime.CallersFrames([]uintptr{record.PC})
f, _ := fs.Next()
entry := logging.Entry{
LogName: g.name,
Timestamp: record.Time,
Severity: severity(record.Level),
Payload: payload,
SourceLocation: &loggingpb.LogEntrySourceLocation{
File: f.File,
Line: int64(f.Line),
},
}
g.mu.Lock()
defer g.mu.Unlock()
if g.logger == nil {
return json.NewEncoder(g.out).Encode(entry)
}
entry.LogName = ""
g.logger.Log(entry)
return g.logger.Flush()
}
func (g *gcpHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
h := g.JSONHandler
return h.WithAttrs(attrs)
}
func (g *gcpHandler) WithGroup(name string) slog.Handler {
h := g.JSONHandler
return h.WithGroup(name)
}