-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathloglayer.go
More file actions
291 lines (265 loc) · 10.4 KB
/
Copy pathloglayer.go
File metadata and controls
291 lines (265 loc) · 10.4 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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
package loglayer
import (
"context"
"maps"
"sync"
"sync/atomic"
)
// Transport is the interface that all LogLayer transports must implement.
type Transport interface {
// ID returns the unique identifier for this transport.
ID() string
// IsEnabled returns whether the transport is currently active.
IsEnabled() bool
// SendToLogger receives a fully assembled log entry and dispatches it.
// Implementations should perform their own level filtering if needed.
SendToLogger(params TransportParams)
// GetLoggerInstance returns the underlying logger instance, if any.
// Returns nil for transports that have no underlying library.
GetLoggerInstance() any
}
// Schema is the resolved assembly shape for an entry. The core publishes
// it to transports and dispatch-time plugin hooks so they can navigate
// [TransportParams.Data] precisely (e.g. find the error map at
// Schema.ErrorFieldName) and decide their own metadata placement.
//
// All four fields are populated from the matching keys on [Config]:
// FieldsKey, MetadataFieldName, ErrorFieldName, Source.FieldName.
type Schema struct {
// FieldsKey is non-empty when the persistent fields are nested under
// this key inside Data. When empty, fields are merged at root.
FieldsKey string
// MetadataFieldName is non-empty when the entry's metadata should
// nest under this key uniformly (both map and non-map values).
// When empty, each transport applies its default placement policy.
MetadataFieldName string
// ErrorFieldName is the key under which the serialized error map
// lives in Data. Always populated; defaults to "err".
ErrorFieldName string
// SourceFieldName is the key under which call-site source info lives
// in Data when [Config.Source.Enabled] is true. Always populated;
// defaults to "source".
SourceFieldName string
}
// TransportParams is the fully assembled log entry passed to each transport.
//
// Data contains the assembled fields and error.
// Metadata carries the raw value passed to WithMetadata; the transport is
// responsible for serializing it in whatever way suits the underlying library.
type TransportParams struct {
LogLevel LogLevel
Messages []any
// Data holds the assembled persistent fields and error. Nil when no
// fields are set and no error is attached. Use len(Data) > 0 to test.
Data Data
// Metadata is the raw value passed to WithMetadata. May be a struct, map,
// or any other type. Nil if WithMetadata was not called or metadata is muted.
Metadata any
Err error
// Fields is the logger's persistent key/value bag, raw (not yet folded into Data).
Fields Fields
// Ctx is the per-call context.Context attached via WithContext, if any.
// Transports can use it to extract trace IDs, span context, deadlines, etc.
// Nil when no Go context was attached.
Ctx context.Context
// Groups holds the entry's group tags (persistent WithGroup first,
// per-call WithGroup appended, deduped). Nil when no groups apply.
// Routing has already consumed the slice before this point; it's
// exposed so transports can include it in the wire payload.
Groups []string
// Schema is the resolved assembly shape (FieldsKey, MetadataFieldName,
// ErrorFieldName, SourceFieldName). Use it to navigate Data and to
// decide metadata placement.
Schema Schema
// Prefix is the value attached via WithPrefix on the emitting
// logger (or set on Config.Prefix at construction), exposed
// verbatim so transports can render it independently from the
// message (e.g. tinted differently, emitted as a structured
// field, rendered in its own column).
//
// Empty string when no prefix was set.
//
// Transports that want a "prefix folded into Messages[0]"
// rendering call [transport.JoinPrefixAndMessages] at the top
// of SendToLogger; the helper has fast-path early returns for
// the no-prefix case, so the per-call cost on a logger that
// hasn't called WithPrefix is one string compare.
Prefix string
}
// transportSet is an immutable snapshot of the transport list and the
// id-keyed lookup. New snapshots are published atomically by the mutators;
// the dispatch path loads the current snapshot and iterates without locking.
type transportSet struct {
list []Transport
byID map[string]Transport
}
// LogLayer is the central logger. It assembles log entries from fields, metadata,
// and error data, then dispatches them to one or more transports.
type LogLayer struct {
config Config
fields Fields
levels *levelState
transports atomic.Pointer[transportSet]
plugins atomic.Pointer[pluginSet]
groups atomic.Pointer[groupSet]
muteFields atomic.Bool
muteMetadata atomic.Bool
hasLazyFields atomic.Bool
// assignedGroups are the persistent group tags applied by WithGroup
// on this logger. Per-call WithGroup on a builder merges with these.
// Set only between Child() and the WithGroup call that produced this
// logger; never mutated post-publish, so the dispatch path can read
// it without synchronization.
assignedGroups []string
// boundCtx is the persistent context.Context applied by WithContext on
// this logger. Per-call WithContext on a builder overrides it for that
// emission. Set only between Child() and the WithContext call that
// produced this logger; never mutated post-publish, so the dispatch
// path can read it without synchronization.
boundCtx context.Context
// prefix is the WithPrefix value for this logger, propagated to
// every emission via TransportParams.Prefix and the dispatch-
// time plugin hook param structs. Transports decide how to
// render it; most call transport.JoinPrefixAndMessages to
// fold it into the first message string. Initialized from
// Config.Prefix at build time; WithPrefix mutates this field
// on a fresh child only. Same lifecycle as assignedGroups /
// boundCtx: never mutated post-publish.
prefix string
// txMu serializes transport mutators (AddTransport / RemoveTransport /
// SetTransports) so two concurrent admin operations on the same
// logger don't lose updates. The dispatch path doesn't take this lock;
// it just Loads the current snapshot.
txMu sync.Mutex
// pluginMu serializes plugin mutators (AddPlugin / RemovePlugin); same
// pattern as txMu.
pluginMu sync.Mutex
// groupMu serializes group mutators; same pattern as txMu.
groupMu sync.Mutex
}
// New creates a new LogLayer from the given Config.
//
// Panics if no transport is provided. For applications that prefer explicit
// error handling on misconfiguration, use Build instead.
func New(config Config) *LogLayer {
l, err := build(config)
if err != nil {
panic(err)
}
return l
}
// Build creates a new LogLayer from the given Config, returning an error
// instead of panicking if the configuration is invalid (e.g. no transport).
//
// Use New for the more concise idiom when misconfiguration is a programmer
// error (the typical case for application setup).
func Build(config Config) (*LogLayer, error) {
return build(config)
}
func build(config Config) (*LogLayer, error) {
if config.Transport != nil && len(config.Transports) > 0 {
return nil, ErrTransportAndTransports
}
all := config.Transports
if config.Transport != nil {
all = []Transport{config.Transport}
}
if len(all) == 0 {
return nil, ErrNoTransport
}
l := &LogLayer{
config: config,
fields: make(Fields),
levels: newLevelState(),
prefix: config.Prefix,
}
if config.ErrorFieldName == "" {
l.config.ErrorFieldName = "err"
}
if config.Source.FieldName == "" {
l.config.Source.FieldName = "source"
}
if config.Disabled {
l.levels.setMaster(false)
}
l.muteFields.Store(config.MuteFields)
l.muteMetadata.Store(config.MuteMetadata)
l.transports.Store(newTransportSet(all))
l.plugins.Store(newPluginSet(append([]Plugin(nil), config.Plugins...)))
ung := config.Routing.Ungrouped
if ung.Mode != UngroupedToTransports && len(ung.Transports) > 0 {
return nil, ErrUngroupedTransportsWithoutMode
}
l.groups.Store(newGroupSet(config.Routing.Groups, config.Routing.ActiveGroups, ung))
return l, nil
}
// newTransportSet builds an immutable transportSet snapshot. Caller must
// ensure all is non-empty.
func newTransportSet(all []Transport) *transportSet {
set := &transportSet{
list: all,
byID: make(map[string]Transport, len(all)),
}
for _, t := range all {
set.byID[t.ID()] = t
}
return set
}
// publishTransports validates and atomically swaps in a new transport set.
// Used by every mutator after building the new slice. Panics if all is empty.
func (l *LogLayer) publishTransports(all []Transport) {
if len(all) == 0 {
panic(ErrNoTransport)
}
l.transports.Store(newTransportSet(all))
}
// loadTransports returns the current transport snapshot. Hot path: called on
// every emission.
func (l *LogLayer) loadTransports() *transportSet {
return l.transports.Load()
}
// Child creates a new LogLayer that inherits the current config, fields (shallow copy),
// level state, transports, plugins, and group routing. Changes to the
// child do not affect the parent.
func (l *LogLayer) Child() *LogLayer {
child := &LogLayer{
config: l.config,
fields: copyFields(l.fields),
levels: l.levels.clone(),
assignedGroups: l.assignedGroups,
boundCtx: l.boundCtx,
prefix: l.prefix,
}
child.muteFields.Store(l.muteFields.Load())
child.muteMetadata.Store(l.muteMetadata.Load())
child.hasLazyFields.Store(l.hasLazyFields.Load())
// transportSet, pluginSet, and groupSet are immutable; mutators publish
// new sets via copy-on-write, so the child can share the parent's
// snapshots until either side mutates. assignedGroups is also immutable
// post-publish (only WithGroup writes it, and only on a fresh child
// before returning), so it can also be shared by reference.
child.transports.Store(l.loadTransports())
child.plugins.Store(l.loadPlugins())
child.groups.Store(l.loadGroups())
return child
}
// WithPrefix creates a child logger with the given prefix prepended to
// every message. The receiver is unchanged.
func (l *LogLayer) WithPrefix(prefix string) *LogLayer {
child := l.Child()
child.prefix = prefix
return child
}
// copyFields returns a shallow copy of src, or nil when src is empty.
// Returning nil saves an allocation per Child() call on loggers that
// haven't accumulated any fields yet (the dominant case for fresh
// per-request loggers built via middleware). Callers that need to write
// must allocate the destination map themselves.
func copyFields(src Fields) Fields {
if len(src) == 0 {
return nil
}
dst := make(Fields, len(src))
maps.Copy(dst, src)
return dst
}