wa-lang.org/wazero@v1.0.2/experimental/logging/log_listener.go (about)

     1  package logging
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"strconv"
     8  	"strings"
     9  
    10  	"wa-lang.org/wazero/api"
    11  	"wa-lang.org/wazero/experimental"
    12  	"wa-lang.org/wazero/internal/wasi_snapshot_preview1"
    13  )
    14  
    15  // NewLoggingListenerFactory is an experimental.FunctionListenerFactory that
    16  // logs all functions that have a name to the writer.
    17  //
    18  // Use NewHostLoggingListenerFactory if only interested in host interactions.
    19  func NewLoggingListenerFactory(writer io.Writer) experimental.FunctionListenerFactory {
    20  	return &loggingListenerFactory{writer: writer}
    21  }
    22  
    23  // NewHostLoggingListenerFactory is an experimental.FunctionListenerFactory
    24  // that logs exported and host functions to the writer.
    25  //
    26  // This is an alternative to NewLoggingListenerFactory, and would weed out
    27  // guest defined functions such as those implementing garbage collection.
    28  //
    29  // For example, "_start" is defined by the guest, but exported, so would be
    30  // written to the writer in order to provide minimal context needed to
    31  // understand host calls such as "fd_open".
    32  func NewHostLoggingListenerFactory(writer io.Writer) experimental.FunctionListenerFactory {
    33  	return &loggingListenerFactory{writer: writer, hostOnly: true}
    34  }
    35  
    36  type loggingListenerFactory struct {
    37  	writer   io.Writer
    38  	hostOnly bool
    39  }
    40  
    41  // NewListener implements the same method as documented on
    42  // experimental.FunctionListener.
    43  func (f *loggingListenerFactory) NewListener(fnd api.FunctionDefinition) experimental.FunctionListener {
    44  	exported := len(fnd.ExportNames()) > 0
    45  	if f.hostOnly && // choose functions defined or callable by the host
    46  		fnd.GoFunction() == nil && // not defined by the host
    47  		!exported { // not callable by the host
    48  		return nil
    49  	}
    50  	return &loggingListener{writer: f.writer, fnd: fnd, isWasi: fnd.ModuleName() == "wasi_snapshot_preview1"}
    51  }
    52  
    53  // nestLevelKey holds state between logger.Before and loggingListener.After to ensure
    54  // call depth is reflected.
    55  type nestLevelKey struct{}
    56  
    57  // loggingListener implements experimental.FunctionListener to log entrance and exit
    58  // of each function call.
    59  type loggingListener struct {
    60  	writer io.Writer
    61  	fnd    api.FunctionDefinition
    62  	isWasi bool
    63  }
    64  
    65  // Before logs to stdout the module and function name, prefixed with '-->' and
    66  // indented based on the call nesting level.
    67  func (l *loggingListener) Before(ctx context.Context, _ api.FunctionDefinition, vals []uint64) context.Context {
    68  	nestLevel, _ := ctx.Value(nestLevelKey{}).(int)
    69  
    70  	l.writeIndented(true, nil, vals, nestLevel+1)
    71  
    72  	// Increase the next nesting level.
    73  	return context.WithValue(ctx, nestLevelKey{}, nestLevel+1)
    74  }
    75  
    76  // After logs to stdout the module and function name, prefixed with '<--' and
    77  // indented based on the call nesting level.
    78  func (l *loggingListener) After(ctx context.Context, _ api.FunctionDefinition, err error, vals []uint64) {
    79  	// Note: We use the nest level directly even though it is the "next" nesting level.
    80  	// This works because our indent of zero nesting is one tab.
    81  	l.writeIndented(false, err, vals, ctx.Value(nestLevelKey{}).(int))
    82  }
    83  
    84  // writeIndented writes an indented message like this: "-->\t\t\t$indentLevel$funcName\n"
    85  func (l *loggingListener) writeIndented(before bool, err error, vals []uint64, indentLevel int) {
    86  	var message strings.Builder
    87  	for i := 1; i < indentLevel; i++ {
    88  		message.WriteByte('\t')
    89  	}
    90  	if before {
    91  		if l.fnd.GoFunction() != nil {
    92  			message.WriteString("==> ")
    93  		} else {
    94  			message.WriteString("--> ")
    95  		}
    96  		l.writeFuncEnter(&message, vals)
    97  	} else { // after
    98  		if l.fnd.GoFunction() != nil {
    99  			message.WriteString("<== ")
   100  		} else {
   101  			message.WriteString("<-- ")
   102  		}
   103  		l.writeFuncExit(&message, err, vals)
   104  	}
   105  	message.WriteByte('\n')
   106  
   107  	_, _ = l.writer.Write([]byte(message.String()))
   108  }
   109  
   110  func (l *loggingListener) writeFuncEnter(message *strings.Builder, vals []uint64) {
   111  	valLen := len(vals)
   112  	message.WriteString(l.fnd.DebugName())
   113  	message.WriteByte('(')
   114  	switch valLen {
   115  	case 0:
   116  	default:
   117  		i := l.writeParam(message, 0, vals)
   118  		for i < valLen {
   119  			message.WriteByte(',')
   120  			i = l.writeParam(message, i, vals)
   121  		}
   122  	}
   123  	message.WriteByte(')')
   124  }
   125  
   126  func (l *loggingListener) writeFuncExit(message *strings.Builder, err error, vals []uint64) {
   127  	if err != nil {
   128  		message.WriteString("error: ")
   129  		message.WriteString(err.Error())
   130  		return
   131  	} else if l.isWasi {
   132  		message.WriteString(wasi_snapshot_preview1.ErrnoName(uint32(vals[0])))
   133  		return
   134  	}
   135  	valLen := len(vals)
   136  	message.WriteByte('(')
   137  	switch valLen {
   138  	case 0:
   139  	default:
   140  		i := l.writeResult(message, 0, vals)
   141  		for i < valLen {
   142  			message.WriteByte(',')
   143  			i = l.writeResult(message, i, vals)
   144  		}
   145  	}
   146  	message.WriteByte(')')
   147  }
   148  
   149  func (l *loggingListener) writeResult(message *strings.Builder, i int, vals []uint64) int {
   150  	return l.writeVal(message, l.fnd.ResultTypes()[i], i, vals)
   151  }
   152  
   153  func (l *loggingListener) writeParam(message *strings.Builder, i int, vals []uint64) int {
   154  	if len(l.fnd.ParamNames()) > 0 {
   155  		message.WriteString(l.fnd.ParamNames()[i])
   156  		message.WriteByte('=')
   157  	}
   158  	return l.writeVal(message, l.fnd.ParamTypes()[i], i, vals)
   159  }
   160  
   161  func (l *loggingListener) writeVal(message *strings.Builder, t api.ValueType, i int, vals []uint64) int {
   162  	v := vals[i]
   163  	i++
   164  	switch t {
   165  	case api.ValueTypeI32:
   166  		message.WriteString(strconv.FormatUint(uint64(uint32(v)), 10))
   167  	case api.ValueTypeI64:
   168  		message.WriteString(strconv.FormatUint(v, 10))
   169  	case api.ValueTypeF32:
   170  		message.WriteString(strconv.FormatFloat(float64(api.DecodeF32(v)), 'g', -1, 32))
   171  	case api.ValueTypeF64:
   172  		message.WriteString(strconv.FormatFloat(api.DecodeF64(v), 'g', -1, 64))
   173  	case 0x7b: // wasm.ValueTypeV128
   174  		message.WriteString(fmt.Sprintf("%016x%016x", v, vals[i])) // fixed-width hex
   175  		i++
   176  	case api.ValueTypeExternref, 0x70: // wasm.ValueTypeFuncref
   177  		message.WriteString(fmt.Sprintf("%016x", v)) // fixed-width hex
   178  	}
   179  	return i
   180  }