Skip to content

Commit 0ab0d5a

Browse files
authored
zapcore: Add PreWriteHook for transforming entries before write (#1534)
This adds a new CheckPreWriteHook type and a Before method on CheckedEntry that allows transforming the Entry and Fields before they are written to any registered Cores. The existing CheckWriteHook (set via After/WithFatalHook) only runs *after* cores have already written the entry, which means it cannot modify what gets logged. PreWriteHook fills that gap: it runs before any Core.Write call, allowing callers to enrich, redact, or reshape log entries and fields in a composable way. Multiple pre-write hooks run in the order they were added. Like other CheckedEntry methods, Before is safe to call on nil references.
1 parent d278c59 commit 0ab0d5a

2 files changed

Lines changed: 110 additions & 1 deletion

File tree

zapcore/entry.go

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,14 @@ func (a CheckWriteAction) OnWrite(ce *CheckedEntry, _ []Field) {
201201

202202
var _ CheckWriteHook = CheckWriteAction(0)
203203

204+
// CheckPreWriteHook is a function that transforms an Entry and its Fields
205+
// before they are written to cores. Register one on a CheckedEntry with the
206+
// Before method.
207+
//
208+
// Pre-write hooks run in the order they were added, before any Core's Write
209+
// method is called. They may modify the Entry and Fields freely.
210+
type CheckPreWriteHook func(Entry, []Field) (Entry, []Field)
211+
204212
// CheckedEntry is an Entry together with a collection of Cores that have
205213
// already agreed to log it.
206214
//
@@ -213,6 +221,7 @@ type CheckedEntry struct {
213221
dirty bool // best-effort detection of pool misuse
214222
after CheckWriteHook
215223
cores []Core
224+
before []CheckPreWriteHook
216225
}
217226

218227
func (ce *CheckedEntry) reset() {
@@ -225,6 +234,10 @@ func (ce *CheckedEntry) reset() {
225234
ce.cores[i] = nil
226235
}
227236
ce.cores = ce.cores[:0]
237+
for i := range ce.before {
238+
ce.before[i] = nil
239+
}
240+
ce.before = ce.before[:0]
228241
}
229242

230243
// Write writes the entry to the stored Cores, returns any errors, and returns
@@ -253,9 +266,14 @@ func (ce *CheckedEntry) Write(fields ...Field) {
253266
}
254267
ce.dirty = true
255268

269+
ent := ce.Entry
270+
for i := range ce.before {
271+
ent, fields = ce.before[i](ent, fields)
272+
}
273+
256274
var err error
257275
for i := range ce.cores {
258-
err = multierr.Append(err, ce.cores[i].Write(ce.Entry, fields))
276+
err = multierr.Append(err, ce.cores[i].Write(ent, fields))
259277
}
260278
if err != nil && ce.ErrorOutput != nil {
261279
_, _ = fmt.Fprintf(
@@ -295,6 +313,18 @@ func (ce *CheckedEntry) Should(ent Entry, should CheckWriteAction) *CheckedEntry
295313
return ce.After(ent, should)
296314
}
297315

316+
// Before adds a pre-write hook that transforms the Entry and Fields before
317+
// they are written to any registered Cores. Multiple hooks run in the order
318+
// they were added. It's safe to call this on nil CheckedEntry references.
319+
func (ce *CheckedEntry) Before(ent Entry, hook CheckPreWriteHook) *CheckedEntry {
320+
if ce == nil {
321+
ce = getCheckedEntry()
322+
ce.Entry = ent
323+
}
324+
ce.before = append(ce.before, hook)
325+
return ce
326+
}
327+
298328
// After sets this CheckEntry's CheckWriteHook, which will be called after this
299329
// log entry has been written. It's safe to call this on nil CheckedEntry
300330
// references.

zapcore/entry_test.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,3 +147,82 @@ type customHook struct {
147147
func (c *customHook) OnWrite(_ *CheckedEntry, _ []Field) {
148148
c.called = true
149149
}
150+
151+
func TestCheckedEntryBefore(t *testing.T) {
152+
t.Run("nil is safe", func(t *testing.T) {
153+
var ce *CheckedEntry
154+
ce = ce.Before(Entry{Message: "hello"}, func(ent Entry, fields []Field) (Entry, []Field) {
155+
ent.Message = "modified"
156+
return ent, fields
157+
})
158+
assert.NotNil(t, ce)
159+
assert.Equal(t, "hello", ce.Entry.Message)
160+
})
161+
162+
t.Run("modifies entry and fields", func(t *testing.T) {
163+
core := &recordingCore{}
164+
var ce *CheckedEntry
165+
ce = ce.AddCore(Entry{Message: "original"}, core)
166+
ce = ce.Before(Entry{}, func(ent Entry, fields []Field) (Entry, []Field) {
167+
ent.Message = "modified"
168+
fields = append(fields, Field{Key: "added", Type: StringType, String: "value"})
169+
return ent, fields
170+
})
171+
ce.Write(Field{Key: "initial", Type: StringType, String: "v"})
172+
173+
assert.Equal(t, "modified", core.entry.Message)
174+
assert.Len(t, core.fields, 2)
175+
assert.Equal(t, "initial", core.fields[0].Key)
176+
assert.Equal(t, "added", core.fields[1].Key)
177+
})
178+
179+
t.Run("multiple hooks chain in order", func(t *testing.T) {
180+
core := &recordingCore{}
181+
var ce *CheckedEntry
182+
ce = ce.AddCore(Entry{Message: "start"}, core)
183+
ce = ce.Before(Entry{}, func(ent Entry, fields []Field) (Entry, []Field) {
184+
ent.Message = ent.Message + "-first"
185+
return ent, fields
186+
})
187+
ce = ce.Before(Entry{}, func(ent Entry, fields []Field) (Entry, []Field) {
188+
ent.Message = ent.Message + "-second"
189+
return ent, fields
190+
})
191+
ce.Write()
192+
193+
assert.Equal(t, "start-first-second", core.entry.Message)
194+
})
195+
196+
t.Run("hooks reset on pool reuse", func(t *testing.T) {
197+
core := &recordingCore{}
198+
var ce *CheckedEntry
199+
ce = ce.AddCore(Entry{Message: "first"}, core)
200+
ce = ce.Before(Entry{}, func(ent Entry, fields []Field) (Entry, []Field) {
201+
ent.Message = "hooked"
202+
return ent, fields
203+
})
204+
ce.Write()
205+
assert.Equal(t, "hooked", core.entry.Message)
206+
207+
// Get a new entry from the pool — hooks should be cleared.
208+
ce2 := getCheckedEntry()
209+
assert.Empty(t, ce2.before)
210+
putCheckedEntry(ce2)
211+
})
212+
}
213+
214+
// recordingCore captures the last entry and fields written to it.
215+
type recordingCore struct {
216+
entry Entry
217+
fields []Field
218+
}
219+
220+
func (c *recordingCore) Enabled(Level) bool { return true }
221+
func (c *recordingCore) With([]Field) Core { return c }
222+
func (c *recordingCore) Check(ent Entry, ce *CheckedEntry) *CheckedEntry { return ce.AddCore(ent, c) }
223+
func (c *recordingCore) Sync() error { return nil }
224+
func (c *recordingCore) Write(ent Entry, fields []Field) error {
225+
c.entry = ent
226+
c.fields = fields
227+
return nil
228+
}

0 commit comments

Comments
 (0)