diff --git a/go.mod b/go.mod index cc5995ee..b87dae83 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.23.0 toolchain go1.23.8 require ( - github.com/sirupsen/logrus v1.9.3 golang.org/x/sys v0.35.0 golang.org/x/tools v0.36.0 ) diff --git a/go.sum b/go.sum index 1608729c..832412eb 100644 --- a/go.sum +++ b/go.sum @@ -1,24 +1,10 @@ -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/etwlogrus/hook.go b/pkg/etwlogrus/hook.go deleted file mode 100644 index 71f658ed..00000000 --- a/pkg/etwlogrus/hook.go +++ /dev/null @@ -1,157 +0,0 @@ -//go:build windows - -package etwlogrus - -import ( - "errors" - "sort" - - "github.com/sirupsen/logrus" - - "github.com/Microsoft/go-winio/pkg/etw" -) - -const defaultEventName = "LogrusEntry" - -// ErrNoProvider is returned when a hook is created without a provider being configured. -var ErrNoProvider = errors.New("no ETW registered provider") - -// HookOpt is an option to change the behavior of the Logrus ETW hook. -type HookOpt func(*Hook) error - -// Hook is a Logrus hook which logs received events to ETW. -type Hook struct { - provider *etw.Provider - closeProvider bool - // allows setting the entry name - getName func(*logrus.Entry) string - // returns additional options to add to the event - getEventsOpts func(*logrus.Entry) []etw.EventOpt -} - -// NewHook registers a new ETW provider and returns a hook to log from it. -// The provider will be closed when the hook is closed. -func NewHook(providerName string, opts ...HookOpt) (*Hook, error) { - opts = append(opts, WithNewETWProvider(providerName)) - - return NewHookFromOpts(opts...) -} - -// NewHookFromProvider creates a new hook based on an existing ETW provider. -// The provider will not be closed when the hook is closed. -func NewHookFromProvider(provider *etw.Provider, opts ...HookOpt) (*Hook, error) { - opts = append(opts, WithExistingETWProvider(provider)) - - return NewHookFromOpts(opts...) -} - -// NewHookFromOpts creates a new hook with the provided options. -// An error is returned if the hook does not have a valid provider. -func NewHookFromOpts(opts ...HookOpt) (*Hook, error) { - h := defaultHook() - - for _, o := range opts { - if err := o(h); err != nil { - return nil, err - } - } - return h, h.validate() -} - -func defaultHook() *Hook { - h := &Hook{} - return h -} - -func (h *Hook) validate() error { - if h.provider == nil { - return ErrNoProvider - } - return nil -} - -// Levels returns the set of levels that this hook wants to receive log entries -// for. -func (*Hook) Levels() []logrus.Level { - return logrus.AllLevels -} - -var logrusToETWLevelMap = map[logrus.Level]etw.Level{ - logrus.PanicLevel: etw.LevelAlways, - logrus.FatalLevel: etw.LevelCritical, - logrus.ErrorLevel: etw.LevelError, - logrus.WarnLevel: etw.LevelWarning, - logrus.InfoLevel: etw.LevelInfo, - logrus.DebugLevel: etw.LevelVerbose, - logrus.TraceLevel: etw.LevelVerbose, -} - -// Fire receives each Logrus entry as it is logged, and logs it to ETW. -func (h *Hook) Fire(e *logrus.Entry) error { - // Logrus defines more levels than ETW typically uses, but analysis is - // easiest when using a consistent set of levels across ETW providers, so we - // map the Logrus levels to ETW levels. - level := logrusToETWLevelMap[e.Level] - if !h.provider.IsEnabledForLevel(level) { - return nil - } - - name := defaultEventName - if h.getName != nil { - if n := h.getName(e); n != "" { - name = n - } - } - - // extra room for two more options in addition to log level to avoid repeated reallocations - // if the user also provides options - opts := make([]etw.EventOpt, 0, 3) - opts = append(opts, etw.WithLevel(level)) - if h.getEventsOpts != nil { - opts = append(opts, h.getEventsOpts(e)...) - } - - // Sort the fields by name so they are consistent in each instance - // of an event. Otherwise, the fields don't line up in WPA. - names := make([]string, 0, len(e.Data)) - hasError := false - for k := range e.Data { - if k == logrus.ErrorKey { - // Always put the error last because it is optional in some events. - hasError = true - } else { - names = append(names, k) - } - } - sort.Strings(names) - - // Reserve extra space for the message and time fields. - fields := make([]etw.FieldOpt, 0, len(e.Data)+2) - fields = append(fields, etw.StringField("Message", e.Message)) - fields = append(fields, etw.Time("Time", e.Time)) - for _, k := range names { - fields = append(fields, etw.SmartField(k, e.Data[k])) - } - if hasError { - fields = append(fields, etw.SmartField(logrus.ErrorKey, e.Data[logrus.ErrorKey])) - } - - // Firing an ETW event is essentially best effort, as the event write can - // fail for reasons completely out of the control of the event writer (such - // as a session listening for the event having no available space in its - // buffers). Therefore, we don't return the error from WriteEvent, as it is - // just noise in many cases. - _ = h.provider.WriteEvent(name, opts, fields) - - return nil -} - -// Close cleans up the hook and closes the ETW provider. If the provder was -// registered by etwlogrus, it will be closed as part of `Close`. If the -// provider was passed in, it will not be closed. -func (h *Hook) Close() error { - if h.closeProvider { - return h.provider.Close() - } - return nil -} diff --git a/pkg/etwlogrus/hook_test.go b/pkg/etwlogrus/hook_test.go deleted file mode 100644 index 3451311a..00000000 --- a/pkg/etwlogrus/hook_test.go +++ /dev/null @@ -1,99 +0,0 @@ -//go:build windows - -package etwlogrus - -import ( - "testing" - - "github.com/sirupsen/logrus" -) - -func fireEvent(name string, value interface{}) { - logrus.WithField("Field", value).Info(name) -} - -// The purpose of this test is to log lots of different field types, to test the -// logic that converts them to ETW. Because we don't have a way to -// programatically validate the ETW events, this test has two main purposes: (1) -// validate nothing causes a panic while logging (2) allow manual validation that -// the data is logged correctly (through a tool like WPA). -func TestFieldLogging(t *testing.T) { - // Sample WPRP to collect this provider is included in HookTest.wprp. - // - // Start collection: - // wpr -start HookTest.wprp -filemode - // - // Stop collection: - // wpr -stop HookTest.etl - h, err := NewHook("HookTest") - if err != nil { - t.Fatal(err) - } - logrus.AddHook(h) - - fireEvent("Bool", true) - fireEvent("BoolSlice", []bool{true, false, true}) - fireEvent("EmptyBoolSlice", []bool{}) - fireEvent("String", "teststring") - fireEvent("StringSlice", []string{"sstr1", "sstr2", "sstr3"}) - fireEvent("EmptyStringSlice", []string{}) - fireEvent("Int", int(1)) - fireEvent("IntSlice", []int{2, 3, 4}) - fireEvent("EmptyIntSlice", []int{}) - fireEvent("Int8", int8(5)) - fireEvent("Int8Slice", []int8{6, 7, 8}) - fireEvent("EmptyInt8Slice", []int8{}) - fireEvent("Int16", int16(9)) - fireEvent("Int16Slice", []int16{10, 11, 12}) - fireEvent("EmptyInt16Slice", []int16{}) - fireEvent("Int32", int32(13)) - fireEvent("Int32Slice", []int32{14, 15, 16}) - fireEvent("EmptyInt32Slice", []int32{}) - fireEvent("Int64", int64(17)) - fireEvent("Int64Slice", []int64{18, 19, 20}) - fireEvent("EmptyInt64Slice", []int64{}) - fireEvent("Uint", uint(21)) - fireEvent("UintSlice", []uint{22, 23, 24}) - fireEvent("EmptyUintSlice", []uint{}) - fireEvent("Uint8", uint8(25)) - fireEvent("Uint8Slice", []uint8{26, 27, 28}) - fireEvent("EmptyUint8Slice", []uint8{}) - fireEvent("Uint16", uint16(29)) - fireEvent("Uint16Slice", []uint16{30, 31, 32}) - fireEvent("EmptyUint16Slice", []uint16{}) - fireEvent("Uint32", uint32(33)) - fireEvent("Uint32Slice", []uint32{34, 35, 36}) - fireEvent("EmptyUint32Slice", []uint32{}) - fireEvent("Uint64", uint64(37)) - fireEvent("Uint64Slice", []uint64{38, 39, 40}) - fireEvent("EmptyUint64Slice", []uint64{}) - fireEvent("Uintptr", uintptr(41)) - fireEvent("UintptrSlice", []uintptr{42, 43, 44}) - fireEvent("EmptyUintptrSlice", []uintptr{}) - fireEvent("Float32", float32(45.46)) - fireEvent("Float32Slice", []float32{47.48, 49.50, 51.52}) - fireEvent("EmptyFloat32Slice", []float32{}) - fireEvent("Float64", float64(53.54)) - fireEvent("Float64Slice", []float64{55.56, 57.58, 59.60}) - fireEvent("EmptyFloat64Slice", []float64{}) - - type struct1 struct { - A float32 - priv int - B []uint - } - type struct2 struct { - A int - B int - } - type struct3 struct { - struct2 - A int - B string - priv string - C struct1 - D uint16 - } - // Unexported fields, and fields in embedded structs, should not log. - fireEvent("Struct", struct3{struct2{-1, -2}, 1, "2s", "-3s", struct1{3.4, -4, []uint{5, 6, 7}}, 8}) -} diff --git a/pkg/etwlogrus/opts.go b/pkg/etwlogrus/opts.go deleted file mode 100644 index 499fca87..00000000 --- a/pkg/etwlogrus/opts.go +++ /dev/null @@ -1,53 +0,0 @@ -//go:build windows - -package etwlogrus - -import ( - "github.com/sirupsen/logrus" - - "github.com/Microsoft/go-winio/pkg/etw" -) - -// etw provider - -// WithNewETWProvider registers a new ETW provider and sets the hook to log using it. -// The provider will be closed when the hook is closed. -func WithNewETWProvider(n string) HookOpt { - return func(h *Hook) error { - provider, err := etw.NewProvider(n, nil) - if err != nil { - return err - } - - h.provider = provider - h.closeProvider = true - return nil - } -} - -// WithExistingETWProvider configures the hook to use an existing ETW provider. -// The provider will not be closed when the hook is closed. -func WithExistingETWProvider(p *etw.Provider) HookOpt { - return func(h *Hook) error { - h.provider = p - h.closeProvider = false - return nil - } -} - -// WithGetName sets the ETW EventName of an event to the value returned by f -// If the name is empty, the default event name will be used. -func WithGetName(f func(*logrus.Entry) string) HookOpt { - return func(h *Hook) error { - h.getName = f - return nil - } -} - -// WithEventOpts allows additional ETW event properties (keywords, tags, etc.) to be specified. -func WithEventOpts(f func(*logrus.Entry) []etw.EventOpt) HookOpt { - return func(h *Hook) error { - h.getEventsOpts = f - return nil - } -} diff --git a/pkg/etwlogrus/HookTest.wprp b/pkg/etwslog/HookTest.wprp similarity index 100% rename from pkg/etwlogrus/HookTest.wprp rename to pkg/etwslog/HookTest.wprp diff --git a/pkg/etwslog/handler.go b/pkg/etwslog/handler.go new file mode 100644 index 00000000..46ab45b7 --- /dev/null +++ b/pkg/etwslog/handler.go @@ -0,0 +1,201 @@ +//go:build windows + +package etwslog + +import ( + "context" + "errors" + "log/slog" + "sort" + + "github.com/Microsoft/go-winio/pkg/etw" +) + +const defaultEventName = "SlogEntry" + +// ErrNoProvider is returned when a handler is created without a provider being configured. +var ErrNoProvider = errors.New("no ETW registered provider") + +// Handler is a [slog.Handler] which logs received events to ETW. +type Handler struct { + provider *etw.Provider + closeProvider bool + // allows setting the event name + getName func(slog.Record) string + // returns additional options to add to the event + getEventsOpts func(slog.Record) []etw.EventOpt + // pre-computed attrs from WithAttrs + attrs []slog.Attr + // group prefix from WithGroup + group string +} + +// slogLevelToETWLevel maps slog levels to ETW levels using range-based mapping. +func slogLevelToETWLevel(level slog.Level) etw.Level { + switch { + case level >= slog.LevelError: + return etw.LevelError + case level >= slog.LevelWarn: + return etw.LevelWarning + case level >= slog.LevelInfo: + return etw.LevelInfo + default: + return etw.LevelVerbose + } +} + +// Enabled reports whether the handler handles records at the given level. +func (h *Handler) Enabled(_ context.Context, level slog.Level) bool { + return h.provider.IsEnabledForLevel(slogLevelToETWLevel(level)) +} + +// Handle writes the record to ETW. +func (h *Handler) Handle(_ context.Context, r slog.Record) error { + level := slogLevelToETWLevel(r.Level) + if !h.provider.IsEnabledForLevel(level) { + return nil + } + + name := defaultEventName + if h.getName != nil { + if n := h.getName(r); n != "" { + name = n + } + } + + // extra room for two more options in addition to log level to avoid repeated reallocations + // if the user also provides options + opts := make([]etw.EventOpt, 0, 3) + opts = append(opts, etw.WithLevel(level)) + if h.getEventsOpts != nil { + opts = append(opts, h.getEventsOpts(r)...) + } + + // Collect all attrs: pre-computed from WithAttrs + record attrs. + allAttrs := make([]slog.Attr, 0, len(h.attrs)+r.NumAttrs()) + allAttrs = append(allAttrs, h.attrs...) + r.Attrs(func(a slog.Attr) bool { + allAttrs = append(allAttrs, a) + return true + }) + + // Sort the attrs by key so they are consistent in each instance + // of an event. Otherwise, the fields don't line up in WPA. + // Put "error" last because it is optional in some events. + var errorAttr *slog.Attr + sorted := make([]slog.Attr, 0, len(allAttrs)) + for i := range allAttrs { + if allAttrs[i].Key == "error" { + a := allAttrs[i] + errorAttr = &a + } else { + sorted = append(sorted, allAttrs[i]) + } + } + sort.Slice(sorted, func(i, j int) bool { + return sorted[i].Key < sorted[j].Key + }) + + // Reserve extra space for the message and time fields. + fields := make([]etw.FieldOpt, 0, len(allAttrs)+2) + fields = append(fields, etw.StringField("Message", r.Message)) + fields = append(fields, etw.Time("Time", r.Time)) + for _, a := range sorted { + fields = append(fields, attrToETWField(h.group, a)) + } + if errorAttr != nil { + fields = append(fields, attrToETWField(h.group, *errorAttr)) + } + + // Firing an ETW event is essentially best effort, as the event write can + // fail for reasons completely out of the control of the event writer (such + // as a session listening for the event having no available space in its + // buffers). Therefore, we don't return the error from WriteEvent, as it is + // just noise in many cases. + _ = h.provider.WriteEvent(name, opts, fields) + + return nil +} + +// WithAttrs returns a new Handler with the given attributes pre-computed. +func (h *Handler) WithAttrs(attrs []slog.Attr) slog.Handler { + newAttrs := make([]slog.Attr, len(h.attrs), len(h.attrs)+len(attrs)) + copy(newAttrs, h.attrs) + newAttrs = append(newAttrs, attrs...) + return &Handler{ + provider: h.provider, + closeProvider: h.closeProvider, + getName: h.getName, + getEventsOpts: h.getEventsOpts, + attrs: newAttrs, + group: h.group, + } +} + +// WithGroup returns a new Handler with the given group name prepended to all +// attribute keys. +func (h *Handler) WithGroup(name string) slog.Handler { + newGroup := name + if h.group != "" { + newGroup = h.group + "." + name + } + return &Handler{ + provider: h.provider, + closeProvider: h.closeProvider, + getName: h.getName, + getEventsOpts: h.getEventsOpts, + attrs: h.attrs, + group: newGroup, + } +} + +// Close cleans up the handler and closes the ETW provider. If the provider was +// registered by etwslog, it will be closed as part of Close. If the +// provider was passed in, it will not be closed. +func (h *Handler) Close() error { + if h.closeProvider { + return h.provider.Close() + } + return nil +} + +func (h *Handler) validate() error { + if h.provider == nil { + return ErrNoProvider + } + return nil +} + +// attrToETWField converts a slog.Attr to an etw.FieldOpt. +func attrToETWField(group string, a slog.Attr) etw.FieldOpt { + key := a.Key + if group != "" { + key = group + "." + key + } + + v := a.Value.Resolve() + switch v.Kind() { + case slog.KindBool: + return etw.BoolField(key, v.Bool()) + case slog.KindInt64: + return etw.Int64Field(key, v.Int64()) + case slog.KindUint64: + return etw.Uint64Field(key, v.Uint64()) + case slog.KindFloat64: + return etw.Float64Field(key, v.Float64()) + case slog.KindString: + return etw.StringField(key, v.String()) + case slog.KindTime: + return etw.Time(key, v.Time()) + case slog.KindGroup: + attrs := v.Group() + fields := make([]etw.FieldOpt, 0, len(attrs)) + for _, ga := range attrs { + fields = append(fields, attrToETWField("", ga)) + } + return etw.Struct(key, fields...) + default: + // KindAny and anything else + return etw.SmartField(key, v.Any()) + } +} diff --git a/pkg/etwslog/handler_test.go b/pkg/etwslog/handler_test.go new file mode 100644 index 00000000..ae096ce1 --- /dev/null +++ b/pkg/etwslog/handler_test.go @@ -0,0 +1,100 @@ +//go:build windows + +package etwslog + +import ( + "log/slog" + "testing" +) + +func fireEvent(logger *slog.Logger, name string, value any) { + logger.Info(name, "Field", value) +} + +// The purpose of this test is to log lots of different field types, to test the +// logic that converts them to ETW. Because we don't have a way to +// programatically validate the ETW events, this test has two main purposes: (1) +// validate nothing causes a panic while logging (2) allow manual validation that +// the data is logged correctly (through a tool like WPA). +func TestFieldLogging(t *testing.T) { + // Sample WPRP to collect this provider is included in HookTest.wprp. + // + // Start collection: + // wpr -start HookTest.wprp -filemode + // + // Stop collection: + // wpr -stop HookTest.etl + h, err := NewHandler("HookTest") + if err != nil { + t.Fatal(err) + } + defer h.Close() + + logger := slog.New(h) + + fireEvent(logger, "Bool", true) + fireEvent(logger, "BoolSlice", []bool{true, false, true}) + fireEvent(logger, "EmptyBoolSlice", []bool{}) + fireEvent(logger, "String", "teststring") + fireEvent(logger, "StringSlice", []string{"sstr1", "sstr2", "sstr3"}) + fireEvent(logger, "EmptyStringSlice", []string{}) + fireEvent(logger, "Int", int(1)) + fireEvent(logger, "IntSlice", []int{2, 3, 4}) + fireEvent(logger, "EmptyIntSlice", []int{}) + fireEvent(logger, "Int8", int8(5)) + fireEvent(logger, "Int8Slice", []int8{6, 7, 8}) + fireEvent(logger, "EmptyInt8Slice", []int8{}) + fireEvent(logger, "Int16", int16(9)) + fireEvent(logger, "Int16Slice", []int16{10, 11, 12}) + fireEvent(logger, "EmptyInt16Slice", []int16{}) + fireEvent(logger, "Int32", int32(13)) + fireEvent(logger, "Int32Slice", []int32{14, 15, 16}) + fireEvent(logger, "EmptyInt32Slice", []int32{}) + fireEvent(logger, "Int64", int64(17)) + fireEvent(logger, "Int64Slice", []int64{18, 19, 20}) + fireEvent(logger, "EmptyInt64Slice", []int64{}) + fireEvent(logger, "Uint", uint(21)) + fireEvent(logger, "UintSlice", []uint{22, 23, 24}) + fireEvent(logger, "EmptyUintSlice", []uint{}) + fireEvent(logger, "Uint8", uint8(25)) + fireEvent(logger, "Uint8Slice", []uint8{26, 27, 28}) + fireEvent(logger, "EmptyUint8Slice", []uint8{}) + fireEvent(logger, "Uint16", uint16(29)) + fireEvent(logger, "Uint16Slice", []uint16{30, 31, 32}) + fireEvent(logger, "EmptyUint16Slice", []uint16{}) + fireEvent(logger, "Uint32", uint32(33)) + fireEvent(logger, "Uint32Slice", []uint32{34, 35, 36}) + fireEvent(logger, "EmptyUint32Slice", []uint32{}) + fireEvent(logger, "Uint64", uint64(37)) + fireEvent(logger, "Uint64Slice", []uint64{38, 39, 40}) + fireEvent(logger, "EmptyUint64Slice", []uint64{}) + fireEvent(logger, "Uintptr", uintptr(41)) + fireEvent(logger, "UintptrSlice", []uintptr{42, 43, 44}) + fireEvent(logger, "EmptyUintptrSlice", []uintptr{}) + fireEvent(logger, "Float32", float32(45.46)) + fireEvent(logger, "Float32Slice", []float32{47.48, 49.50, 51.52}) + fireEvent(logger, "EmptyFloat32Slice", []float32{}) + fireEvent(logger, "Float64", float64(53.54)) + fireEvent(logger, "Float64Slice", []float64{55.56, 57.58, 59.60}) + fireEvent(logger, "EmptyFloat64Slice", []float64{}) + + type struct1 struct { + A float32 + priv int + B []uint + } + type struct2 struct { + A int + B int + } + type struct3 struct { + struct2 + A int + B string + priv string + C struct1 + D uint16 + } + // Unexported fields, and fields in embedded structs, should not log. + fireEvent(logger, "Struct", struct3{struct2{-1, -2}, 1, "2s", "-3s", struct1{3.4, -4, []uint{5, 6, 7}}, 8}) +} diff --git a/pkg/etwslog/opts.go b/pkg/etwslog/opts.go new file mode 100644 index 00000000..87aaba30 --- /dev/null +++ b/pkg/etwslog/opts.go @@ -0,0 +1,83 @@ +//go:build windows + +package etwslog + +import ( + "log/slog" + + "github.com/Microsoft/go-winio/pkg/etw" +) + +// HandlerOpt is an option to change the behavior of the slog ETW handler. +type HandlerOpt func(*Handler) error + +// NewHandler registers a new ETW provider and returns a handler to log from it. +// The provider will be closed when the handler is closed. +func NewHandler(providerName string, opts ...HandlerOpt) (*Handler, error) { + opts = append(opts, WithNewETWProvider(providerName)) + + return NewHandlerFromOpts(opts...) +} + +// NewHandlerFromProvider creates a new handler based on an existing ETW provider. +// The provider will not be closed when the handler is closed. +func NewHandlerFromProvider(provider *etw.Provider, opts ...HandlerOpt) (*Handler, error) { + opts = append(opts, WithExistingETWProvider(provider)) + + return NewHandlerFromOpts(opts...) +} + +// NewHandlerFromOpts creates a new handler with the provided options. +// An error is returned if the handler does not have a valid provider. +func NewHandlerFromOpts(opts ...HandlerOpt) (*Handler, error) { + h := &Handler{} + + for _, o := range opts { + if err := o(h); err != nil { + return nil, err + } + } + return h, h.validate() +} + +// WithNewETWProvider registers a new ETW provider and sets the handler to log using it. +// The provider will be closed when the handler is closed. +func WithNewETWProvider(n string) HandlerOpt { + return func(h *Handler) error { + provider, err := etw.NewProvider(n, nil) + if err != nil { + return err + } + + h.provider = provider + h.closeProvider = true + return nil + } +} + +// WithExistingETWProvider configures the handler to use an existing ETW provider. +// The provider will not be closed when the handler is closed. +func WithExistingETWProvider(p *etw.Provider) HandlerOpt { + return func(h *Handler) error { + h.provider = p + h.closeProvider = false + return nil + } +} + +// WithGetName sets the ETW EventName of an event to the value returned by f. +// If the name is empty, the default event name will be used. +func WithGetName(f func(slog.Record) string) HandlerOpt { + return func(h *Handler) error { + h.getName = f + return nil + } +} + +// WithEventOpts allows additional ETW event properties (keywords, tags, etc.) to be specified. +func WithEventOpts(f func(slog.Record) []etw.EventOpt) HandlerOpt { + return func(h *Handler) error { + h.getEventsOpts = f + return nil + } +}