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()
-}