-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathtruncate.go
More file actions
103 lines (93 loc) · 3.6 KB
/
truncate.go
File metadata and controls
103 lines (93 loc) · 3.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
package slogjson
import (
jsonv1 "encoding/json"
"fmt"
"log/slog"
"unicode/utf8"
jsonv2 "github.com/go-json-experiment/json"
jsonv2text "github.com/go-json-experiment/json/jsontext"
)
// ReplaceAttrTruncate is a replacement function that examines attributes before
// they are logged and if necessary truncates them.
// AWS Cloudwatch has a limit of 256kb, GCP Stackdriver is 100kb, Azure is 32kb total and 8kb per
// field, docker is 16kb, some Java based systems have a max of 8221.
// Since there can be multiple fields, and it is a lot harder to control total length, set the field
// length a bit shorter.
func ReplaceAttrTruncate(maxLogFieldLength int, jsonOptions jsonv2.Options) func(group []string, a slog.Attr) slog.Attr {
// Add a default marshaler for all `error` types
jsonOptions = appendErrorMarshaler(jsonOptions)
return func(group []string, a slog.Attr) slog.Attr {
switch a.Value.Kind() {
case slog.KindString:
// Truncate strings
if s := a.Value.String(); len(s) > maxLogFieldLength {
return slog.String(a.Key, fmt.Sprintf("replaced: true; original_length: %d; truncated: %s", len(s), truncateByBytes(s, maxLogFieldLength)))
}
case slog.KindAny:
value := a.Value.Any()
if value == nil {
return a
}
// Convert []byte to a string for readability and truncate:
if b, ok := value.([]byte); ok {
if len(b) > maxLogFieldLength {
return slog.String(a.Key, fmt.Sprintf("replaced: true; original_length: %d; truncated: %s", len(b), truncateByBytes(string(b), maxLogFieldLength)))
}
return slog.String(a.Key, string(b)) // []byte's are unreadable, so cast to string
}
// Now we want to do 2 things:
// * Use any custom or third party marshallers defined in the json options.
// Example: convert protobuf messages to JSON using the canonical protobuf<->json spec
// (because otherwise things like timestamps get turned into garbage).
// Even if the protobuf is nested inside a struct or a slice ([]any, []*Proto, etc).
// This is difficult because any slice type could contain a protobuf nested in it.
// * Truncate any large structs, slices, strings, etc.
//
// In order to accomplish the above in the most flexible manner, we will pre-marshal the
// value into a RawMessage([]byte), using the same marshaller options our slog handler
// is using, then truncate if necessary.
sjson, err := jsonv2.Marshal(value, jsonOptions)
if err == nil {
a = slog.Any(a.Key, jsonv1.RawMessage(sjson))
}
// Truncate really long raw json and []byte's
switch b := a.Value.Any().(type) {
// TODO: see if there is an existing way to truncate json while keeping it valid json
case jsonv1.RawMessage:
if len(b) > maxLogFieldLength {
return slog.Any(a.Key, replaced{
Replaced: true,
Length: len(b),
// Annoying to have it escaped and embedded, but can still be read if needed
Truncated: truncateByBytes(string(b), maxLogFieldLength),
})
}
case jsonv2text.Value:
if len(b) > maxLogFieldLength {
return slog.Any(a.Key, replaced{
Replaced: true,
Length: len(b),
Truncated: truncateByBytes(string(b), maxLogFieldLength),
})
}
}
}
return a
}
}
type replaced struct {
Replaced bool `json:"replaced"`
Length int `json:"length"`
Truncated string `json:"truncated"`
}
// truncateByBytes truncates based on the number of bytes, making sure to cut
// the string before the start of any multi-byte unicode characters.
func truncateByBytes(s string, n int) string {
if len(s) <= n {
return s
}
for n > 0 && !utf8.RuneStart(s[n]) {
n--
}
return s[:n]
}