pkg/inspection/metadata/logger/logger.go (161 lines of code) (raw):
// Copyright 2024 Google LLC
//
// 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 logger
import (
"bytes"
"context"
"log/slog"
"os"
"github.com/GoogleCloudPlatform/khi/pkg/common/khictx"
"github.com/GoogleCloudPlatform/khi/pkg/common/typedmap"
inspection_task_contextkey "github.com/GoogleCloudPlatform/khi/pkg/inspection/contextkey"
"github.com/GoogleCloudPlatform/khi/pkg/inspection/logger"
"github.com/GoogleCloudPlatform/khi/pkg/inspection/metadata"
"github.com/GoogleCloudPlatform/khi/pkg/parameters"
"github.com/GoogleCloudPlatform/khi/pkg/task"
task_contextkey "github.com/GoogleCloudPlatform/khi/pkg/task/contextkey"
)
var LoggerMetadataKey = metadata.NewMetadataKey[*Logger]("log")
// similarLogThrottlingLogCount is the count of similar logs to start preventing output.
var similarLogThrottlingLogCount = 10
var _ slog.Handler = (*TaskSlogHandler)(nil)
type TaskSlogHandler struct {
enableStdout bool
stdoutHandler slog.Handler
stringHandler slog.Handler
minLogLevel slog.Level
throttle LogThrottler
}
type SerializableLogItem struct {
Id string `json:"id"`
Name string `json:"name"`
Log string `json:"log"`
}
// Enabled implements slog.Handler.
func (t *TaskSlogHandler) Enabled(ctx context.Context, level slog.Level) bool {
if level < t.minLogLevel {
return false
}
if !t.enableStdout {
return t.stringHandler.Enabled(ctx, level)
}
return t.stdoutHandler.Enabled(ctx, level) || t.stringHandler.Enabled(ctx, level)
}
// Handle implements slog.Handler.
func (t *TaskSlogHandler) Handle(ctx context.Context, r slog.Record) error {
throttleStatus := t.throttle.ThrottleStatus(t.getLogKind(r))
if throttleStatus == StatusThrottled {
return nil
}
if throttleStatus == StatusJustBeforeThrottle {
r = r.Clone()
r.Message += "\n (Over 10 similar logs shown for this task. Similar logs will be omitted from next.)"
}
if t.enableStdout {
t.stdoutHandler.Handle(ctx, r)
}
if r.Level >= slog.LevelInfo {
// store string log only for >= info logs.
return t.stringHandler.Handle(ctx, r)
}
return nil
}
// WithAttrs implements slog.Handler.
func (t *TaskSlogHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
return &TaskSlogHandler{
minLogLevel: t.minLogLevel,
enableStdout: t.enableStdout,
stdoutHandler: t.stdoutHandler.WithAttrs(attrs),
stringHandler: t.stringHandler.WithAttrs(attrs),
throttle: t.throttle,
}
}
// WithGroup implements slog.Handler.
func (t *TaskSlogHandler) WithGroup(name string) slog.Handler {
return &TaskSlogHandler{
minLogLevel: t.minLogLevel,
enableStdout: t.enableStdout,
stdoutHandler: t.stdoutHandler.WithGroup(name),
stringHandler: t.stringHandler.WithGroup(name),
throttle: t.throttle,
}
}
// getLogKind returns the log kind from attrs in slog.Record
func (t *TaskSlogHandler) getLogKind(r slog.Record) string {
kind := ""
r.Attrs(func(a slog.Attr) bool {
if a.Key == logger.LogKindAttrKey {
kind = a.Value.String()
return false
}
return true
})
return kind
}
type TaskLogger struct {
id string
name string
logHandler slog.Handler
logBuffer *bytes.Buffer
}
func (t *TaskLogger) Read() string {
return t.logBuffer.String()
}
func (t *TaskLogger) AsSerializableLogItem() *SerializableLogItem {
return &SerializableLogItem{
Id: t.id,
Name: t.name,
Log: t.Read(),
}
}
type Logger struct {
loggers []*TaskLogger
}
var _ metadata.Metadata = (*Logger)(nil)
// Labels implements metadata.Metadata.
func (*Logger) Labels() *typedmap.ReadonlyTypedMap {
return task.NewLabelSet(
metadata.IncludeInRunResult(),
)
}
// ToSerializable implements metadata.Metadata.
func (l *Logger) ToSerializable() interface{} {
result := make([]*SerializableLogItem, 0)
for _, l := range l.loggers {
result = append(result, l.AsSerializableLogItem())
}
return result
}
func (l *Logger) MakeTaskLogger(ctx context.Context, minLevel slog.Level) *TaskLogger {
stdoutWithColor := true
if parameters.Debug.NoColor != nil && *parameters.Debug.NoColor {
stdoutWithColor = false
}
tid, err := khictx.GetValue(ctx, task_contextkey.TaskImplementationIDContextKey)
if err == nil {
iid, err := khictx.GetValue(ctx, inspection_task_contextkey.InspectionTaskInspectionID)
if err == nil {
rid, err := khictx.GetValue(ctx, inspection_task_contextkey.InspectionTaskRunID)
if err == nil {
lb := new(bytes.Buffer)
th := &TaskSlogHandler{
minLogLevel: minLevel,
enableStdout: true,
stdoutHandler: logger.NewKHIFormatLogger(os.Stdout, stdoutWithColor),
stringHandler: logger.NewKHIFormatLogger(lb, false),
throttle: NewConstantLogThrottle(similarLogThrottlingLogCount),
}
tl := &TaskLogger{
id: tid.String(),
name: tid.String(),
logHandler: th,
logBuffer: lb,
}
logger.RegisterTaskLogger(iid, tid, rid, th)
l.loggers = append(l.loggers, tl)
return tl
} else {
slog.Error("given context is not associated with any run id")
}
} else {
slog.Error("given context is not associated with any inspection id")
}
} else {
slog.Error("given context is not associated with any task id")
}
return nil
}
func NewLogger() *Logger {
return &Logger{
loggers: make([]*TaskLogger, 0),
}
}