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  }