github.com/Serizao/go-winio@v0.0.0-20230906082528-f02f7f4ad6e8/pkg/etwlogrus/hook.go (about) 1 //go:build windows 2 // +build windows 3 4 package etwlogrus 5 6 import ( 7 "errors" 8 "sort" 9 10 "github.com/sirupsen/logrus" 11 12 "github.com/Serizao/go-winio/pkg/etw" 13 ) 14 15 const defaultEventName = "LogrusEntry" 16 17 // ErrNoProvider is returned when a hook is created without a provider being configured. 18 var ErrNoProvider = errors.New("no ETW registered provider") 19 20 // HookOpt is an option to change the behavior of the Logrus ETW hook. 21 type HookOpt func(*Hook) error 22 23 // Hook is a Logrus hook which logs received events to ETW. 24 type Hook struct { 25 provider *etw.Provider 26 closeProvider bool 27 // allows setting the entry name 28 getName func(*logrus.Entry) string 29 // returns additional options to add to the event 30 getEventsOpts func(*logrus.Entry) []etw.EventOpt 31 } 32 33 // NewHook registers a new ETW provider and returns a hook to log from it. 34 // The provider will be closed when the hook is closed. 35 func NewHook(providerName string, opts ...HookOpt) (*Hook, error) { 36 opts = append(opts, WithNewETWProvider(providerName)) 37 38 return NewHookFromOpts(opts...) 39 } 40 41 // NewHookFromProvider creates a new hook based on an existing ETW provider. 42 // The provider will not be closed when the hook is closed. 43 func NewHookFromProvider(provider *etw.Provider, opts ...HookOpt) (*Hook, error) { 44 opts = append(opts, WithExistingETWProvider(provider)) 45 46 return NewHookFromOpts(opts...) 47 } 48 49 // NewHookFromOpts creates a new hook with the provided options. 50 // An error is returned if the hook does not have a valid provider. 51 func NewHookFromOpts(opts ...HookOpt) (*Hook, error) { 52 h := defaultHook() 53 54 for _, o := range opts { 55 if err := o(h); err != nil { 56 return nil, err 57 } 58 } 59 return h, h.validate() 60 } 61 62 func defaultHook() *Hook { 63 h := &Hook{} 64 return h 65 } 66 67 func (h *Hook) validate() error { 68 if h.provider == nil { 69 return ErrNoProvider 70 } 71 return nil 72 } 73 74 // Levels returns the set of levels that this hook wants to receive log entries 75 // for. 76 func (*Hook) Levels() []logrus.Level { 77 return logrus.AllLevels 78 } 79 80 var logrusToETWLevelMap = map[logrus.Level]etw.Level{ 81 logrus.PanicLevel: etw.LevelAlways, 82 logrus.FatalLevel: etw.LevelCritical, 83 logrus.ErrorLevel: etw.LevelError, 84 logrus.WarnLevel: etw.LevelWarning, 85 logrus.InfoLevel: etw.LevelInfo, 86 logrus.DebugLevel: etw.LevelVerbose, 87 logrus.TraceLevel: etw.LevelVerbose, 88 } 89 90 // Fire receives each Logrus entry as it is logged, and logs it to ETW. 91 func (h *Hook) Fire(e *logrus.Entry) error { 92 // Logrus defines more levels than ETW typically uses, but analysis is 93 // easiest when using a consistent set of levels across ETW providers, so we 94 // map the Logrus levels to ETW levels. 95 level := logrusToETWLevelMap[e.Level] 96 if !h.provider.IsEnabledForLevel(level) { 97 return nil 98 } 99 100 name := defaultEventName 101 if h.getName != nil { 102 if n := h.getName(e); n != "" { 103 name = n 104 } 105 } 106 107 // extra room for two more options in addition to log level to avoid repeated reallocations 108 // if the user also provides options 109 opts := make([]etw.EventOpt, 0, 3) 110 opts = append(opts, etw.WithLevel(level)) 111 if h.getEventsOpts != nil { 112 opts = append(opts, h.getEventsOpts(e)...) 113 } 114 115 // Sort the fields by name so they are consistent in each instance 116 // of an event. Otherwise, the fields don't line up in WPA. 117 names := make([]string, 0, len(e.Data)) 118 hasError := false 119 for k := range e.Data { 120 if k == logrus.ErrorKey { 121 // Always put the error last because it is optional in some events. 122 hasError = true 123 } else { 124 names = append(names, k) 125 } 126 } 127 sort.Strings(names) 128 129 // Reserve extra space for the message and time fields. 130 fields := make([]etw.FieldOpt, 0, len(e.Data)+2) 131 fields = append(fields, etw.StringField("Message", e.Message)) 132 fields = append(fields, etw.Time("Time", e.Time)) 133 for _, k := range names { 134 fields = append(fields, etw.SmartField(k, e.Data[k])) 135 } 136 if hasError { 137 fields = append(fields, etw.SmartField(logrus.ErrorKey, e.Data[logrus.ErrorKey])) 138 } 139 140 // Firing an ETW event is essentially best effort, as the event write can 141 // fail for reasons completely out of the control of the event writer (such 142 // as a session listening for the event having no available space in its 143 // buffers). Therefore, we don't return the error from WriteEvent, as it is 144 // just noise in many cases. 145 _ = h.provider.WriteEvent(name, opts, fields) 146 147 return nil 148 } 149 150 // Close cleans up the hook and closes the ETW provider. If the provder was 151 // registered by etwlogrus, it will be closed as part of `Close`. If the 152 // provider was passed in, it will not be closed. 153 func (h *Hook) Close() error { 154 if h.closeProvider { 155 return h.provider.Close() 156 } 157 return nil 158 }