// Package logging includes utilities used to log function calls. This is in // an independent package to avoid dependency cycles. package logging import ( "context" "encoding/hex" "fmt" "io" "strconv" "strings" "github.com/tetratelabs/wazero/api" ) // ValueType is an extended form of api.ValueType, used to control logging in // cases such as bitmasks or strings. type ValueType = api.ValueType const ( ValueTypeI32 = api.ValueTypeI32 ValueTypeI64 = api.ValueTypeI64 ValueTypeF32 = api.ValueTypeF32 ValueTypeF64 = api.ValueTypeF64 ValueTypeV128 ValueType = 0x7b // same as wasm.ValueTypeV128 ValueTypeFuncref ValueType = 0x70 // same as wasm.ValueTypeFuncref ValueTypeExternref = api.ValueTypeExternref // ValueTypeMemI32 is a non-standard type which writes ValueTypeI32 from the memory offset. ValueTypeMemI32 = 0xfd // ValueTypeMemH64 is a non-standard type which writes 64-bits fixed-width hex from the memory offset. ValueTypeMemH64 = 0xfe // ValueTypeString is a non-standard type describing an offset/len pair of a string. ValueTypeString = 0xff ) type LogScopes uint64 const ( LogScopeNone = LogScopes(0) LogScopeClock LogScopes = 1 << iota LogScopeProc LogScopeFilesystem LogScopeMemory LogScopePoll LogScopeRandom LogScopeSock LogScopeAll = LogScopes(0xffffffffffffffff) ) func scopeName(s LogScopes) string { switch s { case LogScopeClock: return "clock" case LogScopeProc: return "proc" case LogScopeFilesystem: return "filesystem" case LogScopeMemory: return "memory" case LogScopePoll: return "poll" case LogScopeRandom: return "random" case LogScopeSock: return "sock" default: return fmt.Sprintf("", s) } } // IsEnabled returns true if the scope (or group of scopes) is enabled. func (f LogScopes) IsEnabled(scope LogScopes) bool { return f&scope != 0 } // String implements fmt.Stringer by returning each enabled log scope. func (f LogScopes) String() string { if f == LogScopeAll { return "all" } var builder strings.Builder for i := 0; i <= 63; i++ { // cycle through all bits to reduce code and maintenance target := LogScopes(1 << i) if f.IsEnabled(target) { if name := scopeName(target); name != "" { if builder.Len() > 0 { builder.WriteByte('|') } builder.WriteString(name) } } } return builder.String() } // LoggerKey is a context.Context Value key with a FunctionLogger value. type LoggerKey struct{} type ParamLogger func(ctx context.Context, mod api.Module, w Writer, params []uint64) type ParamSampler func(ctx context.Context, mod api.Module, params []uint64) bool type ResultLogger func(ctx context.Context, mod api.Module, w Writer, params, results []uint64) type Writer interface { io.Writer io.StringWriter io.ByteWriter } // ValWriter formats an indexed value. For example, if `vals[i]` is a // ValueTypeI32, this would format it by default as signed. If a // ValueTypeString, it would read `vals[i+1]` and write the string from memory. type ValWriter func(ctx context.Context, mod api.Module, w Writer, i uint32, vals []uint64) func Config(fnd api.FunctionDefinition) (paramLoggers []ParamLogger, resultLoggers []ResultLogger) { types := fnd.ParamTypes() names := fnd.ParamNames() if paramLen := uint32(len(types)); paramLen > 0 { paramLoggers = make([]ParamLogger, paramLen) hasParamNames := len(names) > 0 for i, t := range types { if hasParamNames { paramLoggers[i] = NewParamLogger(uint32(i), names[i], t) } else { paramLoggers[i] = (¶mLogger{idx: uint32(i), valWriter: ValWriterForType(t)}).Log } } } if resultLen := uint32(len(fnd.ResultTypes())); resultLen > 0 { resultLoggers = make([]ResultLogger, resultLen) hasResultNames := len(fnd.ResultNames()) > 0 for i, t := range fnd.ResultTypes() { if hasResultNames { resultLoggers[i] = NewResultLogger(uint32(i), fnd.ResultNames()[i], t) } else { resultLoggers[i] = (&resultLogger{idx: uint32(i), valWriter: ValWriterForType(t)}).Log } } } return } type paramLogger struct { idx uint32 valWriter ValWriter } func (n *paramLogger) Log(ctx context.Context, mod api.Module, w Writer, params []uint64) { n.valWriter(ctx, mod, w, n.idx, params) } func NewParamLogger(idx uint32, name string, t ValueType) ParamLogger { return (&namedParamLogger{idx: idx, name: name, valWriter: ValWriterForType(t)}).Log } type namedParamLogger struct { idx uint32 name string valWriter ValWriter } func (n *namedParamLogger) Log(ctx context.Context, mod api.Module, w Writer, params []uint64) { w.WriteString(n.name) //nolint w.WriteByte('=') //nolint n.valWriter(ctx, mod, w, n.idx, params) } type resultLogger struct { idx uint32 valWriter ValWriter } func (n *resultLogger) Log(ctx context.Context, mod api.Module, w Writer, _, results []uint64) { n.valWriter(ctx, mod, w, n.idx, results) } func NewResultLogger(idx uint32, name string, t ValueType) ResultLogger { return (&namedResultLogger{idx, name, ValWriterForType(t)}).Log } type namedResultLogger struct { idx uint32 name string valWriter ValWriter } func (n *namedResultLogger) Log(ctx context.Context, mod api.Module, w Writer, _, results []uint64) { w.WriteString(n.name) //nolint w.WriteByte('=') //nolint n.valWriter(ctx, mod, w, n.idx, results) } func ValWriterForType(vt ValueType) ValWriter { switch vt { case ValueTypeI32: return writeI32 case ValueTypeI64: return writeI64 case ValueTypeF32: return writeF32 case ValueTypeF64: return writeF64 case ValueTypeV128: return writeV128 case ValueTypeExternref, ValueTypeFuncref: return writeRef case ValueTypeMemI32: return writeMemI32 case ValueTypeMemH64: return writeMemH64 case ValueTypeString: return writeString default: panic(fmt.Errorf("BUG: unsupported type %d", vt)) } } func writeI32(_ context.Context, _ api.Module, w Writer, i uint32, vals []uint64) { v := vals[i] w.WriteString(strconv.FormatInt(int64(int32(v)), 10)) //nolint } func writeI64(_ context.Context, _ api.Module, w Writer, i uint32, vals []uint64) { v := vals[i] w.WriteString(strconv.FormatInt(int64(v), 10)) //nolint } func writeF32(_ context.Context, _ api.Module, w Writer, i uint32, vals []uint64) { v := vals[i] s := strconv.FormatFloat(float64(api.DecodeF32(v)), 'g', -1, 32) w.WriteString(s) //nolint } func writeF64(_ context.Context, _ api.Module, w Writer, i uint32, vals []uint64) { v := vals[i] s := strconv.FormatFloat(api.DecodeF64(v), 'g', -1, 64) w.WriteString(s) //nolint } // logV128 logs in fixed-width hex func writeV128(_ context.Context, _ api.Module, w Writer, i uint32, vals []uint64) { v1, v2 := vals[i], vals[i+1] w.WriteString(fmt.Sprintf("%016x%016x", v1, v2)) //nolint } // logRef logs in fixed-width hex func writeRef(_ context.Context, _ api.Module, w Writer, i uint32, vals []uint64) { v := vals[i] w.WriteString(fmt.Sprintf("%016x", v)) //nolint } func writeMemI32(_ context.Context, mod api.Module, w Writer, i uint32, vals []uint64) { offset := uint32(vals[i]) byteCount := uint32(4) if v, ok := mod.Memory().ReadUint32Le(offset); ok { w.WriteString(strconv.FormatInt(int64(int32(v)), 10)) //nolint } else { // log the positions that were out of memory WriteOOM(w, offset, byteCount) } } func writeMemH64(_ context.Context, mod api.Module, w Writer, i uint32, vals []uint64) { offset := uint32(vals[i]) byteCount := uint32(8) if s, ok := mod.Memory().Read(offset, byteCount); ok { hex.NewEncoder(w).Write(s) //nolint } else { // log the positions that were out of memory WriteOOM(w, offset, byteCount) } } func writeString(_ context.Context, mod api.Module, w Writer, i uint32, vals []uint64) { offset, byteCount := uint32(vals[i]), uint32(vals[i+1]) WriteStringOrOOM(mod.Memory(), w, offset, byteCount) } func WriteStringOrOOM(mem api.Memory, w Writer, offset, byteCount uint32) { if s, ok := mem.Read(offset, byteCount); ok { w.Write(s) //nolint } else { // log the positions that were out of memory WriteOOM(w, offset, byteCount) } } func WriteOOM(w Writer, offset uint32, byteCount uint32) { w.WriteString("OOM(") //nolint w.WriteString(strconv.Itoa(int(offset))) //nolint w.WriteByte(',') //nolint w.WriteString(strconv.Itoa(int(byteCount))) //nolint w.WriteByte(')') //nolint }