Path: blob/main/components/proxy/plugins/logif/plugin.go
2500 views
// Copyright (c) 2021 Gitpod GmbH. All rights reserved.1// Licensed under the GNU Affero General Public License (AGPL).2// See License.AGPL.txt in the project root for license information.34package logif56import (7"bytes"8"encoding/json"9"fmt"10"os"11"strings"12"time"1314"github.com/PaesslerAG/gval"15"github.com/buger/jsonparser"16"github.com/caddyserver/caddy/v2"17"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"18"github.com/caddyserver/caddy/v2/modules/logging"19jsonselect "github.com/gitpod-io/gitpod/proxy/plugins/jsonselect"20"github.com/gitpod-io/gitpod/proxy/plugins/logif/lang"21"go.uber.org/zap"22"go.uber.org/zap/buffer"23"go.uber.org/zap/zapcore"24"golang.org/x/term"25)2627const (28moduleName = "if"29moduleID = "caddy.logging.encoders." + moduleName30)3132func init() {33caddy.RegisterModule(ConditionalEncoder{})34}3536type ConditionalEncoder struct {37zapcore.Encoder `json:"-"`38zapcore.EncoderConfig `json:"-"`3940EncRaw json.RawMessage `json:"encoder,omitempty" caddy:"namespace=caddy.logging.encoders inline_key=format"`41Eval gval.Evaluable `json:"-"`42Expr string43Logger func(...caddy.Module) *zap.Logger `json:"-"`44Formatter string45}4647func (ce ConditionalEncoder) Clone() zapcore.Encoder {48ret := ConditionalEncoder{49Encoder: ce.Encoder.Clone(),50EncoderConfig: ce.EncoderConfig,51Eval: ce.Eval,52Logger: ce.Logger,53Formatter: ce.Formatter,54}55return ret56}5758func (ce ConditionalEncoder) EncodeEntry(e zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) {59// Clone the original encoder to be sure we don't mess up it60enc := ce.Encoder.Clone()6162if ce.Formatter == "console" {63// Add the zap entries to the console encoder64// todo > Set the values according to line_ending, time_format, level_format65// todo > Investigate duration_format too?66enc.AddString(ce.LevelKey, e.Level.String())67enc.AddTime(ce.TimeKey, e.Time)68enc.AddString(ce.NameKey, e.LoggerName)69enc.AddString(ce.MessageKey, e.Message)70// todo > caller, stack71} else if ce.Formatter == "jsonselect" {72// Use the JSON encoder that JSONSelect wraps73jsonEncoder, ok := ce.Encoder.(jsonselect.JSONSelectEncoder)74if !ok {75return nil, fmt.Errorf("unexpected encoder type %T", ce.Encoder)76}77enc = jsonEncoder.Encoder78}7980// Store the logging encoder's buffer81buf, err := enc.EncodeEntry(e, fields)82if err != nil {83return buf, err84}85data := buf.Bytes()8687// Strip non JSON-like prefix from the data buffer when it comes from a non JSON encoder88if pos := bytes.Index(data, []byte(`{"`)); ce.Formatter == "console" && pos != -1 {89data = data[pos:]90}9192// Extract values93values := make(map[string]interface{})94for _, key := range lang.Fields {95path := strings.Split(key, ">")96val, typ, _, err := jsonparser.Get(data, path...)97if err != nil {98// Field not found, ignore the current expression99ce.Logger(&ce).Warn("field not found: please fix or remove it", zap.String("field", key))100continue101}102switch typ {103case jsonparser.NotExist:104// todo > try to reproduce105case jsonparser.Number, jsonparser.String, jsonparser.Boolean:106values[key] = string(val)107default:108// Advice to remove it from the expression109ce.Logger(&ce).Warn("field has an unsupported value type: please fix or remove it", zap.String("field", key), zap.String("type", typ.String()))110}111}112113// Evaluate the expression against values114res, err := lang.Execute(ce.Eval, values)115emit, ok := res.(bool)116if !ok {117ce.Logger(&ce).Error("expecting a boolean expression", zap.String("return", fmt.Sprintf("%T", res)))118goto emitNothing119}120121if emit {122// Using the original (wrapped) encoder for output123return ce.Encoder.EncodeEntry(e, fields)124}125126emitNothing:127buf.Reset()128return buf, nil129}130131func (ConditionalEncoder) CaddyModule() caddy.ModuleInfo {132return caddy.ModuleInfo{133ID: moduleID, // see https://github.com/caddyserver/caddy/blob/ef7f15f3a42474319e2db0dff6720d91c153f0bf/caddyconfig/httpcaddyfile/builtins.go#L720134New: func() caddy.Module {135return new(ConditionalEncoder)136},137}138}139140func (ce *ConditionalEncoder) Provision(ctx caddy.Context) error {141// Store the logger142ce.Logger = ctx.Logger143144if len(ce.Expr) == 0 {145ctx.Logger(ce).Error("must provide an expression")146return nil147}148149if ce.EncRaw == nil {150ce.Encoder, ce.Formatter = newDefaultProductionLogEncoder(true)151152ctx.Logger(ce).Warn("fallback to a default production logging encoder")153return nil154}155156val, err := ctx.LoadModule(ce, "EncRaw")157if err != nil {158return fmt.Errorf("loading fallback encoder module: %v", err)159}160switch v := val.(type) {161case *logging.JSONEncoder:162ce.EncoderConfig = v.LogEncoderConfig.ZapcoreEncoderConfig()163case *logging.ConsoleEncoder:164ce.EncoderConfig = v.LogEncoderConfig.ZapcoreEncoderConfig()165case *jsonselect.JSONSelectEncoder:166ce.EncoderConfig = v.LogEncoderConfig.ZapcoreEncoderConfig()167default:168return fmt.Errorf("unsupported encoder type %T", v)169}170ce.Encoder = val.(zapcore.Encoder)171172eval, err := lang.Compile(ce.Expr)173if err != nil {174return fmt.Errorf(err.Error())175}176ce.Eval = eval177178return nil179}180181func newDefaultProductionLogEncoder(colorize bool) (zapcore.Encoder, string) {182encCfg := zap.NewProductionEncoderConfig()183if term.IsTerminal(int(os.Stdout.Fd())) {184// if interactive terminal, make output more human-readable by default185encCfg.EncodeTime = func(ts time.Time, encoder zapcore.PrimitiveArrayEncoder) {186encoder.AppendString(ts.UTC().Format("2006/01/02 15:04:05.000"))187}188if colorize {189encCfg.EncodeLevel = zapcore.CapitalColorLevelEncoder190}191return zapcore.NewConsoleEncoder(encCfg), "console"192}193return zapcore.NewJSONEncoder(encCfg), "json"194}195196// Interface guards197var (198_ zapcore.Encoder = (*ConditionalEncoder)(nil)199_ caddy.Provisioner = (*ConditionalEncoder)(nil)200_ caddyfile.Unmarshaler = (*ConditionalEncoder)(nil)201)202203204