github.com/tetratelabs/wazero@v1.7.3-0.20240513003603-48f702e154b5/experimental/logging/log_listener.go (about) 1 package logging 2 3 import ( 4 "bufio" 5 "context" 6 "io" 7 8 "github.com/tetratelabs/wazero/api" 9 "github.com/tetratelabs/wazero/experimental" 10 aslogging "github.com/tetratelabs/wazero/internal/assemblyscript/logging" 11 "github.com/tetratelabs/wazero/internal/logging" 12 "github.com/tetratelabs/wazero/internal/wasip1" 13 wasilogging "github.com/tetratelabs/wazero/internal/wasip1/logging" 14 ) 15 16 type Writer interface { 17 io.Writer 18 io.StringWriter 19 } 20 21 // LogScopes is a bit flag of host function groups to log. e.g. LogScopeRandom. 22 // 23 // To specify all scopes, use LogScopeAll. For multiple scopes, OR them 24 // together like this: 25 // 26 // scope = logging.LogScopeRandom | logging.LogScopeFilesystem 27 // 28 // Note: Numeric values are not intended to be interpreted except as bit flags. 29 type LogScopes = logging.LogScopes 30 31 const ( 32 // LogScopeNone means nothing should be logged 33 LogScopeNone = logging.LogScopeNone 34 // LogScopeClock enables logging for functions such as `clock_time_get`. 35 LogScopeClock = logging.LogScopeClock 36 // LogScopeProc enables logging for functions such as `proc_exit`. 37 // 38 // Note: This includes functions that both log and exit. e.g. `abort`. 39 LogScopeProc = logging.LogScopeProc 40 // LogScopeFilesystem enables logging for functions such as `path_open`. 41 // 42 // Note: This doesn't log writes to the console. 43 LogScopeFilesystem = logging.LogScopeFilesystem 44 // LogScopeMemory enables logging for functions such as 45 // `emscripten_notify_memory_growth`. 46 LogScopeMemory = logging.LogScopeMemory 47 // LogScopePoll enables logging for functions such as `poll_oneoff`. 48 LogScopePoll = logging.LogScopePoll 49 // LogScopeRandom enables logging for functions such as `random_get`. 50 LogScopeRandom = logging.LogScopeRandom 51 // LogScopeSock enables logging for functions such as `sock_accept`. 52 LogScopeSock = logging.LogScopeSock 53 // LogScopeAll means all functions should be logged. 54 LogScopeAll = logging.LogScopeAll 55 ) 56 57 // NewLoggingListenerFactory is an experimental.FunctionListenerFactory that 58 // logs all functions that have a name to the writer. 59 // 60 // Use NewHostLoggingListenerFactory if only interested in host interactions. 61 func NewLoggingListenerFactory(w Writer) experimental.FunctionListenerFactory { 62 return &loggingListenerFactory{w: toInternalWriter(w), scopes: LogScopeAll} 63 } 64 65 // NewHostLoggingListenerFactory is an experimental.FunctionListenerFactory 66 // that logs exported and host functions to the writer. 67 // 68 // This is an alternative to NewLoggingListenerFactory, and would weed out 69 // guest defined functions such as those implementing garbage collection. 70 // 71 // For example, "_start" is defined by the guest, but exported, so would be 72 // written to the w in order to provide minimal context needed to 73 // understand host calls such as "args_get". 74 // 75 // The scopes parameter can be set to LogScopeAll or constrained. 76 func NewHostLoggingListenerFactory(w Writer, scopes logging.LogScopes) experimental.FunctionListenerFactory { 77 return &loggingListenerFactory{w: toInternalWriter(w), hostOnly: true, scopes: scopes} 78 } 79 80 func toInternalWriter(w Writer) logging.Writer { 81 if w, ok := w.(logging.Writer); ok { 82 return w 83 } 84 return bufio.NewWriter(w) 85 } 86 87 type loggingListenerFactory struct { 88 w logging.Writer 89 hostOnly bool 90 scopes logging.LogScopes 91 stack logStack 92 } 93 94 type flusher interface { 95 Flush() error 96 } 97 98 // NewFunctionListener implements the same method as documented on 99 // experimental.FunctionListener. 100 func (f *loggingListenerFactory) NewFunctionListener(fnd api.FunctionDefinition) experimental.FunctionListener { 101 exported := len(fnd.ExportNames()) > 0 102 if f.hostOnly && // choose functions defined or callable by the host 103 fnd.GoFunction() == nil && // not defined by the host 104 !exported { // not callable by the host 105 return nil 106 } 107 108 var pLoggers []logging.ParamLogger 109 var pSampler logging.ParamSampler 110 var rLoggers []logging.ResultLogger 111 switch fnd.ModuleName() { 112 case wasip1.InternalModuleName: 113 if !wasilogging.IsInLogScope(fnd, f.scopes) { 114 return nil 115 } 116 pSampler, pLoggers, rLoggers = wasilogging.Config(fnd) 117 case "env": 118 // env is difficult because the same module name is used for different 119 // ABI. 120 pLoggers, rLoggers = logging.Config(fnd) 121 switch fnd.Name() { 122 case "emscripten_notify_memory_growth": 123 if !logging.LogScopeMemory.IsEnabled(f.scopes) { 124 return nil 125 } 126 default: 127 if !aslogging.IsInLogScope(fnd, f.scopes) { 128 return nil 129 } 130 } 131 default: 132 // We don't know the scope of the function, so compare against all. 133 if f.scopes != logging.LogScopeAll { 134 return nil 135 } 136 pLoggers, rLoggers = logging.Config(fnd) 137 } 138 139 var before, after string 140 if fnd.GoFunction() != nil { 141 before = "==> " + fnd.DebugName() 142 after = "<==" 143 } else { 144 before = "--> " + fnd.DebugName() 145 after = "<--" 146 } 147 return &loggingListener{ 148 w: f.w, 149 beforePrefix: before, 150 afterPrefix: after, 151 pLoggers: pLoggers, 152 pSampler: pSampler, 153 rLoggers: rLoggers, 154 stack: &f.stack, 155 } 156 } 157 158 type logStack struct { 159 params [][]uint64 160 } 161 162 func (s *logStack) push(params []uint64) { 163 s.params = append(s.params, params) 164 } 165 166 func (s *logStack) pop() []uint64 { 167 i := len(s.params) - 1 168 params := s.params[i] 169 s.params[i] = nil 170 s.params = s.params[:i] 171 return params 172 } 173 174 func (s *logStack) count() (n int) { 175 for _, p := range s.params { 176 if p != nil { 177 n++ 178 } 179 } 180 return n 181 } 182 183 // loggingListener implements experimental.FunctionListener to log entrance and after 184 // of each function call. 185 type loggingListener struct { 186 w logging.Writer 187 beforePrefix, afterPrefix string 188 pLoggers []logging.ParamLogger 189 pSampler logging.ParamSampler 190 rLoggers []logging.ResultLogger 191 stack *logStack 192 } 193 194 // Before logs to stdout the module and function name, prefixed with '-->' and 195 // indented based on the call nesting level. 196 func (l *loggingListener) Before(ctx context.Context, mod api.Module, def api.FunctionDefinition, params []uint64, _ experimental.StackIterator) { 197 // First, see if this invocation is sampled. 198 sampled := true 199 if s := l.pSampler; s != nil { 200 sampled = s(ctx, mod, params) 201 } 202 203 if sampled { 204 l.logIndented(l.stack.count(), l.beforePrefix, func() { 205 l.logParams(ctx, mod, params) 206 }) 207 params = append([]uint64{}, params...) 208 } else { 209 params = nil 210 } 211 212 l.stack.push(params) 213 } 214 215 // After logs to stdout the module and function name, prefixed with '<--' and 216 // indented based on the call nesting level. 217 func (l *loggingListener) After(ctx context.Context, mod api.Module, def api.FunctionDefinition, results []uint64) { 218 if params := l.stack.pop(); params != nil { 219 l.logIndented(l.stack.count(), l.afterPrefix, func() { 220 l.logResults(ctx, mod, params, results) 221 }) 222 } 223 } 224 225 func (l *loggingListener) Abort(ctx context.Context, mod api.Module, def api.FunctionDefinition, _ error) { 226 l.stack.pop() 227 } 228 229 // logIndented writes an indentation level and prefix prior to calling log to 230 // output the log line. 231 func (l *loggingListener) logIndented(nestLevel int, prefix string, log func()) { 232 for i := 0; i < nestLevel; i++ { 233 l.w.WriteByte('\t') //nolint 234 } 235 l.w.WriteString(prefix) //nolint 236 log() 237 l.w.WriteByte('\n') //nolint 238 239 if f, ok := l.w.(flusher); ok { 240 f.Flush() //nolint 241 } 242 } 243 244 func (l *loggingListener) logParams(ctx context.Context, mod api.Module, params []uint64) { 245 paramLen := len(l.pLoggers) 246 l.w.WriteByte('(') //nolint 247 if paramLen > 0 { 248 l.pLoggers[0](ctx, mod, l.w, params) 249 for i := 1; i < paramLen; i++ { 250 l.w.WriteByte(',') //nolint 251 l.pLoggers[i](ctx, mod, l.w, params) 252 } 253 } 254 l.w.WriteByte(')') //nolint 255 } 256 257 func (l *loggingListener) logResults(ctx context.Context, mod api.Module, params, results []uint64) { 258 resultLen := len(l.rLoggers) 259 if resultLen == 0 { 260 return 261 } 262 l.w.WriteByte(' ') //nolint 263 switch resultLen { 264 case 1: 265 l.rLoggers[0](ctx, mod, l.w, params, results) 266 default: 267 l.w.WriteByte('(') //nolint 268 l.rLoggers[0](ctx, mod, l.w, params, results) 269 for i := 1; i < resultLen; i++ { 270 l.w.WriteByte(',') //nolint 271 l.rLoggers[i](ctx, mod, l.w, params, results) 272 } 273 l.w.WriteByte(')') //nolint 274 } 275 }