diff --git a/pkg/log/zap/field.go b/pkg/log/zap/field.go
new file mode 100644
index 0000000000000000000000000000000000000000..77ddf563646e6e91cf48966a24a632fe7a89475a
--- /dev/null
+++ b/pkg/log/zap/field.go
@@ -0,0 +1,56 @@
+package zap
+
+import (
+	"context"
+	"fmt"
+
+	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
+)
+
+const unknownCaller = "unknown"
+
+func Category(category string) zapcore.Field {
+	return zap.String("category", category)
+}
+
+func Component(component string) zapcore.Field {
+	return zap.String("component", component)
+}
+
+func Event(event string) zapcore.Field {
+	return zap.String("event", event)
+}
+
+func Object(v any) zapcore.Field {
+	switch object := v.(type) {
+	case string:
+		return zap.String("object", object)
+	case fmt.Stringer:
+		return zap.String("object", object.String())
+	default:
+		return zap.Any("object", object)
+	}
+}
+
+type callerKey struct{}
+
+func ContextWithCaller(parent context.Context, caller string) context.Context {
+	return context.WithValue(parent, callerKey{}, caller)
+}
+
+func CallerFromContext(ctx context.Context) zapcore.Field {
+	caller, ok := ctx.Value(callerKey{}).(string)
+	if !ok {
+		caller = unknownCaller
+	}
+	return zap.String("caller", caller)
+}
+
+func Attr(attr any) zapcore.Field {
+	return zap.Any("attr", attr)
+}
+
+func Tags(tags ...string) zapcore.Field {
+	return zap.Strings("tags", tags)
+}