diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index da74d23776081bd96dc7d3d084548e047975c67f..e1db8fbdd50c958f5701105d7aacc72f0a493127 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -22,7 +22,7 @@ stages: - combine-artifacts - tag - doc-update - - version_check + - version-check go-test: stage: test @@ -66,8 +66,10 @@ build-workers: - mkdir -p release - GOOS=js GOARCH=wasm go build -ldflags '-w -s' -trimpath -o release/xxdk-channelsIndexedDkWorker.wasm ./indexedDb/impl/channels/... - GOOS=js GOARCH=wasm go build -ldflags '-w -s' -trimpath -o release/xxdk-dmIndexedDkWorker.wasm ./indexedDb/impl/dm/... + - GOOS=js GOARCH=wasm go build -ldflags '-w -s' -trimpath -o release/xxdk-logFileWorker.wasm ./logging/workerThread/... - cp indexedDb/impl/channels/channelsIndexedDbWorker.js release/ - cp indexedDb/impl/dm/dmIndexedDbWorker.js release/ + - cp logging/workerThread/logFileWorker.js release/ artifacts: paths: - release/ @@ -105,8 +107,10 @@ combine-artifacts: - 'curl --header "PRIVATE-TOKEN: $GITLAB_ACCESS_TOKEN" --output release/xxdk.wasm $CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/jobs/$BUILD_JOB_ID/artifacts/release/xxdk.wasm' - 'curl --header "PRIVATE-TOKEN: $GITLAB_ACCESS_TOKEN" --output release/xxdk-channelsIndexedDkWorker.wasm $CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/jobs/$BUILD_WORKERS_JOB_ID/artifacts/release/xxdk-channelsIndexedDkWorker.wasm' - 'curl --header "PRIVATE-TOKEN: $GITLAB_ACCESS_TOKEN" --output release/xxdk-dmIndexedDkWorker.wasm $CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/jobs/$BUILD_WORKERS_JOB_ID/artifacts/release/xxdk-dmIndexedDkWorker.wasm' + - 'curl --header "PRIVATE-TOKEN: $GITLAB_ACCESS_TOKEN" --output release/xxdk-logFileWorker.wasm $CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/jobs/$BUILD_WORKERS_JOB_ID/artifacts/release/xxdk-logFileWorker.wasm' - 'curl --header "PRIVATE-TOKEN: $GITLAB_ACCESS_TOKEN" --output release/channelsIndexedDbWorker.js $CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/jobs/$BUILD_WORKERS_JOB_ID/artifacts/release/channelsIndexedDbWorker.js' - 'curl --header "PRIVATE-TOKEN: $GITLAB_ACCESS_TOKEN" --output release/dmIndexedDbWorker.js $CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/jobs/$BUILD_WORKERS_JOB_ID/artifacts/release/dmIndexedDbWorker.js' + - 'curl --header "PRIVATE-TOKEN: $GITLAB_ACCESS_TOKEN" --output release/logFileWorker.js $CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/jobs/$BUILD_WORKERS_JOB_ID/artifacts/release/logFileWorker.js' - ls release artifacts: @@ -127,14 +131,14 @@ doc-update: - tags image: $DOCKER_IMAGE script: - # We use GOPRIVATE blank because not want to directly pull client, we want to use the public cache. + # GOPRIVATE is cleared so that the public cache is pulled instead of directly pulling client. - GOOS=js GOARCH=wasm GOPRIVATE="" go install gitlab.com/elixxir/xxdk-wasm@$CI_COMMIT_SHA only: - release - master -version_check: - stage: version_check +version-check: + stage: version-check except: - tags only: diff --git a/Makefile b/Makefile index f2aef79a98a682b68cc2134da3e0d1892ab5fac3..bc170d109666ddd78cbfd9a9dc0388b2559b5504 100644 --- a/Makefile +++ b/Makefile @@ -31,6 +31,7 @@ binary: worker_binaries: GOOS=js GOARCH=wasm go build -ldflags '-w -s' -trimpath -o xxdk-channelsIndexedDkWorker.wasm ./indexedDb/impl/channels/... GOOS=js GOARCH=wasm go build -ldflags '-w -s' -trimpath -o xxdk-dmIndexedDkWorker.wasm ./indexedDb/impl/dm/... + GOOS=js GOARCH=wasm go build -ldflags '-w -s' -trimpath -o xxdk-logFileWorker.wasm ./logging/workerThread/... binaries: binary worker_binaries diff --git a/indexedDb/impl/channels/main.go b/indexedDb/impl/channels/main.go index 1ab19b8e741658bcd09268a8b7a8abc8db6d2ae1..5290d0c89b6eedf92a1571aea04ff5b7fbfdc668 100644 --- a/indexedDb/impl/channels/main.go +++ b/indexedDb/impl/channels/main.go @@ -12,6 +12,7 @@ package main import ( "fmt" jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/xxdk-wasm/logging" "gitlab.com/elixxir/xxdk-wasm/wasm" "gitlab.com/elixxir/xxdk-wasm/worker" "syscall/js" @@ -22,19 +23,16 @@ const SEMVER = "0.1.0" func init() { // Set up Javascript console listener set at level INFO - ll := wasm.NewJsConsoleLogListener(jww.LevelInfo) - jww.SetLogListeners(ll.Listen) + ll := logging.NewJsConsoleLogListener(jww.LevelInfo) + logging.AddLogListener(ll.Listen) jww.SetStdoutThreshold(jww.LevelFatal + 1) jww.INFO.Printf("xxDK channels web worker version: v%s", SEMVER) } func main() { - fmt.Println("[WW] Starting xxDK WebAssembly Channels Database Worker.") jww.INFO.Print("[WW] Starting xxDK WebAssembly Channels Database Worker.") js.Global().Set("LogLevel", js.FuncOf(wasm.LogLevel)) - js.Global().Set("LogToFile", js.FuncOf(wasm.LogToFile)) - js.Global().Set("RegisterLogWriter", js.FuncOf(wasm.RegisterLogWriter)) m := &manager{mh: worker.NewThreadManager("ChannelsIndexedDbWorker", true)} m.registerCallbacks() diff --git a/indexedDb/impl/dm/main.go b/indexedDb/impl/dm/main.go index 37885aa182d76bf56a562efbb1b318b10cdaf638..20b20a0856c78ac753798c9fd4a692e4a88e4852 100644 --- a/indexedDb/impl/dm/main.go +++ b/indexedDb/impl/dm/main.go @@ -12,6 +12,7 @@ package main import ( "fmt" jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/xxdk-wasm/logging" "gitlab.com/elixxir/xxdk-wasm/wasm" "gitlab.com/elixxir/xxdk-wasm/worker" "syscall/js" @@ -22,19 +23,16 @@ const SEMVER = "0.1.0" func init() { // Set up Javascript console listener set at level INFO - ll := wasm.NewJsConsoleLogListener(jww.LevelInfo) - jww.SetLogListeners(ll.Listen) + ll := logging.NewJsConsoleLogListener(jww.LevelInfo) + logging.AddLogListener(ll.Listen) jww.SetStdoutThreshold(jww.LevelFatal + 1) jww.INFO.Printf("xxDK DM web worker version: v%s", SEMVER) } func main() { - fmt.Println("[WW] Starting xxDK WebAssembly DM Database Worker.") jww.INFO.Print("[WW] Starting xxDK WebAssembly DM Database Worker.") js.Global().Set("LogLevel", js.FuncOf(wasm.LogLevel)) - js.Global().Set("LogToFile", js.FuncOf(wasm.LogToFile)) - js.Global().Set("RegisterLogWriter", js.FuncOf(wasm.RegisterLogWriter)) m := &manager{mh: worker.NewThreadManager("DmIndexedDbWorker", true)} m.registerCallbacks() diff --git a/logging/console.go b/logging/console.go new file mode 100644 index 0000000000000000000000000000000000000000..8c2edd821210d338f73ff18bfabedfc2f319d7d9 --- /dev/null +++ b/logging/console.go @@ -0,0 +1,97 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file. // +//////////////////////////////////////////////////////////////////////////////// + +//go:build js && wasm + +package logging + +import ( + jww "github.com/spf13/jwalterweatherman" + "io" + "syscall/js" +) + +var consoleObj = js.Global().Get("console") + +// Console contains the Javascript console object, which provides access to the +// browser's debugging console. This structure is defined for only a single +// method on the console object. For example, if the method is set to debug, +// then all calls to console.Write will print a debug message to the Javascript +// console. +// +// Doc: https://developer.mozilla.org/en-US/docs/Web/API/console +type Console struct { + method string + js.Value +} + +// Write writes the data to the Javascript console with preset method. Returns +// the number of bytes written. +func (c *Console) Write(p []byte) (n int, err error) { + c.Call(c.method, string(p)) + return len(p), nil +} + +// JsConsoleLogListener redirects log output to the Javascript console using the +// correct console method. +type JsConsoleLogListener struct { + jww.Threshold + js.Value + + trace *Console + debug *Console + info *Console + error *Console + warn *Console + critical *Console + fatal *Console + def *Console +} + +// NewJsConsoleLogListener initialises a new log listener that listener for the +// specific threshold and prints the logs to the Javascript console. +func NewJsConsoleLogListener(threshold jww.Threshold) *JsConsoleLogListener { + return &JsConsoleLogListener{ + Threshold: threshold, + Value: consoleObj, + trace: &Console{"debug", consoleObj}, + debug: &Console{"log", consoleObj}, + info: &Console{"info", consoleObj}, + warn: &Console{"warn", consoleObj}, + error: &Console{"error", consoleObj}, + critical: &Console{"error", consoleObj}, + fatal: &Console{"error", consoleObj}, + def: &Console{"log", consoleObj}, + } +} + +// Listen is called for every logging event. This function adheres to the +// [jwalterweatherman.LogListener] type. +func (ll *JsConsoleLogListener) Listen(t jww.Threshold) io.Writer { + if t < ll.Threshold { + return nil + } + + switch t { + case jww.LevelTrace: + return ll.trace + case jww.LevelDebug: + return ll.debug + case jww.LevelInfo: + return ll.info + case jww.LevelWarn: + return ll.warn + case jww.LevelError: + return ll.error + case jww.LevelCritical: + return ll.critical + case jww.LevelFatal: + return ll.fatal + default: + return ll.def + } +} diff --git a/logging/jwwListeners.go b/logging/jwwListeners.go new file mode 100644 index 0000000000000000000000000000000000000000..7d83f3d5bcc69190c505dfd0d0676fbe6104d1d0 --- /dev/null +++ b/logging/jwwListeners.go @@ -0,0 +1,78 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file. // +//////////////////////////////////////////////////////////////////////////////// + +package logging + +import ( + jww "github.com/spf13/jwalterweatherman" + "sync" +) + +// logListeners contains a map of all registered log listeners keyed on a unique +// ID that can be used to remove the listener once it has been added. This +// global keeps track of all listeners that are registered to jwalterweatherman +// logging. +var logListeners = newLogListenerList() + +type logListenerList struct { + listeners map[uint64]jww.LogListener + currentID uint64 + sync.Mutex +} + +func newLogListenerList() logListenerList { + return logListenerList{ + listeners: make(map[uint64]jww.LogListener), + currentID: 0, + } +} + +// AddLogListener registers the log listener with jwalterweatherman. Returns a +// unique ID that can be used to remove the listener. +func AddLogListener(ll jww.LogListener) uint64 { + logListeners.Lock() + defer logListeners.Unlock() + + id := logListeners.addLogListener(ll) + jww.SetLogListeners(logListeners.mapToSlice()...) + return id +} + +// RemoveLogListener unregisters the log listener with the ID from +// jwalterweatherman. +func RemoveLogListener(id uint64) { + logListeners.Lock() + defer logListeners.Unlock() + + logListeners.removeLogListener(id) + jww.SetLogListeners(logListeners.mapToSlice()...) + +} + +// addLogListener adds the listener to the list and returns its unique ID. +func (lll *logListenerList) addLogListener(ll jww.LogListener) uint64 { + id := lll.currentID + lll.currentID++ + lll.listeners[id] = ll + + return id +} + +// removeLogListener removes the listener with the specified ID from the list. +func (lll *logListenerList) removeLogListener(id uint64) { + delete(lll.listeners, id) +} + +// mapToSlice converts the map of listeners to a slice of listeners so that it +// can be registered with jwalterweatherman.SetLogListeners. +func (lll *logListenerList) mapToSlice() []jww.LogListener { + listeners := make([]jww.LogListener, 0, len(lll.listeners)) + for _, l := range lll.listeners { + listeners = append(listeners, l) + } + return listeners +} diff --git a/logging/logLevel.go b/logging/logLevel.go new file mode 100644 index 0000000000000000000000000000000000000000..895857475c4c647d7625b43644e6c4f32be98155 --- /dev/null +++ b/logging/logLevel.go @@ -0,0 +1,89 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file. // +//////////////////////////////////////////////////////////////////////////////// + +//go:build js && wasm + +package logging + +import ( + "fmt" + "github.com/pkg/errors" + jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/xxdk-wasm/utils" + "log" + "syscall/js" +) + +// LogLevel sets level of logging. All logs at the set level and below will be +// displayed (e.g., when log level is ERROR, only ERROR, CRITICAL, and FATAL +// messages will be printed). +// +// The default log level without updates is INFO. +func LogLevel(threshold jww.Threshold) error { + if threshold < jww.LevelTrace || threshold > jww.LevelFatal { + return errors.Errorf("log level is not valid: log level: %d", threshold) + } + + jww.SetLogThreshold(threshold) + jww.SetFlags(log.LstdFlags | log.Lmicroseconds) + + ll := NewJsConsoleLogListener(threshold) + AddLogListener(ll.Listen) + jww.SetStdoutThreshold(jww.LevelFatal + 1) + + msg := fmt.Sprintf("Log level set to: %s", threshold) + switch threshold { + case jww.LevelTrace: + fallthrough + case jww.LevelDebug: + fallthrough + case jww.LevelInfo: + jww.INFO.Print(msg) + case jww.LevelWarn: + jww.WARN.Print(msg) + case jww.LevelError: + jww.ERROR.Print(msg) + case jww.LevelCritical: + jww.CRITICAL.Print(msg) + case jww.LevelFatal: + jww.FATAL.Print(msg) + } + + return nil +} + +// LogLevelJS sets level of logging. All logs at the set level and below will be +// displayed (e.g., when log level is ERROR, only ERROR, CRITICAL, and FATAL +// messages will be printed). +// +// Log level options: +// +// TRACE - 0 +// DEBUG - 1 +// INFO - 2 +// WARN - 3 +// ERROR - 4 +// CRITICAL - 5 +// FATAL - 6 +// +// The default log level without updates is INFO. +// +// Parameters: +// - args[0] - Log level (int). +// +// Returns: +// - Throws TypeError if the log level is invalid. +func LogLevelJS(_ js.Value, args []js.Value) any { + threshold := jww.Threshold(args[0].Int()) + err := LogLevel(threshold) + if err != nil { + utils.Throw(utils.TypeError, err) + return nil + } + + return nil +} diff --git a/logging/logger.go b/logging/logger.go new file mode 100644 index 0000000000000000000000000000000000000000..7da185f34ff81c31475dec3531c407bd53344114 --- /dev/null +++ b/logging/logger.go @@ -0,0 +1,537 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file. // +//////////////////////////////////////////////////////////////////////////////// + +//go:build js && wasm + +package logging + +import ( + "encoding/binary" + "encoding/json" + "fmt" + "github.com/armon/circbuf" + "github.com/pkg/errors" + jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/xxdk-wasm/utils" + "gitlab.com/elixxir/xxdk-wasm/worker" + "io" + "strconv" + "sync/atomic" + "syscall/js" + "time" +) + +const ( + // DefaultInitThreshold is the log threshold used for the initial log before + // any logging options is set. + DefaultInitThreshold = jww.LevelTrace + + // logListenerChanSize is the size of the listener channel that stores log + // messages before they are written. + logListenerChanSize = 1500 +) + +// List of tags that can be used when sending a message or registering a handler +// to receive a message. +const ( + NewLogFileTag worker.Tag = "NewLogFile" + WriteLogTag worker.Tag = "WriteLog" + GetFileTag worker.Tag = "GetFile" + GetFileExtTag worker.Tag = "GetFileExt" + SizeTag worker.Tag = "Size" +) + +// logger is the global that all jwalterweatherman logging is sent to. +var logger *Logger + +// Logger manages the recording of jwalterweatherman logs. It can write logs to +// a local, in-memory buffer or to an external worker. +type Logger struct { + threshold jww.Threshold + maxLogFileSize int + logListenerID uint64 + + listenChan chan []byte + mode atomic.Uint32 + processQuit chan struct{} + + cb *circbuf.Buffer + wm *worker.Manager +} + +// InitLogger initializes the logger. Include this in the init function in main. +func InitLogger() *Logger { + logger = NewLogger() + return logger +} + +// GetLogger returns the Logger object, used to manager where logging is +// recorded. +func GetLogger() *Logger { + return logger +} + +// NewLogger creates a new Logger that begins storing the first +// DefaultInitThreshold log entries. If either the log file or log worker is +// enabled, then these logs are redirected to the set destination. If the +// channel fills up with no log recorder enabled, then the listener is disabled. +func NewLogger() *Logger { + lf := newLogger() + + // Add the log listener + lf.logListenerID = AddLogListener(lf.Listen) + + jww.INFO.Printf("[LOG] Enabled initial log file listener in %s with ID %d "+ + "at threshold %s that can store %d entries", + lf.getMode(), lf.logListenerID, lf.Threshold(), cap(lf.listenChan)) + + return lf +} + +// newLogger initialises a Logger without adding it as a log listener. +func newLogger() *Logger { + lf := &Logger{ + threshold: DefaultInitThreshold, + listenChan: make(chan []byte, logListenerChanSize), + mode: atomic.Uint32{}, + processQuit: make(chan struct{}), + } + lf.setMode(initMode) + + return lf +} + +// LogToFile starts logging to a local, in-memory log file. +func (l *Logger) LogToFile(threshold jww.Threshold, maxLogFileSize int) error { + err := l.prepare(threshold, maxLogFileSize, fileMode) + if err != nil { + return err + } + + b, err := circbuf.NewBuffer(int64(maxLogFileSize)) + if err != nil { + return err + } + l.cb = b + + sendLog := func(p []byte) { + if n, err2 := l.cb.Write(p); err2 != nil { + jww.ERROR.Printf( + "[LOG] Error writing log to circular buffer: %+v", err2) + } else if n != len(p) { + jww.ERROR.Printf( + "[LOG] Wrote %d bytes when %d bytes expected", n, len(p)) + } + } + go l.processLog(workerMode, sendLog, l.processQuit) + + return nil +} + +// LogToFileWorker starts a new worker that begins listening for logs and +// writing them to file. This function blocks until the worker has started. +func (l *Logger) LogToFileWorker(threshold jww.Threshold, maxLogFileSize int, + wasmJsPath, workerName string) error { + err := l.prepare(threshold, maxLogFileSize, workerMode) + if err != nil { + return err + } + + // Create new worker manager, which will start the worker and wait until + // communication has been established + wm, err := worker.NewManager(wasmJsPath, workerName, false) + if err != nil { + return err + } + l.wm = wm + + // Register the callback used by the Javascript to request the log file. + // This prevents an error print when GetFileExtTag is not registered. + l.wm.RegisterCallback(GetFileExtTag, func([]byte) { + jww.DEBUG.Print("[LOG] Received file requested from external " + + "Javascript. Ignoring file.") + }) + + data, err := json.Marshal(l.maxLogFileSize) + if err != nil { + return err + } + + // Send message to initialize the log file listener + errChan := make(chan error) + l.wm.SendMessage(NewLogFileTag, data, func(data []byte) { + if len(data) > 0 { + errChan <- errors.New(string(data)) + } else { + errChan <- nil + } + }) + + // Wait for worker to respond + select { + case err = <-errChan: + if err != nil { + return err + } + case <-time.After(worker.ResponseTimeout): + return errors.Errorf("timed out after %s waiting for new log "+ + "file in worker to initialize", worker.ResponseTimeout) + } + + jww.INFO.Printf("[LOG] Initialized log to file web worker %s.", workerName) + + sendLog := func(p []byte) { l.wm.SendMessage(WriteLogTag, p, nil) } + go l.processLog(workerMode, sendLog, l.processQuit) + + return nil +} + +// processLog processes the log messages sent to the listener channel and sends +// them to the appropriate recorder. +func (l *Logger) processLog(m mode, sendLog func(p []byte), quit chan struct{}) { + jww.INFO.Printf("[LOG] Starting log file processing thread in %s.", m) + + for { + select { + case <-quit: + jww.INFO.Printf("[LOG] Stopping log file processing thread.") + return + case p := <-l.listenChan: + go sendLog(p) + } + } +} + +// prepare sets the threshold, maxLogFileSize, and mode of the logger and +// prints a log message indicating this information. +func (l *Logger) prepare( + threshold jww.Threshold, maxLogFileSize int, m mode) error { + if m := l.getMode(); m != initMode { + return errors.Errorf("log already set to %s", m) + } else if threshold < jww.LevelTrace || threshold > jww.LevelFatal { + return errors.Errorf("log level of %d is invalid", threshold) + } + + l.threshold = threshold + l.maxLogFileSize = maxLogFileSize + l.setMode(m) + + msg := fmt.Sprintf("[LOG] Outputting log to file in %s of max size %d "+ + "with level %s", m, l.MaxSize(), l.Threshold()) + switch l.Threshold() { + case jww.LevelTrace: + fallthrough + case jww.LevelDebug: + fallthrough + case jww.LevelInfo: + jww.INFO.Print(msg) + case jww.LevelWarn: + jww.WARN.Print(msg) + case jww.LevelError: + jww.ERROR.Print(msg) + case jww.LevelCritical: + jww.CRITICAL.Print(msg) + case jww.LevelFatal: + jww.FATAL.Print(msg) + } + + return nil +} + +// StopLogging stops the logging of log messages and disables the log listener. +// If the log worker is running, it is terminated. Once logging is stopped, it +// cannot be resumed the log file cannot be recovered. +func (l *Logger) StopLogging() { + jww.DEBUG.Printf("[LOG] Removing log listener with ID %d", l.logListenerID) + RemoveLogListener(l.logListenerID) + + switch l.getMode() { + case workerMode: + go l.wm.Terminate() + jww.DEBUG.Printf("[LOG] Terminated log worker.") + case fileMode: + jww.DEBUG.Printf("[LOG] Reset circular buffer.") + l.cb.Reset() + } + + select { + case l.processQuit <- struct{}{}: + jww.DEBUG.Printf("[LOG] Sent quit channel to log process.") + default: + jww.DEBUG.Printf("[LOG] Failed to stop log processes.") + } +} + +// GetFile returns the entire log file. +// +// If the log file is listening locally, it returns it from the local buffer. If +// it is listening from the worker, it blocks until the file is returned. +func (l *Logger) GetFile() []byte { + switch l.getMode() { + case fileMode: + return l.cb.Bytes() + case workerMode: + fileChan := make(chan []byte) + l.wm.SendMessage(GetFileTag, nil, func(data []byte) { fileChan <- data }) + + select { + case file := <-fileChan: + return file + case <-time.After(worker.ResponseTimeout): + jww.FATAL.Panicf("[LOG] Timed out after %s waiting for log "+ + "file from worker", worker.ResponseTimeout) + return nil + } + default: + return nil + } +} + +// Threshold returns the log level threshold used in the file. +func (l *Logger) Threshold() jww.Threshold { + return l.threshold +} + +// MaxSize returns the max size, in bytes, that the log file is allowed to be. +func (l *Logger) MaxSize() int { + return l.maxLogFileSize +} + +// Size returns the current size, in bytes, written to the log file. +// +// If the log file is listening locally, it returns it from the local buffer. If +// it is listening from the worker, it blocks until the size is returned. +func (l *Logger) Size() int { + switch l.getMode() { + case fileMode: + return int(l.cb.Size()) + case workerMode: + sizeChan := make(chan []byte) + l.wm.SendMessage(SizeTag, nil, func(data []byte) { sizeChan <- data }) + + select { + case data := <-sizeChan: + return int(jww.Threshold(binary.LittleEndian.Uint64(data))) + case <-time.After(worker.ResponseTimeout): + jww.FATAL.Panicf("[LOG] Timed out after %s waiting for log "+ + "file size from worker", worker.ResponseTimeout) + return 0 + } + default: + return 0 + } +} + +//////////////////////////////////////////////////////////////////////////////// +// JWW Listener // +//////////////////////////////////////////////////////////////////////////////// + +// Listen is called for every logging event. This function adheres to the +// [jwalterweatherman.LogListener] type. +func (l *Logger) Listen(t jww.Threshold) io.Writer { + if t < l.threshold { + return nil + } + + return l +} + +// Write sends the bytes to the listener channel. It always returns the length +// of p and a nil error. This function adheres to the io.Writer interface. +func (l *Logger) Write(p []byte) (n int, err error) { + select { + case l.listenChan <- append([]byte{}, p...): + default: + jww.ERROR.Printf( + "[LOG] Logger channel filled. Log file recording stopping.") + l.StopLogging() + return 0, errors.Errorf( + "Logger channel filled. Log file recording stopping.") + } + return len(p), nil +} + +//////////////////////////////////////////////////////////////////////////////// +// Log File Mode // +//////////////////////////////////////////////////////////////////////////////// + +// mode represents the state of the Logger. +type mode uint32 + +const ( + initMode mode = iota + fileMode + workerMode +) + +func (l *Logger) setMode(m mode) { l.mode.Store(uint32(m)) } +func (l *Logger) getMode() mode { return mode(l.mode.Load()) } + +// String returns a human-readable representation of the mode for logging and +// debugging. This function adheres to the fmt.Stringer interface. +func (m mode) String() string { + switch m { + case initMode: + return "uninitialized mode" + case fileMode: + return "file mode" + case workerMode: + return "worker mode" + default: + return "invalid mode: " + strconv.Itoa(int(m)) + } +} + +//////////////////////////////////////////////////////////////////////////////// +// Javascript Bindings // +//////////////////////////////////////////////////////////////////////////////// + +// GetLoggerJS returns the Logger object, used to manager where logging is +// recorded and accessing the log file. +// +// Returns: +// - A Javascript representation of the [Logger] object. +func GetLoggerJS(js.Value, []js.Value) any { + return newLoggerJS(GetLogger()) +} + +// newLoggerJS creates a new Javascript compatible object (map[string]any) that +// matches the [Logger] structure. +func newLoggerJS(lfw *Logger) map[string]any { + logFileWorker := map[string]any{ + "LogToFile": js.FuncOf(lfw.LogToFileJS), + "LogToFileWorker": js.FuncOf(lfw.LogToFileWorkerJS), + "StopLogging": js.FuncOf(lfw.StopLoggingJS), + "GetFile": js.FuncOf(lfw.GetFileJS), + "Threshold": js.FuncOf(lfw.ThresholdJS), + "MaxSize": js.FuncOf(lfw.MaxSizeJS), + "Size": js.FuncOf(lfw.SizeJS), + "Worker": js.FuncOf(lfw.WorkerJS), + } + + return logFileWorker +} + +// LogToFileJS starts logging to a local, in-memory log file. +// +// Parameters: +// - args[0] - Log level (int). +// - args[1] - Max log file size, in bytes (int). +// +// Returns: +// - Throws a TypeError if starting the log file fails. +func (l *Logger) LogToFileJS(_ js.Value, args []js.Value) any { + threshold := jww.Threshold(args[0].Int()) + maxLogFileSize := args[1].Int() + + err := l.LogToFile(threshold, maxLogFileSize) + if err != nil { + utils.Throw(utils.TypeError, err) + return nil + } + + return nil +} + +// LogToFileWorkerJS starts a new worker that begins listening for logs and +// writing them to file. This function blocks until the worker has started. +// +// Parameters: +// - args[0] - Log level (int). +// - args[1] - Max log file size, in bytes (int). +// - args[2] - Path to Javascript start file for the worker WASM (string). +// - args[3] - Name of the worker (used in logs) (string). +// +// Returns a promise: +// - Resolves to nothing on success (void). +// - Rejected with an error if starting the worker fails. +func (l *Logger) LogToFileWorkerJS(_ js.Value, args []js.Value) any { + threshold := jww.Threshold(args[0].Int()) + maxLogFileSize := args[1].Int() + wasmJsPath := args[2].String() + workerName := args[3].String() + + promiseFn := func(resolve, reject func(args ...any) js.Value) { + err := l.LogToFileWorker( + threshold, maxLogFileSize, wasmJsPath, workerName) + if err != nil { + reject(utils.JsTrace(err)) + } else { + resolve() + } + } + + return utils.CreatePromise(promiseFn) +} + +// StopLoggingJS stops the logging of log messages and disables the log +// listener. If the log worker is running, it is terminated. Once logging is +// stopped, it cannot be resumed the log file cannot be recovered. +func (l *Logger) StopLoggingJS(js.Value, []js.Value) any { + l.StopLogging() + + return nil +} + +// GetFileJS returns the entire log file. +// +// If the log file is listening locally, it returns it from the local buffer. If +// it is listening from the worker, it blocks until the file is returned. +// +// Returns a promise: +// - Resolves to the log file contents (string). +func (l *Logger) GetFileJS(js.Value, []js.Value) any { + promiseFn := func(resolve, _ func(args ...any) js.Value) { + resolve(string(l.GetFile())) + } + + return utils.CreatePromise(promiseFn) +} + +// ThresholdJS returns the log level threshold used in the file. +// +// Returns: +// - Log level (int). +func (l *Logger) ThresholdJS(js.Value, []js.Value) any { + return int(l.Threshold()) +} + +// MaxSizeJS returns the max size, in bytes, that the log file is allowed to be. +// +// Returns: +// - Max file size (int). +func (l *Logger) MaxSizeJS(js.Value, []js.Value) any { + return l.MaxSize() +} + +// SizeJS returns the current size, in bytes, written to the log file. +// +// If the log file is listening locally, it returns it from the local buffer. If +// it is listening from the worker, it blocks until the size is returned. +// +// Returns a promise: +// - Resolves to the current file size (int). +func (l *Logger) SizeJS(js.Value, []js.Value) any { + promiseFn := func(resolve, _ func(args ...any) js.Value) { + resolve(l.Size()) + } + + return utils.CreatePromise(promiseFn) +} + +// WorkerJS returns the web worker object. +// +// Returns: +// - Javascript worker object. If the worker has not been initialized, it +// returns null. +func (l *Logger) WorkerJS(js.Value, []js.Value) any { + if l.getMode() == workerMode { + return l.wm.GetWorker() + } + + return js.Null() +} diff --git a/logging/logger_test.go b/logging/logger_test.go new file mode 100644 index 0000000000000000000000000000000000000000..0b5267be9cabaf995ddc8fcdba4b5d3ec8eea133 --- /dev/null +++ b/logging/logger_test.go @@ -0,0 +1,172 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file. // +//////////////////////////////////////////////////////////////////////////////// + +//go:build js && wasm + +package logging + +import ( + "bytes" + "fmt" + jww "github.com/spf13/jwalterweatherman" + "testing" +) + +// Tests InitLogger +func TestInitLogger(t *testing.T) { +} + +// Tests GetLogger +func TestGetLogger(t *testing.T) { +} + +// Tests NewLogger +func TestNewLogger(t *testing.T) { +} + +// Tests Logger.LogToFile +func TestLogger_LogToFile(t *testing.T) { + jww.SetStdoutThreshold(jww.LevelTrace) + l := NewLogger() + + err := l.LogToFile(jww.LevelTrace, 50000000) + if err != nil { + t.Fatalf("Failed to LogToFile: %+v", err) + } + + jww.INFO.Printf("test") + + file := l.cb.Bytes() + fmt.Printf("file:----------------------------\n%s\n---------------------------------\n", file) +} + +// Tests Logger.LogToFileWorker +func TestLogger_LogToFileWorker(t *testing.T) { +} + +// Tests Logger.processLog +func TestLogger_processLog(t *testing.T) { +} + +// Tests Logger.prepare +func TestLogger_prepare(t *testing.T) { +} + +// Tests Logger.StopLogging +func TestLogger_StopLogging(t *testing.T) { +} + +// Tests Logger.GetFile +func TestLogger_GetFile(t *testing.T) { +} + +// Tests Logger.Threshold +func TestLogger_Threshold(t *testing.T) { +} + +// Tests Logger.MaxSize +func TestLogger_MaxSize(t *testing.T) { +} + +// Tests Logger.Size +func TestLogger_Size(t *testing.T) { +} + +// Tests Logger.Listen +func TestLogger_Listen(t *testing.T) { + + // l := newLogger() + +} + +// Tests that Logger.Write can fill the listenChan channel completely and that +// all messages are received in the order they were added. +func TestLogger_Write(t *testing.T) { + l := newLogger() + expectedLogs := make([][]byte, logListenerChanSize) + + for i := range expectedLogs { + p := []byte( + fmt.Sprintf("Log message %d of %d.", i+1, logListenerChanSize)) + expectedLogs[i] = p + n, err := l.Listen(jww.LevelError).Write(p) + if err != nil { + t.Errorf("Received impossible error (%d): %+v", i, err) + } else if n != len(p) { + t.Errorf("Received incorrect bytes written (%d)."+ + "\nexpected: %d\nreceived: %d", i, len(p), n) + } + } + + for i, expected := range expectedLogs { + select { + case received := <-l.listenChan: + if !bytes.Equal(expected, received) { + t.Errorf("Received unexpected meessage (%d)."+ + "\nexpected: %q\nreceived: %q", i, expected, received) + } + default: + t.Errorf("Failed to read from channel.") + } + } +} + +// Error path: Tests that Logger.Write returns an error when the listener +// channel is full. +func TestLogger_Write_ChannelFilledError(t *testing.T) { + l := newLogger() + expectedLogs := make([][]byte, logListenerChanSize) + + for i := range expectedLogs { + p := []byte( + fmt.Sprintf("Log message %d of %d.", i+1, logListenerChanSize)) + expectedLogs[i] = p + n, err := l.Listen(jww.LevelError).Write(p) + if err != nil { + t.Errorf("Received impossible error (%d): %+v", i, err) + } else if n != len(p) { + t.Errorf("Received incorrect bytes written (%d)."+ + "\nexpected: %d\nreceived: %d", i, len(p), n) + } + } + + _, err := l.Write([]byte("test")) + if err == nil { + t.Error("Failed to receive error when the chanel should be full.") + } +} + +// Tests that Logger.getMode gets the same value set with Logger.setMode. +func TestLogger_setMode_getMode(t *testing.T) { + l := newLogger() + + for i, m := range []mode{initMode, fileMode, workerMode, 12} { + l.setMode(m) + received := l.getMode() + if m != received { + t.Errorf("Received wrong mode (%d).\nexpected: %s\nreceived: %s", + i, m, received) + } + } + +} + +// Unit test of mode.String. +func Test_mode_String(t *testing.T) { + for m, expected := range map[mode]string{ + initMode: "uninitialized mode", + fileMode: "file mode", + workerMode: "worker mode", + 12: "invalid mode: 12", + } { + s := m.String() + if s != expected { + t.Errorf("Wrong string for mode %d.\nexpected: %s\nreceived: %s", + m, expected, s) + } + } +} diff --git a/logging/workerThread/logFileWorker.js b/logging/workerThread/logFileWorker.js new file mode 100644 index 0000000000000000000000000000000000000000..159bfaa0d919a4f0cb2758af48d80c65891e7820 --- /dev/null +++ b/logging/workerThread/logFileWorker.js @@ -0,0 +1,17 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file. // +//////////////////////////////////////////////////////////////////////////////// + +importScripts('wasm_exec.js'); + +const go = new Go(); +const binPath = 'xxdk-logFileWorker.wasm' +WebAssembly.instantiateStreaming(fetch(binPath), go.importObject).then((result) => { + go.run(result.instance); + LogLevel(1); +}).catch((err) => { + console.error(err); +}); \ No newline at end of file diff --git a/logging/workerThread/main.go b/logging/workerThread/main.go new file mode 100644 index 0000000000000000000000000000000000000000..91b059f3da1195b0ff244027608a0f8a98482fed --- /dev/null +++ b/logging/workerThread/main.go @@ -0,0 +1,110 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file. // +//////////////////////////////////////////////////////////////////////////////// + +//go:build js && wasm + +package main + +import ( + "encoding/binary" + "encoding/json" + "fmt" + "github.com/armon/circbuf" + "github.com/pkg/errors" + jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/xxdk-wasm/logging" + "gitlab.com/elixxir/xxdk-wasm/worker" + "syscall/js" +) + +// SEMVER is the current semantic version of the xxDK Logger web worker. +const SEMVER = "0.1.0" + +func init() { + // Set up Javascript console listener set at level INFO + ll := logging.NewJsConsoleLogListener(jww.LevelDebug) + logging.AddLogListener(ll.Listen) + jww.SetStdoutThreshold(jww.LevelFatal + 1) + jww.INFO.Printf("xxDK Logger web worker version: v%s", SEMVER) +} + +// workerLogFile manages communication with the main thread and writing incoming +// logging messages to the log file. +type workerLogFile struct { + wtm *worker.ThreadManager + b *circbuf.Buffer +} + +func main() { + jww.INFO.Print("[LOG] Starting xxDK WebAssembly Logger Worker.") + + js.Global().Set("LogLevel", js.FuncOf(logging.LogLevelJS)) + + wlf := workerLogFile{wtm: worker.NewThreadManager("Logger", false)} + + wlf.registerCallbacks() + + wlf.wtm.SignalReady() + <-make(chan bool) + fmt.Println("[WW] Closing xxDK WebAssembly Log Worker.") +} + +// registerCallbacks registers all the necessary callbacks for the main thread +// to get the file and file metadata. +func (wlf *workerLogFile) registerCallbacks() { + // Callback for logging.LogToFileWorker + wlf.wtm.RegisterCallback(logging.NewLogFileTag, + func(data []byte) ([]byte, error) { + var maxLogFileSize int64 + err := json.Unmarshal(data, &maxLogFileSize) + if err != nil { + return []byte(err.Error()), err + } + + wlf.b, err = circbuf.NewBuffer(maxLogFileSize) + if err != nil { + return []byte(err.Error()), err + } + + jww.DEBUG.Printf("[LOG] Created new worker log file of size %d", + maxLogFileSize) + + return []byte{}, nil + }) + + // Callback for Logging.GetFile + wlf.wtm.RegisterCallback(logging.WriteLogTag, + func(data []byte) ([]byte, error) { + n, err := wlf.b.Write(data) + if err != nil { + return nil, err + } else if n != len(data) { + return nil, errors.Errorf( + "wrote %d bytes; expected %d bytes", n, len(data)) + } + + return nil, nil + }, + ) + + // Callback for Logging.GetFile + wlf.wtm.RegisterCallback(logging.GetFileTag, func([]byte) ([]byte, error) { + return wlf.b.Bytes(), nil + }) + + // Callback for Logging.GetFile + wlf.wtm.RegisterCallback(logging.GetFileExtTag, func([]byte) ([]byte, error) { + return wlf.b.Bytes(), nil + }) + + // Callback for Logging.Size + wlf.wtm.RegisterCallback(logging.SizeTag, func([]byte) ([]byte, error) { + b := make([]byte, 8) + binary.LittleEndian.PutUint64(b, uint64(wlf.b.TotalWritten())) + return b, nil + }) +} diff --git a/main.go b/main.go index 6a25f8d5da5dc386ecd2a1ecc31acd49220f3ada..a0d09748fd6a06ce6ddbda72554f136b4f45a741 100644 --- a/main.go +++ b/main.go @@ -10,7 +10,7 @@ package main import ( - "fmt" + "gitlab.com/elixxir/xxdk-wasm/logging" "os" "syscall/js" @@ -21,10 +21,13 @@ import ( ) func init() { + // Start logger first to capture all logging events + logging.InitLogger() + // Overwrites setting the log level to INFO done in bindings so that the // Javascript console can be used - ll := wasm.NewJsConsoleLogListener(jww.LevelInfo) - jww.SetLogListeners(ll.Listen) + ll := logging.NewJsConsoleLogListener(jww.LevelInfo) + logging.AddLogListener(ll.Listen) jww.SetStdoutThreshold(jww.LevelFatal + 1) // Check that the WASM binary version is correct @@ -35,7 +38,10 @@ func init() { } func main() { - fmt.Println("Starting xxDK WebAssembly bindings.") + jww.INFO.Printf("Starting xxDK WebAssembly bindings.") + + // logging/worker.go + js.Global().Set("GetLogger", js.FuncOf(logging.GetLoggerJS)) // storage/password.go js.Global().Set("GetOrInitPassword", js.FuncOf(storage.GetOrInitPassword)) @@ -141,7 +147,6 @@ func main() { // wasm/logging.go js.Global().Set("LogLevel", js.FuncOf(wasm.LogLevel)) - js.Global().Set("LogToFile", js.FuncOf(wasm.LogToFile)) js.Global().Set("RegisterLogWriter", js.FuncOf(wasm.RegisterLogWriter)) js.Global().Set("EnableGrpcLogs", js.FuncOf(wasm.EnableGrpcLogs)) diff --git a/wasm/logging.go b/wasm/logging.go index 46358b226f0549e1f05f3bb74c08f66a81a8198e..8199edc02a829a8e8b81b59dbfc73cddb4d46857 100644 --- a/wasm/logging.go +++ b/wasm/logging.go @@ -10,21 +10,11 @@ package wasm import ( - "fmt" - "github.com/armon/circbuf" - "github.com/pkg/errors" - jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/v4/bindings" - "gitlab.com/elixxir/xxdk-wasm/utils" - "io" - "log" + "gitlab.com/elixxir/xxdk-wasm/logging" "syscall/js" ) -// logListeners is a list of all registered log listeners. This is used to add -// additional log listener without overwriting previously registered listeners. -var logListeners []jww.LogListener - // LogLevel sets level of logging. All logs at the set level and below will be // displayed (e.g., when log level is ERROR, only ERROR, CRITICAL, and FATAL // messages will be printed). @@ -46,91 +36,8 @@ var logListeners []jww.LogListener // // Returns: // - Throws TypeError if the log level is invalid. -func LogLevel(_ js.Value, args []js.Value) any { - threshold := jww.Threshold(args[0].Int()) - if threshold < jww.LevelTrace || threshold > jww.LevelFatal { - err := errors.Errorf("log level is not valid: log level: %d", threshold) - utils.Throw(utils.TypeError, err) - return nil - } - - jww.SetLogThreshold(threshold) - jww.SetFlags(log.LstdFlags | log.Lmicroseconds) - - ll := NewJsConsoleLogListener(threshold) - logListeners = append(logListeners, ll.Listen) - jww.SetLogListeners(logListeners...) - jww.SetStdoutThreshold(jww.LevelFatal + 1) - - msg := fmt.Sprintf("Log level set to: %s", threshold) - switch threshold { - case jww.LevelTrace: - fallthrough - case jww.LevelDebug: - fallthrough - case jww.LevelInfo: - jww.INFO.Print(msg) - case jww.LevelWarn: - jww.WARN.Print(msg) - case jww.LevelError: - jww.ERROR.Print(msg) - case jww.LevelCritical: - jww.CRITICAL.Print(msg) - case jww.LevelFatal: - jww.FATAL.Print(msg) - } - - return nil -} - -// LogToFile enables logging to a file that can be downloaded. -// -// Parameters: -// - args[0] - Log level (int). -// - args[1] - Log file name (string). -// - args[2] - Max log file size, in bytes (int). -// -// Returns: -// - A Javascript representation of the [LogFile] object, which allows -// accessing the contents of the log file and other metadata. -func LogToFile(_ js.Value, args []js.Value) any { - threshold := jww.Threshold(args[0].Int()) - if threshold < jww.LevelTrace || threshold > jww.LevelFatal { - err := errors.Errorf("log level is not valid: log level: %d", threshold) - utils.Throw(utils.TypeError, err) - return nil - } - - // Create new log file output - ll, err := NewLogFile(args[1].String(), threshold, args[2].Int()) - if err != nil { - utils.Throw(utils.TypeError, err) - return nil - } - - logListeners = append(logListeners, ll.Listen) - jww.SetLogListeners(logListeners...) - - msg := fmt.Sprintf("Outputting log to file %s of max size %d with level %s", - ll.name, ll.b.Size(), threshold) - switch threshold { - case jww.LevelTrace: - fallthrough - case jww.LevelDebug: - fallthrough - case jww.LevelInfo: - jww.INFO.Print(msg) - case jww.LevelWarn: - jww.WARN.Print(msg) - case jww.LevelError: - jww.ERROR.Print(msg) - case jww.LevelCritical: - jww.CRITICAL.Print(msg) - case jww.LevelFatal: - jww.FATAL.Print(msg) - } - - return newLogFileJS(ll) +func LogLevel(this js.Value, args []js.Value) any { + return logging.LogLevelJS(this, args) } // logWriter wraps Javascript callbacks to adhere to the [bindings.LogWriter] @@ -164,174 +71,3 @@ func EnableGrpcLogs(_ js.Value, args []js.Value) any { bindings.EnableGrpcLogs(&logWriter{args[0].Invoke}) return nil } - -//////////////////////////////////////////////////////////////////////////////// -// Javascript Console Log Listener // -//////////////////////////////////////////////////////////////////////////////// - -// console contains the Javascript console object, which provides access to the -// browser's debugging console. This structure detects logging types and prints -// it using the correct logging method. -type console struct { - call string - js.Value -} - -// Write writes the data to the Javascript console at the level specified by the -// call. -func (c *console) Write(p []byte) (n int, err error) { - c.Call(c.call, string(p)) - return len(p), nil -} - -// JsConsoleLogListener redirects log output to the Javascript console. -type JsConsoleLogListener struct { - jww.Threshold - js.Value - - trace *console - debug *console - info *console - error *console - warn *console - critical *console - fatal *console - def *console -} - -// NewJsConsoleLogListener initialises a new log listener that listener for the -// specific threshold and prints the logs to the Javascript console. -func NewJsConsoleLogListener(threshold jww.Threshold) *JsConsoleLogListener { - consoleObj := js.Global().Get("console") - return &JsConsoleLogListener{ - Threshold: threshold, - Value: consoleObj, - trace: &console{"debug", consoleObj}, - debug: &console{"log", consoleObj}, - info: &console{"info", consoleObj}, - warn: &console{"warn", consoleObj}, - error: &console{"error", consoleObj}, - critical: &console{"error", consoleObj}, - fatal: &console{"error", consoleObj}, - def: &console{"log", consoleObj}, - } -} - -// Listen is called for every logging event. This function adheres to the -// [jwalterweatherman.LogListener] type. -func (ll *JsConsoleLogListener) Listen(t jww.Threshold) io.Writer { - if t < ll.Threshold { - return nil - } - - switch t { - case jww.LevelTrace: - return ll.trace - case jww.LevelDebug: - return ll.debug - case jww.LevelInfo: - return ll.info - case jww.LevelWarn: - return ll.warn - case jww.LevelError: - return ll.error - case jww.LevelCritical: - return ll.critical - case jww.LevelFatal: - return ll.fatal - default: - return ll.def - } -} - -//////////////////////////////////////////////////////////////////////////////// -// Log File Log Listener // -//////////////////////////////////////////////////////////////////////////////// - -// LogFile represents a virtual log file in memory. It contains a circular -// buffer that limits the log file, overwriting the oldest logs. -type LogFile struct { - name string - threshold jww.Threshold - b *circbuf.Buffer -} - -// NewLogFile initialises a new [LogFile] for log writing. -func NewLogFile( - name string, threshold jww.Threshold, maxSize int) (*LogFile, error) { - // Create new buffer of the specified size - b, err := circbuf.NewBuffer(int64(maxSize)) - if err != nil { - return nil, err - } - - return &LogFile{ - name: name, - threshold: threshold, - b: b, - }, nil -} - -// newLogFileJS creates a new Javascript compatible object (map[string]any) that -// matches the [LogFile] structure. -func newLogFileJS(lf *LogFile) map[string]any { - logFile := map[string]any{ - "Name": js.FuncOf(lf.Name), - "Threshold": js.FuncOf(lf.Threshold), - "GetFile": js.FuncOf(lf.GetFile), - "MaxSize": js.FuncOf(lf.MaxSize), - "Size": js.FuncOf(lf.Size), - } - - return logFile -} - -// Listen is called for every logging event. This function adheres to the -// [jwalterweatherman.LogListener] type. -func (lf *LogFile) Listen(t jww.Threshold) io.Writer { - if t < lf.threshold { - return nil - } - - return lf.b -} - -// Name returns the name of the log file. -// -// Returns: -// - File name (string). -func (lf *LogFile) Name(js.Value, []js.Value) any { - return lf.name -} - -// Threshold returns the log level threshold used in the file. -// -// Returns: -// - Log level (string). -func (lf *LogFile) Threshold(js.Value, []js.Value) any { - return lf.threshold.String() -} - -// GetFile returns the entire log file. -// -// Returns: -// - Log file contents (string). -func (lf *LogFile) GetFile(js.Value, []js.Value) any { - return string(lf.b.Bytes()) -} - -// MaxSize returns the max size, in bytes, that the log file is allowed to be. -// -// Returns: -// - Max file size (int). -func (lf *LogFile) MaxSize(js.Value, []js.Value) any { - return lf.b.Size() -} - -// Size returns the current size, in bytes, written to the log file. -// -// Returns: -// - Current file size (int). -func (lf *LogFile) Size(js.Value, []js.Value) any { - return lf.b.TotalWritten() -}