Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/proxy/plugins/logif/plugin.go
2500 views
1
// Copyright (c) 2021 Gitpod GmbH. All rights reserved.
2
// Licensed under the GNU Affero General Public License (AGPL).
3
// See License.AGPL.txt in the project root for license information.
4
5
package logif
6
7
import (
8
"bytes"
9
"encoding/json"
10
"fmt"
11
"os"
12
"strings"
13
"time"
14
15
"github.com/PaesslerAG/gval"
16
"github.com/buger/jsonparser"
17
"github.com/caddyserver/caddy/v2"
18
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
19
"github.com/caddyserver/caddy/v2/modules/logging"
20
jsonselect "github.com/gitpod-io/gitpod/proxy/plugins/jsonselect"
21
"github.com/gitpod-io/gitpod/proxy/plugins/logif/lang"
22
"go.uber.org/zap"
23
"go.uber.org/zap/buffer"
24
"go.uber.org/zap/zapcore"
25
"golang.org/x/term"
26
)
27
28
const (
29
moduleName = "if"
30
moduleID = "caddy.logging.encoders." + moduleName
31
)
32
33
func init() {
34
caddy.RegisterModule(ConditionalEncoder{})
35
}
36
37
type ConditionalEncoder struct {
38
zapcore.Encoder `json:"-"`
39
zapcore.EncoderConfig `json:"-"`
40
41
EncRaw json.RawMessage `json:"encoder,omitempty" caddy:"namespace=caddy.logging.encoders inline_key=format"`
42
Eval gval.Evaluable `json:"-"`
43
Expr string
44
Logger func(...caddy.Module) *zap.Logger `json:"-"`
45
Formatter string
46
}
47
48
func (ce ConditionalEncoder) Clone() zapcore.Encoder {
49
ret := ConditionalEncoder{
50
Encoder: ce.Encoder.Clone(),
51
EncoderConfig: ce.EncoderConfig,
52
Eval: ce.Eval,
53
Logger: ce.Logger,
54
Formatter: ce.Formatter,
55
}
56
return ret
57
}
58
59
func (ce ConditionalEncoder) EncodeEntry(e zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) {
60
// Clone the original encoder to be sure we don't mess up it
61
enc := ce.Encoder.Clone()
62
63
if ce.Formatter == "console" {
64
// Add the zap entries to the console encoder
65
// todo > Set the values according to line_ending, time_format, level_format
66
// todo > Investigate duration_format too?
67
enc.AddString(ce.LevelKey, e.Level.String())
68
enc.AddTime(ce.TimeKey, e.Time)
69
enc.AddString(ce.NameKey, e.LoggerName)
70
enc.AddString(ce.MessageKey, e.Message)
71
// todo > caller, stack
72
} else if ce.Formatter == "jsonselect" {
73
// Use the JSON encoder that JSONSelect wraps
74
jsonEncoder, ok := ce.Encoder.(jsonselect.JSONSelectEncoder)
75
if !ok {
76
return nil, fmt.Errorf("unexpected encoder type %T", ce.Encoder)
77
}
78
enc = jsonEncoder.Encoder
79
}
80
81
// Store the logging encoder's buffer
82
buf, err := enc.EncodeEntry(e, fields)
83
if err != nil {
84
return buf, err
85
}
86
data := buf.Bytes()
87
88
// Strip non JSON-like prefix from the data buffer when it comes from a non JSON encoder
89
if pos := bytes.Index(data, []byte(`{"`)); ce.Formatter == "console" && pos != -1 {
90
data = data[pos:]
91
}
92
93
// Extract values
94
values := make(map[string]interface{})
95
for _, key := range lang.Fields {
96
path := strings.Split(key, ">")
97
val, typ, _, err := jsonparser.Get(data, path...)
98
if err != nil {
99
// Field not found, ignore the current expression
100
ce.Logger(&ce).Warn("field not found: please fix or remove it", zap.String("field", key))
101
continue
102
}
103
switch typ {
104
case jsonparser.NotExist:
105
// todo > try to reproduce
106
case jsonparser.Number, jsonparser.String, jsonparser.Boolean:
107
values[key] = string(val)
108
default:
109
// Advice to remove it from the expression
110
ce.Logger(&ce).Warn("field has an unsupported value type: please fix or remove it", zap.String("field", key), zap.String("type", typ.String()))
111
}
112
}
113
114
// Evaluate the expression against values
115
res, err := lang.Execute(ce.Eval, values)
116
emit, ok := res.(bool)
117
if !ok {
118
ce.Logger(&ce).Error("expecting a boolean expression", zap.String("return", fmt.Sprintf("%T", res)))
119
goto emitNothing
120
}
121
122
if emit {
123
// Using the original (wrapped) encoder for output
124
return ce.Encoder.EncodeEntry(e, fields)
125
}
126
127
emitNothing:
128
buf.Reset()
129
return buf, nil
130
}
131
132
func (ConditionalEncoder) CaddyModule() caddy.ModuleInfo {
133
return caddy.ModuleInfo{
134
ID: moduleID, // see https://github.com/caddyserver/caddy/blob/ef7f15f3a42474319e2db0dff6720d91c153f0bf/caddyconfig/httpcaddyfile/builtins.go#L720
135
New: func() caddy.Module {
136
return new(ConditionalEncoder)
137
},
138
}
139
}
140
141
func (ce *ConditionalEncoder) Provision(ctx caddy.Context) error {
142
// Store the logger
143
ce.Logger = ctx.Logger
144
145
if len(ce.Expr) == 0 {
146
ctx.Logger(ce).Error("must provide an expression")
147
return nil
148
}
149
150
if ce.EncRaw == nil {
151
ce.Encoder, ce.Formatter = newDefaultProductionLogEncoder(true)
152
153
ctx.Logger(ce).Warn("fallback to a default production logging encoder")
154
return nil
155
}
156
157
val, err := ctx.LoadModule(ce, "EncRaw")
158
if err != nil {
159
return fmt.Errorf("loading fallback encoder module: %v", err)
160
}
161
switch v := val.(type) {
162
case *logging.JSONEncoder:
163
ce.EncoderConfig = v.LogEncoderConfig.ZapcoreEncoderConfig()
164
case *logging.ConsoleEncoder:
165
ce.EncoderConfig = v.LogEncoderConfig.ZapcoreEncoderConfig()
166
case *jsonselect.JSONSelectEncoder:
167
ce.EncoderConfig = v.LogEncoderConfig.ZapcoreEncoderConfig()
168
default:
169
return fmt.Errorf("unsupported encoder type %T", v)
170
}
171
ce.Encoder = val.(zapcore.Encoder)
172
173
eval, err := lang.Compile(ce.Expr)
174
if err != nil {
175
return fmt.Errorf(err.Error())
176
}
177
ce.Eval = eval
178
179
return nil
180
}
181
182
func newDefaultProductionLogEncoder(colorize bool) (zapcore.Encoder, string) {
183
encCfg := zap.NewProductionEncoderConfig()
184
if term.IsTerminal(int(os.Stdout.Fd())) {
185
// if interactive terminal, make output more human-readable by default
186
encCfg.EncodeTime = func(ts time.Time, encoder zapcore.PrimitiveArrayEncoder) {
187
encoder.AppendString(ts.UTC().Format("2006/01/02 15:04:05.000"))
188
}
189
if colorize {
190
encCfg.EncodeLevel = zapcore.CapitalColorLevelEncoder
191
}
192
return zapcore.NewConsoleEncoder(encCfg), "console"
193
}
194
return zapcore.NewJSONEncoder(encCfg), "json"
195
}
196
197
// Interface guards
198
var (
199
_ zapcore.Encoder = (*ConditionalEncoder)(nil)
200
_ caddy.Provisioner = (*ConditionalEncoder)(nil)
201
_ caddyfile.Unmarshaler = (*ConditionalEncoder)(nil)
202
)
203
204