Repository: rclone/rclone (language: go)

Issue:
Flag --use-json-log does not working as expected 
<!--

We understand you are having a problem with rclone; we want to help you with that!

**STOP and READ**
**YOUR POST WILL BE REMOVED IF IT IS LOW QUALITY**:
Please show the effort you've put into solving the problem and please be specific.
People are volunteering their time to help! Low effort posts are not likely to get good answers!

If you think you might have found a bug, try to replicate it with the latest beta (or stable).
The update instructions are available at https://rclone.org/commands/rclone_selfupdate/

If you can still replicate it or just got a question then please use the rclone forum:

    https://forum.rclone.org/

for a quick response instead of filing an issue on this repo.

If nothing else helps, then please fill in the info below which helps us help you.

**DO NOT REDACT** any information except passwords/keys/personal info.

You should use 3 backticks to begin and end your paste to make it readable.

Make sure to include a log obtained with '-vv'.

You can also use '-vv --log-file bug.log' and a service such as https://pastebin.com or https://gist.github.com/

Thank you

The Rclone Developers

-->

#### The associated forum post URL from `https://forum.rclone.org`
https://forum.rclone.org/t/non-jsoned-output-when-using-use-json-log/27299/9


#### What is the problem you are having with rclone?
using follow command does not generating logfile in json format if fail, the log is plain text:
`rclone lsjson server: --low-level-retries 1 --contimeout 6s --use-json-log --log-file err.log` 



#### What is your rclone version (output from `rclone version`)
rclone v1.57.0
- os/version: ubuntu 20.04 (64 bit)
- os/kernel: 5.4.0-100-generic (x86_64)
- os/type: linux
- os/arch: amd64
- go/version: go1.17.2
- go/linking: static
- go/tags: none


#### Which OS you are using and how many bits (e.g. Windows 7, 64 bit)
Linux Ubuntu 20.04


#### Which cloud storage system are you using? (e.g. Google Drive)
SFTP

#### A log from the command with the `-vv` flag (e.g. output from `rclone -vv copy /tmp remote:tmp`)
with -vv the log has json, but the last line is not a json
```
{"level":"debug","msg":"Version \"v1.57.0\" starting with parameters [\"rclone\" \"lsjson\" \"server:\" \"--low-level-retries\" \"1\" \"--contimeout\" \"6s\" \"--use-json-log\" \"--log-file\" \"err.log\" \"-vv\"]","object":"rclone","objectType":"string","source":"cmd/cmd.go:408","time":"2022-03-15T08:36:52.234974-04:00"}
{"level":"debug","msg":"Creating backend with remote \"server:\"","source":"fs/newfs.go:28","time":"2022-03-15T08:36:52.235601-04:00"}
{"level":"debug","msg":"Using config file from \"/home/localroot/.config/rclone/rclone.conf\"","source":"config/config.go:353","time":"2022-03-15T08:36:52.235786-04:00"}
{"level":"debug","msg":"low level retry 1/1 (error couldn't connect SSH: dial tcp: lookup REDACT: no such host)","object":"pacer","objectType":"string","source":"fs/pacer.go:90","time":"2022-03-15T08:36:52.236656-04:00"}
{"level":"debug","msg":"Rate limited, increasing sleep to 200ms","object":"pacer","objectType":"string","source":"fs/pacer.go:46","time":"2022-03-15T08:36:52.23675-04:00"}
2022/03/15 08:36:52 Failed to create file system for "server:": NewFs: couldn't connect SSH: dial tcp: lookup REDACT: no such host
```



<!--- Please keep the note below for others who read your bug report. -->

#### How to use GitHub

* Please use the 👍 [reaction](https://blog.github.com/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) to show that you are affected by the same issue.
* Please don't comment if you have no relevant information to add. It's just extra noise for everyone subscribed to this issue.
* Subscribe to receive notifications on status change and new comments.



Discussion / hints:
It also happend for me with google drive after all retries

```
{"level":"debug","msg":"8 go routines active\n","source":"cmd/cmd.go:298","time":"2022-09-17T15:31:22.280164+00:00"}
2022/09/17 15:31:22 Failed to copy: googleapi: Error 403: User rate limit exceeded., userRateLimitExceeded
```

command
```
rclone --config ./logs/rclone/process_id_1.conf copy --drive-server-side-across-configs -vv --ignore-existing --tpslimit 3 --transfers 3 --low-level-retries 3 --drive-chunk-size 32M --drive-copy-shortcut-content --drive-acknowledge-abuse --use-json-log --log-file=./logs/rclone/process_id_1.log src001: dst001:test/offline/aaa/```
It looks like rclone isn't logging fatal errors with the rclone logger.

So any places where we have `log.Fatalf` or `log.Fatal` we need to be calling a new `fs.Fatalf` function.
I am experiencing this issue as well via librclone.

For instance if calling `rc/error` to test error handling, the output in the terminal looks like this:

`2023/07/17 03:37:32 ERROR : rc: "rc/error": error: arbitrary error on input map[]`
@xenago I think this is the same problem. Calling `log.Fatalf`. What we need to do is create an `fs.Fatalf` and then go through the entire codebase changing `log.Fatalf` to `fs.Fatalf`.
I wanted to use `rclone` in Qt application as a backed for uploading files to wherever.

Parsing text messages to distinguish progress/"Connecting to server"/errors "by hand" felt silly, so I've found out that there's `--use-json-long`, but as other's already noted, it's inconsistent. For example, I do get DEBUG messages as JSON:

```
{"level":"debug","msg":"low level retry 10/10 (error dial tcp 127.0.0.1:2121: connect: connection refused)","object":"pacer","objectType":"string","source":"fs/pacer.go:90","time":"2024-05-28T14:11:04.442773+03:00"}
```

But errors in plaintext:

```
Failed to create file system for \":ftp:\": NewFs: failed to make FTP connection to \"127.0.0.1:2121\": dial tcp 127.0.0.1:2121: connect: connection refused
```

Also, v1.66.0 no longer use "ERROR :" suffix for fatal errors as 1.53.3 in Debian 11 did :/ .

So yeah.. consistency is a bit lacking.
> I think this is the same problem. Calling `log.Fatalf`. What we need to do is create an `fs.Fatalf` and then go through the entire codebase changing `log.Fatalf` to `fs.Fatalf`.

This is still the problem if someone wants to have a go at fixing it.

We should probably have a lint rule to detect use of `log.Print` and `log.Fatal` also - @albertony can we do that?

> We should probably have a lint rule to detect use of `log.Print` and `log.Fatal` also - @albertony can we do that?

Haven't tried it, but it seems using [go-ruleguard](https://github.com/quasilyte/go-ruleguard) could be an elegant solution for defining our own custom linting rules. Golangci-lint integrates [go-critic](https://github.com/go-critic/go-critic), which in turn includes ruleguard [go-ruleguard](https://github.com/quasilyte/go-ruleguard).

Fix patch:
diff --git a/.golangci.yml b/.golangci.yml
index f4092493a6c68..4d3a8480ce216 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -100,10 +100,45 @@ linters-settings:
     # as documented here: https://staticcheck.io/docs/configuration/options/#checks
     checks: ["all", "-ST1000", "-ST1003", "-ST1016", "-ST1020", "-ST1021", "-ST1022", "-ST1023"]
   gocritic:
-    disabled-checks:
-      - appendAssign
-      - captLocal
-      - commentFormatting
-      - exitAfterDefer
-      - ifElseChain
-      - singleCaseSwitch
+    # Enable all default checks with some exceptions and some additions (commented).
+    # Cannot use both enabled-checks and disabled-checks, so must specify all to be used.
+    disable-all: true
+    enabled-checks:
+      #- appendAssign # Enabled by default
+      - argOrder
+      - assignOp
+      - badCall
+      - badCond
+      #- captLocal # Enabled by default
+      - caseOrder
+      - codegenComment
+      #- commentFormatting # Enabled by default
+      - defaultCaseOrder
+      - deprecatedComment
+      - dupArg
+      - dupBranchBody
+      - dupCase
+      - dupSubExpr
+      - elseif
+      #- exitAfterDefer # Enabled by default
+      - flagDeref
+      - flagName
+      #- ifElseChain # Enabled by default
+      - mapKey
+      - newDeref
+      - offBy1
+      - regexpMust
+      - ruleguard # Not enabled by default
+      #- singleCaseSwitch # Enabled by default
+      - sloppyLen
+      - sloppyTypeAssert
+      - switchTrue
+      - typeSwitchVar
+      - underef
+      - unlambda
+      - unslice
+      - valSwap
+      - wrapperFunc
+    settings:
+      ruleguard:
+        rules: "${configDir}/bin/rules.go"
diff --git a/backend/yandex/yandex.go b/backend/yandex/yandex.go
index aabb2f8dbdd35..7171491ea2c43 100644
--- a/backend/yandex/yandex.go
+++ b/backend/yandex/yandex.go
@@ -7,7 +7,6 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	"log"
 	"net/http"
 	"net/url"
 	"path"
@@ -283,7 +282,7 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e
 		if err != nil {
 			return nil, fmt.Errorf("couldn't save OAuth token: %w", err)
 		}
-		log.Printf("Automatically upgraded OAuth config.")
+		fs.Logf(nil, "Automatically upgraded OAuth config.")
 	}
 	oAuthClient, _, err := oauthutil.NewClient(ctx, name, m, oauthConfig)
 	if err != nil {
diff --git a/bin/not-in-stable.go b/bin/not-in-stable.go
index dc685cb6fedea..57a0257600b43 100644
--- a/bin/not-in-stable.go
+++ b/bin/not-in-stable.go
@@ -29,7 +29,7 @@ func readCommits(from, to string) (logMap map[string]string, logs []string) {
 	cmd := exec.Command("git", "log", "--oneline", from+".."+to)
 	out, err := cmd.Output()
 	if err != nil {
-		log.Fatalf("failed to run git log %s: %v", from+".."+to, err)
+		log.Fatalf("failed to run git log %s: %v", from+".."+to, err) //nolint:gocritic // Don't include gocritic when running golangci-lint to avoid ruleguard suggesting fs. intead of log.
 	}
 	logMap = map[string]string{}
 	logs = []string{}
@@ -39,7 +39,7 @@ func readCommits(from, to string) (logMap map[string]string, logs []string) {
 		}
 		match := logRe.FindSubmatch(line)
 		if match == nil {
-			log.Fatalf("failed to parse line: %q", line)
+			log.Fatalf("failed to parse line: %q", line) //nolint:gocritic // Don't include gocritic when running golangci-lint to avoid ruleguard suggesting fs. intead of log.
 		}
 		var hash, logMessage = string(match[1]), string(match[2])
 		logMap[logMessage] = hash
@@ -52,12 +52,12 @@ func main() {
 	flag.Parse()
 	args := flag.Args()
 	if len(args) != 0 {
-		log.Fatalf("Syntax: %s", os.Args[0])
+		log.Fatalf("Syntax: %s", os.Args[0]) //nolint:gocritic // Don't include gocritic when running golangci-lint to avoid ruleguard suggesting fs. intead of log.
 	}
 	// v1.54.0
 	versionBytes, err := os.ReadFile("VERSION")
 	if err != nil {
-		log.Fatalf("Failed to read version: %v", err)
+		log.Fatalf("Failed to read version: %v", err) //nolint:gocritic // Don't include gocritic when running golangci-lint to avoid ruleguard suggesting fs. intead of log.
 	}
 	if versionBytes[0] == 'v' {
 		versionBytes = versionBytes[1:]
@@ -65,7 +65,7 @@ func main() {
 	versionBytes = bytes.TrimSpace(versionBytes)
 	semver := semver.New(string(versionBytes))
 	stable := fmt.Sprintf("v%d.%d", semver.Major, semver.Minor-1)
-	log.Printf("Finding commits in %v not in stable %s", semver, stable)
+	log.Printf("Finding commits in %v not in stable %s", semver, stable) //nolint:gocritic // Don't include gocritic when running golangci-lint to avoid ruleguard suggesting fs. intead of log.
 	masterMap, masterLogs := readCommits(stable+".0", "master")
 	stableMap, _ := readCommits(stable+".0", stable+"-stable")
 	for _, logMessage := range masterLogs {
diff --git a/bin/rules.go b/bin/rules.go
new file mode 100644
index 0000000000000..05645adf56ec5
--- /dev/null
+++ b/bin/rules.go
@@ -0,0 +1,51 @@
+// Ruleguard file implementing custom linting rules.
+//
+// Note that when used from golangci-lint (using the gocritic linter configured
+// with the ruleguard check), because rule files are not handled by
+// golangci-lint itself, changes will not invalidate the golangci-lint cache,
+// and you must manually clean to cache (golangci-lint cache clean) for them to
+// be considered, as explained here:
+// https://www.quasilyte.dev/blog/post/ruleguard/#using-from-the-golangci-lint
+//
+// Note that this file is ignored from build with a build constraint, but using
+// a different than "ignore" to avoid go mod tidy making dsl an indirect
+// dependency, as explained here:
+// https://github.com/quasilyte/go-ruleguard?tab=readme-ov-file#troubleshooting
+
+//go:build ruleguard
+// +build ruleguard
+
+// Package gorules implementing custom linting rules using ruleguard
+package gorules
+
+import "github.com/quasilyte/go-ruleguard/dsl"
+
+// Suggest rewriting "log.(Print|Fatal|Panic)(f|ln)?" to
+// "fs.(Printf|Fatalf|Panicf)", and do it if running golangci-lint with
+// argument --fix. The suggestion wraps a single non-string single argument or
+// variadic arguments in fmt.Sprint to be compatible with format string
+// argument of fs functions.
+//
+// Caveats:
+//   - After applying the suggestions, imports may have to be fixed manually,
+//     removing unused "log", adding "github.com/rclone/rclone/fs" and "fmt",
+//     and if there was a variable named "fs" or "fmt" in the scope the name
+//     clash must be fixed.
+//   - Suggested code is incorrect when within fs package itself, due to the
+//     "fs."" prefix. Could handle it using condition
+//     ".Where(m.File().PkgPath.Matches(`github.com/rclone/rclone/fs`))"
+//     but not sure how to avoid duplicating all checks with and without this
+//     condition so haven't bothered yet.
+func useFsLog(m dsl.Matcher) {
+	m.Match(`log.Print($x)`, `log.Println($x)`).Where(m["x"].Type.Is(`string`)).Suggest(`fs.Log(nil, $x)`)
+	m.Match(`log.Print($*args)`, `log.Println($*args)`).Suggest(`fs.Log(nil, fmt.Sprint($args))`)
+	m.Match(`log.Printf($*args)`).Suggest(`fs.Logf(nil, $args)`)
+
+	m.Match(`log.Fatal($x)`, `log.Fatalln($x)`).Where(m["x"].Type.Is(`string`)).Suggest(`fs.Fatal(nil, $x)`)
+	m.Match(`log.Fatal($*args)`, `log.Fatalln($*args)`).Suggest(`fs.Fatal(nil, fmt.Sprint($args))`)
+	m.Match(`log.Fatalf($*args)`).Suggest(`fs.Fatalf(nil, $args)`)
+
+	m.Match(`log.Panic($x)`, `log.Panicln($x)`).Where(m["x"].Type.Is(`string`)).Suggest(`fs.Panic(nil, $x)`)
+	m.Match(`log.Panic($*args)`, `log.Panicln($*args)`).Suggest(`fs.Panic(nil, fmt.Sprint($args))`)
+	m.Match(`log.Panicf($*args)`).Suggest(`fs.Panicf(nil, $args)`)
+}
diff --git a/cmd/cat/cat.go b/cmd/cat/cat.go
index 9961dc2229da8..c2325dd4c63f5 100644
--- a/cmd/cat/cat.go
+++ b/cmd/cat/cat.go
@@ -4,11 +4,11 @@ package cat
 import (
 	"context"
 	"io"
-	"log"
 	"os"
 	"strings"
 
 	"github.com/rclone/rclone/cmd"
+	"github.com/rclone/rclone/fs"
 	"github.com/rclone/rclone/fs/config/flags"
 	"github.com/rclone/rclone/fs/operations"
 	"github.com/spf13/cobra"
@@ -79,7 +79,7 @@ files, use:
 		usedHead := head > 0
 		usedTail := tail > 0
 		if usedHead && usedTail || usedHead && usedOffset || usedTail && usedOffset {
-			log.Fatalf("Can only use one of  --head, --tail or --offset with --count")
+			fs.Fatalf(nil, "Can only use one of  --head, --tail or --offset with --count")
 		}
 		if head > 0 {
 			offset = 0
diff --git a/cmd/cmd.go b/cmd/cmd.go
index 2aca5341d8118..1223bddeeb6f3 100644
--- a/cmd/cmd.go
+++ b/cmd/cmd.go
@@ -10,7 +10,6 @@ import (
 	"context"
 	"errors"
 	"fmt"
-	"log"
 	"os"
 	"os/exec"
 	"path"
@@ -88,7 +87,7 @@ func NewFsFile(remote string) (fs.Fs, string) {
 	_, fsPath, err := fspath.SplitFs(remote)
 	if err != nil {
 		err = fs.CountError(err)
-		log.Fatalf("Failed to create file system for %q: %v", remote, err)
+		fs.Fatalf(nil, "Failed to create file system for %q: %v", remote, err)
 	}
 	f, err := cache.Get(context.Background(), remote)
 	switch err {
@@ -100,7 +99,7 @@ func NewFsFile(remote string) (fs.Fs, string) {
 		return f, ""
 	default:
 		err = fs.CountError(err)
-		log.Fatalf("Failed to create file system for %q: %v", remote, err)
+		fs.Fatalf(nil, "Failed to create file system for %q: %v", remote, err)
 	}
 	return nil, ""
 }
@@ -116,13 +115,13 @@ func newFsFileAddFilter(remote string) (fs.Fs, string) {
 		if !fi.InActive() {
 			err := fmt.Errorf("can't limit to single files when using filters: %v", remote)
 			err = fs.CountError(err)
-			log.Fatal(err.Error())
+			fs.Fatal(nil, err.Error())
 		}
 		// Limit transfers to this file
 		err := fi.AddFile(fileName)
 		if err != nil {
 			err = fs.CountError(err)
-			log.Fatalf("Failed to limit to single file %q: %v", remote, err)
+			fs.Fatalf(nil, "Failed to limit to single file %q: %v", remote, err)
 		}
 	}
 	return f, fileName
@@ -144,7 +143,7 @@ func newFsDir(remote string) fs.Fs {
 	f, err := cache.Get(context.Background(), remote)
 	if err != nil {
 		err = fs.CountError(err)
-		log.Fatalf("Failed to create file system for %q: %v", remote, err)
+		fs.Fatalf(nil, "Failed to create file system for %q: %v", remote, err)
 	}
 	cache.Pin(f) // pin indefinitely since it was on the CLI
 	return f
@@ -186,24 +185,24 @@ func NewFsSrcDstFiles(args []string) (fsrc fs.Fs, srcFileName string, fdst fs.Fs
 		var err error
 		dstRemote, dstFileName, err = fspath.Split(dstRemote)
 		if err != nil {
-			log.Fatalf("Parsing %q failed: %v", args[1], err)
+			fs.Fatalf(nil, "Parsing %q failed: %v", args[1], err)
 		}
 		if dstRemote == "" {
 			dstRemote = "."
 		}
 		if dstFileName == "" {
-			log.Fatalf("%q is a directory", args[1])
+			fs.Fatalf(nil, "%q is a directory", args[1])
 		}
 	}
 	fdst, err := cache.Get(context.Background(), dstRemote)
 	switch err {
 	case fs.ErrorIsFile:
 		_ = fs.CountError(err)
-		log.Fatalf("Source doesn't exist or is a directory and destination is a file")
+		fs.Fatalf(nil, "Source doesn't exist or is a directory and destination is a file")
 	case nil:
 	default:
 		_ = fs.CountError(err)
-		log.Fatalf("Failed to create file system for destination %q: %v", dstRemote, err)
+		fs.Fatalf(nil, "Failed to create file system for destination %q: %v", dstRemote, err)
 	}
 	cache.Pin(fdst) // pin indefinitely since it was on the CLI
 	return
@@ -213,13 +212,13 @@ func NewFsSrcDstFiles(args []string) (fsrc fs.Fs, srcFileName string, fdst fs.Fs
 func NewFsDstFile(args []string) (fdst fs.Fs, dstFileName string) {
 	dstRemote, dstFileName, err := fspath.Split(args[0])
 	if err != nil {
-		log.Fatalf("Parsing %q failed: %v", args[0], err)
+		fs.Fatalf(nil, "Parsing %q failed: %v", args[0], err)
 	}
 	if dstRemote == "" {
 		dstRemote = "."
 	}
 	if dstFileName == "" {
-		log.Fatalf("%q is a directory", args[0])
+		fs.Fatalf(nil, "%q is a directory", args[0])
 	}
 	fdst = newFsDir(dstRemote)
 	return
@@ -328,9 +327,9 @@ func Run(Retry bool, showStats bool, cmd *cobra.Command, f func() error) {
 	if cmdErr != nil {
 		nerrs := accounting.GlobalStats().GetErrors()
 		if nerrs <= 1 {
-			log.Printf("Failed to %s: %v", cmd.Name(), cmdErr)
+			fs.Logf(nil, "Failed to %s: %v", cmd.Name(), cmdErr)
 		} else {
-			log.Printf("Failed to %s with %d errors: last error was: %v", cmd.Name(), nerrs, cmdErr)
+			fs.Logf(nil, "Failed to %s with %d errors: last error was: %v", cmd.Name(), nerrs, cmdErr)
 		}
 	}
 	resolveExitCode(cmdErr)
@@ -383,7 +382,7 @@ func initConfig() {
 	// Set the global options from the flags
 	err := fs.GlobalOptionsInit()
 	if err != nil {
-		log.Fatalf("Failed to initialise global options: %v", err)
+		fs.Fatalf(nil, "Failed to initialise global options: %v", err)
 	}
 
 	ctx := context.Background()
@@ -423,7 +422,7 @@ func initConfig() {
 	// Start the remote control server if configured
 	_, err = rcserver.Start(context.Background(), &rc.Opt)
 	if err != nil {
-		log.Fatalf("Failed to start remote control: %v", err)
+		fs.Fatalf(nil, "Failed to start remote control: %v", err)
 	}
 
 	// Setup CPU profiling if desired
@@ -432,19 +431,19 @@ func initConfig() {
 		f, err := os.Create(*cpuProfile)
 		if err != nil {
 			err = fs.CountError(err)
-			log.Fatal(err)
+			fs.Fatal(nil, fmt.Sprint(err))
 		}
 		err = pprof.StartCPUProfile(f)
 		if err != nil {
 			err = fs.CountError(err)
-			log.Fatal(err)
+			fs.Fatal(nil, fmt.Sprint(err))
 		}
 		atexit.Register(func() {
 			pprof.StopCPUProfile()
 			err := f.Close()
 			if err != nil {
 				err = fs.CountError(err)
-				log.Fatal(err)
+				fs.Fatal(nil, fmt.Sprint(err))
 			}
 		})
 	}
@@ -456,17 +455,17 @@ func initConfig() {
 			f, err := os.Create(*memProfile)
 			if err != nil {
 				err = fs.CountError(err)
-				log.Fatal(err)
+				fs.Fatal(nil, fmt.Sprint(err))
 			}
 			err = pprof.WriteHeapProfile(f)
 			if err != nil {
 				err = fs.CountError(err)
-				log.Fatal(err)
+				fs.Fatal(nil, fmt.Sprint(err))
 			}
 			err = f.Close()
 			if err != nil {
 				err = fs.CountError(err)
-				log.Fatal(err)
+				fs.Fatal(nil, fmt.Sprint(err))
 			}
 		})
 	}
@@ -530,6 +529,6 @@ func Main() {
 		if strings.HasPrefix(err.Error(), "unknown command") && selfupdateEnabled {
 			Root.PrintErrf("You could use '%s selfupdate' to get latest features.\n\n", Root.CommandPath())
 		}
-		log.Fatalf("Fatal error: %v", err)
+		fs.Fatalf(nil, "Fatal error: %v", err)
 	}
 }
diff --git a/cmd/dedupe/dedupe.go b/cmd/dedupe/dedupe.go
index 38c6e41a0ce8c..e280306eccbda 100644
--- a/cmd/dedupe/dedupe.go
+++ b/cmd/dedupe/dedupe.go
@@ -3,7 +3,7 @@ package dedupe
 
 import (
 	"context"
-	"log"
+	"fmt"
 
 	"github.com/rclone/rclone/cmd"
 	"github.com/rclone/rclone/fs"
@@ -142,7 +142,7 @@ Or
 		if len(args) > 1 {
 			err := dedupeMode.Set(args[0])
 			if err != nil {
-				log.Fatal(err)
+				fs.Fatal(nil, fmt.Sprint(err))
 			}
 			args = args[1:]
 		}
diff --git a/cmd/genautocomplete/genautocomplete_bash.go b/cmd/genautocomplete/genautocomplete_bash.go
index f9eb83a685dcc..e4c08d662c7df 100644
--- a/cmd/genautocomplete/genautocomplete_bash.go
+++ b/cmd/genautocomplete/genautocomplete_bash.go
@@ -1,10 +1,11 @@
 package genautocomplete
 
 import (
-	"log"
+	"fmt"
 	"os"
 
 	"github.com/rclone/rclone/cmd"
+	"github.com/rclone/rclone/fs"
 	"github.com/spf13/cobra"
 )
 
@@ -50,7 +51,7 @@ current shell.
 			if args[0] == "-" {
 				err := cmd.Root.GenBashCompletionV2(os.Stdout, false)
 				if err != nil {
-					log.Fatal(err)
+					fs.Fatal(nil, fmt.Sprint(err))
 				}
 				return
 			}
@@ -58,7 +59,7 @@ current shell.
 		}
 		err := cmd.Root.GenBashCompletionFileV2(out, false)
 		if err != nil {
-			log.Fatal(err)
+			fs.Fatal(nil, fmt.Sprint(err))
 		}
 	},
 }
diff --git a/cmd/genautocomplete/genautocomplete_fish.go b/cmd/genautocomplete/genautocomplete_fish.go
index 8a419fb9169bd..e8a8014e2e725 100644
--- a/cmd/genautocomplete/genautocomplete_fish.go
+++ b/cmd/genautocomplete/genautocomplete_fish.go
@@ -1,10 +1,11 @@
 package genautocomplete
 
 import (
-	"log"
+	"fmt"
 	"os"
 
 	"github.com/rclone/rclone/cmd"
+	"github.com/rclone/rclone/fs"
 	"github.com/spf13/cobra"
 )
 
@@ -39,7 +40,7 @@ If output_file is "-", then the output will be written to stdout.
 			if args[0] == "-" {
 				err := cmd.Root.GenFishCompletion(os.Stdout, true)
 				if err != nil {
-					log.Fatal(err)
+					fs.Fatal(nil, fmt.Sprint(err))
 				}
 				return
 			}
@@ -47,7 +48,7 @@ If output_file is "-", then the output will be written to stdout.
 		}
 		err := cmd.Root.GenFishCompletionFile(out, true)
 		if err != nil {
-			log.Fatal(err)
+			fs.Fatal(nil, fmt.Sprint(err))
 		}
 	},
 }
diff --git a/cmd/genautocomplete/genautocomplete_powershell.go b/cmd/genautocomplete/genautocomplete_powershell.go
index e07bd70b62077..0fda7d730628f 100644
--- a/cmd/genautocomplete/genautocomplete_powershell.go
+++ b/cmd/genautocomplete/genautocomplete_powershell.go
@@ -1,10 +1,11 @@
 package genautocomplete
 
 import (
-	"log"
+	"fmt"
 	"os"
 
 	"github.com/rclone/rclone/cmd"
+	"github.com/rclone/rclone/fs"
 	"github.com/spf13/cobra"
 )
 
@@ -31,13 +32,13 @@ If output_file is "-" or missing, then the output will be written to stdout.
 		if len(args) == 0 || (len(args) > 0 && args[0] == "-") {
 			err := cmd.Root.GenPowerShellCompletion(os.Stdout)
 			if err != nil {
-				log.Fatal(err)
+				fs.Fatal(nil, fmt.Sprint(err))
 			}
 			return
 		}
 		err := cmd.Root.GenPowerShellCompletionFile(args[0])
 		if err != nil {
-			log.Fatal(err)
+			fs.Fatal(nil, fmt.Sprint(err))
 		}
 	},
 }
diff --git a/cmd/genautocomplete/genautocomplete_zsh.go b/cmd/genautocomplete/genautocomplete_zsh.go
index 23333fb7c14a4..ef80254e857f2 100644
--- a/cmd/genautocomplete/genautocomplete_zsh.go
+++ b/cmd/genautocomplete/genautocomplete_zsh.go
@@ -1,10 +1,11 @@
 package genautocomplete
 
 import (
-	"log"
+	"fmt"
 	"os"
 
 	"github.com/rclone/rclone/cmd"
+	"github.com/rclone/rclone/fs"
 	"github.com/spf13/cobra"
 )
 
@@ -39,7 +40,7 @@ If output_file is "-", then the output will be written to stdout.
 			if args[0] == "-" {
 				err := cmd.Root.GenZshCompletion(os.Stdout)
 				if err != nil {
-					log.Fatal(err)
+					fs.Fatal(nil, fmt.Sprint(err))
 				}
 				return
 			}
@@ -47,12 +48,12 @@ If output_file is "-", then the output will be written to stdout.
 		}
 		outFile, err := os.Create(out)
 		if err != nil {
-			log.Fatal(err)
+			fs.Fatal(nil, fmt.Sprint(err))
 		}
 		defer func() { _ = outFile.Close() }()
 		err = cmd.Root.GenZshCompletion(outFile)
 		if err != nil {
-			log.Fatal(err)
+			fs.Fatal(nil, fmt.Sprint(err))
 		}
 	},
 }
diff --git a/cmd/gendocs/gendocs.go b/cmd/gendocs/gendocs.go
index 5864283db83df..ffd6e23fd47d4 100644
--- a/cmd/gendocs/gendocs.go
+++ b/cmd/gendocs/gendocs.go
@@ -4,7 +4,6 @@ package gendocs
 import (
 	"bytes"
 	"fmt"
-	"log"
 	"os"
 	"path"
 	"path/filepath"
@@ -14,6 +13,7 @@ import (
 	"time"
 
 	"github.com/rclone/rclone/cmd"
+	"github.com/rclone/rclone/fs"
 	"github.com/rclone/rclone/fs/config/flags"
 	"github.com/rclone/rclone/lib/file"
 	"github.com/spf13/cobra"
@@ -144,7 +144,7 @@ rclone.org website.`,
 			var buf bytes.Buffer
 			err := frontmatterTemplate.Execute(&buf, data)
 			if err != nil {
-				log.Fatalf("Failed to render frontmatter template: %v", err)
+				fs.Fatalf(nil, "Failed to render frontmatter template: %v", err)
 			}
 			return buf.String()
 		}
diff --git a/cmd/help.go b/cmd/help.go
index 55c89ea8a7c54..c9906d684fc8e 100644
--- a/cmd/help.go
+++ b/cmd/help.go
@@ -3,7 +3,6 @@ package cmd
 import (
 	"context"
 	"fmt"
-	"log"
 	"os"
 	"regexp"
 	"sort"
@@ -76,7 +75,7 @@ var helpFlags = &cobra.Command{
 			if len(args) > 0 {
 				re, err := filter.GlobStringToRegexp(args[0], false, true)
 				if err != nil {
-					log.Fatalf("Invalid flag filter: %v", err)
+					fs.Fatalf(nil, "Invalid flag filter: %v", err)
 				}
 				fs.Debugf(nil, "Flag filter: %s", re.String())
 				filterFlagsRe = re
@@ -286,7 +285,7 @@ func quoteString(v interface{}) string {
 func showBackend(name string) {
 	backend, err := fs.Find(name)
 	if err != nil {
-		log.Fatal(err)
+		fs.Fatal(nil, fmt.Sprint(err))
 	}
 	var standardOptions, advancedOptions fs.Options
 	done := map[string]struct{}{}
diff --git a/cmd/mount2/mount.go b/cmd/mount2/mount.go
index 4548705a5a84e..012142a295a1b 100644
--- a/cmd/mount2/mount.go
+++ b/cmd/mount2/mount.go
@@ -5,7 +5,6 @@ package mount2
 
 import (
 	"fmt"
-	"log"
 	"runtime"
 	"time"
 
@@ -150,7 +149,7 @@ func mountOptions(fsys *FS, f fs.Fs, opt *mountlib.Options) (mountOpts *fuse.Mou
 		opts = append(opts, "ro")
 	}
 	if fsys.opt.WritebackCache {
-		log.Printf("FIXME --write-back-cache not supported")
+		fs.Printf(nil, "FIXME --write-back-cache not supported")
 		// FIXME opts = append(opts,fuse.WritebackCache())
 	}
 	// Some OS X only options
diff --git a/cmd/mountlib/mount.go b/cmd/mountlib/mount.go
index 0e54193983973..275d5a4346807 100644
--- a/cmd/mountlib/mount.go
+++ b/cmd/mountlib/mount.go
@@ -5,7 +5,6 @@ import (
 	"context"
 	_ "embed"
 	"fmt"
-	"log"
 	"os"
 	"runtime"
 	"strings"
@@ -311,7 +310,7 @@ func NewMountCommand(commandName string, hidden bool, mount MountFn) *cobra.Comm
 					err = mnt.Wait()
 				}
 				if err != nil {
-					log.Fatalf("Fatal error: %v", err)
+					fs.Fatalf(nil, "Fatal error: %v", err)
 				}
 				return
 			}
@@ -339,7 +338,7 @@ func NewMountCommand(commandName string, hidden bool, mount MountFn) *cobra.Comm
 				atexit.Unregister(handle)
 			}
 			if err != nil {
-				log.Fatalf("Fatal error: %v", err)
+				fs.Fatalf(nil, "Fatal error: %v", err)
 			}
 		},
 	}
diff --git a/cmd/mountlib/rc.go b/cmd/mountlib/rc.go
index a896e8c5fcc7e..8c5c94d5d035c 100644
--- a/cmd/mountlib/rc.go
+++ b/cmd/mountlib/rc.go
@@ -3,7 +3,6 @@ package mountlib
 import (
 	"context"
 	"errors"
-	"log"
 	"sort"
 	"sync"
 	"time"
@@ -123,12 +122,12 @@ func mountRc(ctx context.Context, in rc.Params) (out rc.Params, err error) {
 	mnt := NewMountPoint(mountFn, mountPoint, fdst, &mountOpt, &vfsOpt)
 	_, err = mnt.Mount()
 	if err != nil {
-		log.Printf("mount FAILED: %v", err)
+		fs.Logf(nil, "mount FAILED: %v", err)
 		return nil, err
 	}
 	go func() {
 		if err = mnt.Wait(); err != nil {
-			log.Printf("unmount FAILED: %v", err)
+			fs.Logf(nil, "unmount FAILED: %v", err)
 			return
 		}
 		mountMu.Lock()
diff --git a/cmd/ncdu/ncdu.go b/cmd/ncdu/ncdu.go
index f326e8ad2323c..42117ab0d59f6 100644
--- a/cmd/ncdu/ncdu.go
+++ b/cmd/ncdu/ncdu.go
@@ -929,23 +929,23 @@ func (u *UI) Run() error {
 		return fmt.Errorf("screen init: %w", err)
 	}
 
-	// Hijack fs.LogPrint so that it doesn't corrupt the screen.
-	if logPrint := fs.LogPrint; !log.Redirected() {
+	// Hijack fs.LogOutput so that it doesn't corrupt the screen.
+	if logOutput := fs.LogOutput; !log.Redirected() {
 		type log struct {
 			text  string
 			level fs.LogLevel
 		}
 		var logs []log
-		fs.LogPrint = func(level fs.LogLevel, text string) {
+		fs.LogOutput = func(level fs.LogLevel, text string) {
 			if len(logs) > 100 {
 				logs = logs[len(logs)-100:]
 			}
 			logs = append(logs, log{level: level, text: text})
 		}
 		defer func() {
-			fs.LogPrint = logPrint
+			fs.LogOutput = logOutput
 			for i := range logs {
-				logPrint(logs[i].level, logs[i].text)
+				logOutput(logs[i].level, logs[i].text)
 			}
 		}()
 	}
diff --git a/cmd/progress.go b/cmd/progress.go
index fbdcd19a1ca90..679c1d30d3b53 100644
--- a/cmd/progress.go
+++ b/cmd/progress.go
@@ -28,12 +28,12 @@ const (
 // It returns a func which should be called to stop the stats.
 func startProgress() func() {
 	stopStats := make(chan struct{})
-	oldLogPrint := fs.LogPrint
+	oldLogOutput := fs.LogOutput
 	oldSyncPrint := operations.SyncPrintf
 
 	if !log.Redirected() {
 		// Intercept the log calls if not logging to file or syslog
-		fs.LogPrint = func(level fs.LogLevel, text string) {
+		fs.LogOutput = func(level fs.LogLevel, text string) {
 			printProgress(fmt.Sprintf("%s %-6s: %s", time.Now().Format(logTimeFormat), level, text))
 
 		}
@@ -60,7 +60,7 @@ func startProgress() func() {
 			case <-stopStats:
 				ticker.Stop()
 				printProgress("")
-				fs.LogPrint = oldLogPrint
+				fs.LogOutput = oldLogOutput
 				operations.SyncPrintf = oldSyncPrint
 				fmt.Println("")
 				return
diff --git a/cmd/rcat/rcat.go b/cmd/rcat/rcat.go
index bb53c2a2360c6..3d767d115a9ce 100644
--- a/cmd/rcat/rcat.go
+++ b/cmd/rcat/rcat.go
@@ -3,11 +3,11 @@ package rcat
 
 import (
 	"context"
-	"log"
 	"os"
 	"time"
 
 	"github.com/rclone/rclone/cmd"
+	"github.com/rclone/rclone/fs"
 	"github.com/rclone/rclone/fs/config/flags"
 	"github.com/rclone/rclone/fs/operations"
 	"github.com/spf13/cobra"
@@ -64,7 +64,7 @@ destination which can use retries.`,
 
 		stat, _ := os.Stdin.Stat()
 		if (stat.Mode() & os.ModeCharDevice) != 0 {
-			log.Fatalf("nothing to read from standard input (stdin).")
+			fs.Fatalf(nil, "nothing to read from standard input (stdin).")
 		}
 
 		fdst, dstFileName := cmd.NewFsDstFile(args)
diff --git a/cmd/rcd/rcd.go b/cmd/rcd/rcd.go
index 1dc875d283050..391a63321dd80 100644
--- a/cmd/rcd/rcd.go
+++ b/cmd/rcd/rcd.go
@@ -3,9 +3,9 @@ package rcd
 
 import (
 	"context"
-	"log"
 
 	"github.com/rclone/rclone/cmd"
+	"github.com/rclone/rclone/fs"
 	"github.com/rclone/rclone/fs/rc"
 	"github.com/rclone/rclone/fs/rc/rcflags"
 	"github.com/rclone/rclone/fs/rc/rcserver"
@@ -39,7 +39,7 @@ See the [rc documentation](/rc/) for more info on the rc flags.
 	Run: func(command *cobra.Command, args []string) {
 		cmd.CheckArgs(0, 1, command, args)
 		if rc.Opt.Enabled {
-			log.Fatalf("Don't supply --rc flag when using rcd")
+			fs.Fatalf(nil, "Don't supply --rc flag when using rcd")
 		}
 
 		// Start the rc
@@ -50,10 +50,10 @@ See the [rc documentation](/rc/) for more info on the rc flags.
 
 		s, err := rcserver.Start(context.Background(), &rc.Opt)
 		if err != nil {
-			log.Fatalf("Failed to start remote control: %v", err)
+			fs.Fatalf(nil, "Failed to start remote control: %v", err)
 		}
 		if s == nil {
-			log.Fatal("rc server not configured")
+			fs.Fatal(nil, "rc server not configured")
 		}
 
 		// Notify stopping on exit
diff --git a/cmd/selfupdate/selfupdate.go b/cmd/selfupdate/selfupdate.go
index 6e0b61e8d4e15..cca72a81bf726 100644
--- a/cmd/selfupdate/selfupdate.go
+++ b/cmd/selfupdate/selfupdate.go
@@ -14,7 +14,6 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	"log"
 	"net/http"
 	"os"
 	"os/exec"
@@ -83,19 +82,19 @@ var cmdSelfUpdate = &cobra.Command{
 		}
 		if Opt.Package != "zip" {
 			if Opt.Package != "deb" && Opt.Package != "rpm" {
-				log.Fatalf("--package should be one of zip|deb|rpm")
+				fs.Fatalf(nil, "--package should be one of zip|deb|rpm")
 			}
 			if runtime.GOOS != "linux" {
-				log.Fatalf(".deb and .rpm packages are supported only on Linux")
+				fs.Fatalf(nil, ".deb and .rpm packages are supported only on Linux")
 			} else if os.Geteuid() != 0 && !Opt.Check {
-				log.Fatalf(".deb and .rpm must be installed by root")
+				fs.Fatalf(nil, ".deb and .rpm must be installed by root")
 			}
 			if Opt.Output != "" && !Opt.Check {
 				fmt.Println("Warning: --output is ignored with --package deb|rpm")
 			}
 		}
 		if err := InstallUpdate(context.Background(), &Opt); err != nil {
-			log.Fatalf("Error: %v", err)
+			fs.Fatalf(nil, "Error: %v", err)
 		}
 	},
 }
diff --git a/cmd/serve/dlna/cds.go b/cmd/serve/dlna/cds.go
index 94b37bf18e1bb..2e2752c73d0d7 100644
--- a/cmd/serve/dlna/cds.go
+++ b/cmd/serve/dlna/cds.go
@@ -5,7 +5,6 @@ import (
 	"encoding/xml"
 	"errors"
 	"fmt"
-	"log"
 	"net/http"
 	"net/url"
 	"os"
@@ -360,7 +359,7 @@ func (o *object) FilePath() string {
 // Returns the ObjectID for the object. This is used in various ContentDirectory actions.
 func (o object) ID() string {
 	if !path.IsAbs(o.Path) {
-		log.Panicf("Relative object path: %s", o.Path)
+		fs.Panicf(nil, "Relative object path: %s", o.Path)
 	}
 	if len(o.Path) == 1 {
 		return "0"
diff --git a/cmd/serve/dlna/dlna_util.go b/cmd/serve/dlna/dlna_util.go
index d54dfea5b942f..a63f31f5bbdcb 100644
--- a/cmd/serve/dlna/dlna_util.go
+++ b/cmd/serve/dlna/dlna_util.go
@@ -5,7 +5,6 @@ import (
 	"encoding/xml"
 	"fmt"
 	"io"
-	"log"
 	"net"
 	"net/http"
 	"net/http/httptest"
@@ -31,7 +30,7 @@ func makeDefaultFriendlyName() string {
 func makeDeviceUUID(unique string) string {
 	h := md5.New()
 	if _, err := io.WriteString(h, unique); err != nil {
-		log.Panicf("makeDeviceUUID write failed: %s", err)
+		fs.Panicf(nil, "makeDeviceUUID write failed: %s", err)
 	}
 	buf := h.Sum(nil)
 	return upnp.FormatUUID(buf)
@@ -41,7 +40,7 @@ func makeDeviceUUID(unique string) string {
 func listInterfaces() []net.Interface {
 	ifs, err := net.Interfaces()
 	if err != nil {
-		log.Printf("list network interfaces: %v", err)
+		fs.Logf(nil, "list network interfaces: %v", err)
 		return []net.Interface{}
 	}
 
@@ -71,7 +70,7 @@ func didlLite(chardata string) string {
 func mustMarshalXML(value interface{}) []byte {
 	ret, err := xml.MarshalIndent(value, "", "  ")
 	if err != nil {
-		log.Panicf("mustMarshalXML failed to marshal %v: %s", value, err)
+		fs.Panicf(nil, "mustMarshalXML failed to marshal %v: %s", value, err)
 	}
 	return ret
 }
diff --git a/cmd/serve/http/http.go b/cmd/serve/http/http.go
index 5ffa2d0a30bae..9ace8e663d2cd 100644
--- a/cmd/serve/http/http.go
+++ b/cmd/serve/http/http.go
@@ -6,7 +6,6 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	"log"
 	"net/http"
 	"os"
 	"path"
@@ -92,7 +91,7 @@ control the stats printing.
 		cmd.Run(false, true, command, func() error {
 			s, err := run(context.Background(), f, Opt)
 			if err != nil {
-				log.Fatal(err)
+				fs.Fatal(nil, fmt.Sprint(err))
 			}
 
 			defer systemd.Notify()()
diff --git a/cmd/siginfo_bsd.go b/cmd/siginfo_bsd.go
index 2f5798f53aa35..1144459d6dc31 100644
--- a/cmd/siginfo_bsd.go
+++ b/cmd/siginfo_bsd.go
@@ -3,11 +3,11 @@
 package cmd
 
 import (
-	"log"
 	"os"
 	"os/signal"
 	"syscall"
 
+	"github.com/rclone/rclone/fs"
 	"github.com/rclone/rclone/fs/accounting"
 )
 
@@ -17,7 +17,7 @@ func SigInfoHandler() {
 	signal.Notify(signals, syscall.SIGINFO)
 	go func() {
 		for range signals {
-			log.Printf("%v\n", accounting.GlobalStats())
+			fs.Printf(nil, "%v\n", accounting.GlobalStats())
 		}
 	}()
 }
diff --git a/cmd/touch/touch.go b/cmd/touch/touch.go
index 4290c47bd4247..913d052a85926 100644
--- a/cmd/touch/touch.go
+++ b/cmd/touch/touch.go
@@ -6,7 +6,6 @@ import (
 	"context"
 	"errors"
 	"fmt"
-	"log"
 	"time"
 
 	"github.com/rclone/rclone/cmd"
@@ -86,7 +85,7 @@ then add the ` + "`--localtime`" + ` flag.
 func newFsDst(args []string) (f fs.Fs, remote string) {
 	root, remote, err := fspath.Split(args[0])
 	if err != nil {
-		log.Fatalf("Parsing %q failed: %v", args[0], err)
+		fs.Fatalf(nil, "Parsing %q failed: %v", args[0], err)
 	}
 	if root == "" {
 		root = "."
diff --git a/fs/config/config.go b/fs/config/config.go
index be0a2a914120c..0e078e131fc68 100644
--- a/fs/config/config.go
+++ b/fs/config/config.go
@@ -6,7 +6,6 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
-	"log"
 	mathrand "math/rand"
 	"os"
 	"path/filepath"
@@ -372,7 +371,7 @@ func LoadedData() Storage {
 			}
 			dataLoaded = true
 		} else {
-			log.Fatalf("Failed to load config file %q: %v", configPath, err)
+			fs.Fatalf(nil, "Failed to load config file %q: %v", configPath, err)
 		}
 	}
 	return data
diff --git a/fs/config/config_read_password.go b/fs/config/config_read_password.go
index 8f3c79933079d..688126fb17471 100644
--- a/fs/config/config_read_password.go
+++ b/fs/config/config_read_password.go
@@ -7,9 +7,9 @@ package config
 
 import (
 	"fmt"
-	"log"
 	"os"
 
+	"github.com/rclone/rclone/fs"
 	"github.com/rclone/rclone/lib/terminal"
 )
 
@@ -22,7 +22,7 @@ func ReadPassword() string {
 	line, err := terminal.ReadPassword(stdin)
 	_, _ = fmt.Fprintln(os.Stderr)
 	if err != nil {
-		log.Fatalf("Failed to read password: %v", err)
+		fs.Fatalf(nil, "Failed to read password: %v", err)
 	}
 	return string(line)
 }
diff --git a/fs/config/configflags/configflags.go b/fs/config/configflags/configflags.go
index 483e41a67f6e9..ab8f102866477 100644
--- a/fs/config/configflags/configflags.go
+++ b/fs/config/configflags/configflags.go
@@ -5,7 +5,6 @@ package configflags
 // Options set by command line flags
 import (
 	"context"
-	"log"
 	"net"
 	"os"
 	"strconv"
@@ -68,7 +67,7 @@ func ParseHeaders(headers []string) []*fs.HTTPOption {
 	for _, header := range headers {
 		parts := strings.SplitN(header, ":", 2)
 		if len(parts) == 1 {
-			log.Fatalf("Failed to parse '%s' as an HTTP header. Expecting a string like: 'Content-Encoding: gzip'", header)
+			fs.Fatalf(nil, "Failed to parse '%s' as an HTTP header. Expecting a string like: 'Content-Encoding: gzip'", header)
 		}
 		option := &fs.HTTPOption{
 			Key:   strings.TrimSpace(parts[0]),
@@ -101,7 +100,7 @@ func SetFlags(ci *fs.ConfigInfo) {
 	// Process -q flag
 	if quiet {
 		if verbose > 0 {
-			log.Fatalf("Can't set -v and -q")
+			fs.Fatalf(nil, "Can't set -v and -q")
 		}
 		ci.LogLevel = fs.LogLevelError
 	}
@@ -110,10 +109,10 @@ func SetFlags(ci *fs.ConfigInfo) {
 	logLevelFlag := pflag.Lookup("log-level")
 	if logLevelFlag != nil && logLevelFlag.Changed {
 		if verbose > 0 {
-			log.Fatalf("Can't set -v and --log-level")
+			fs.Fatalf(nil, "Can't set -v and --log-level")
 		}
 		if quiet {
-			log.Fatalf("Can't set -q and --log-level")
+			fs.Fatalf(nil, "Can't set -q and --log-level")
 		}
 	}
 
@@ -121,7 +120,7 @@ func SetFlags(ci *fs.ConfigInfo) {
 	switch {
 	case deleteBefore && (deleteDuring || deleteAfter),
 		deleteDuring && deleteAfter:
-		log.Fatalf(`Only one of --delete-before, --delete-during or --delete-after can be used.`)
+		fs.Fatalf(nil, `Only one of --delete-before, --delete-during or --delete-after can be used.`)
 	case deleteBefore:
 		ci.DeleteMode = fs.DeleteModeBefore
 	case deleteDuring:
@@ -136,10 +135,10 @@ func SetFlags(ci *fs.ConfigInfo) {
 	if bindAddr != "" {
 		addrs, err := net.LookupIP(bindAddr)
 		if err != nil {
-			log.Fatalf("--bind: Failed to parse %q as IP address: %v", bindAddr, err)
+			fs.Fatalf(nil, "--bind: Failed to parse %q as IP address: %v", bindAddr, err)
 		}
 		if len(addrs) != 1 {
-			log.Fatalf("--bind: Expecting 1 IP address for %q but got %d", bindAddr, len(addrs))
+			fs.Fatalf(nil, "--bind: Expecting 1 IP address for %q but got %d", bindAddr, len(addrs))
 		}
 		ci.BindAddr = addrs[0]
 	}
@@ -147,7 +146,7 @@ func SetFlags(ci *fs.ConfigInfo) {
 	// Process --disable
 	if disableFeatures != "" {
 		if disableFeatures == "help" {
-			log.Fatalf("Possible backend features are: %s\n", strings.Join(new(fs.Features).List(), ", "))
+			fs.Fatalf(nil, "Possible backend features are: %s\n", strings.Join(new(fs.Features).List(), ", "))
 		}
 		ci.DisableFeatures = strings.Split(disableFeatures, ",")
 	}
@@ -169,7 +168,7 @@ func SetFlags(ci *fs.ConfigInfo) {
 		for _, kv := range metadataSet {
 			equal := strings.IndexRune(kv, '=')
 			if equal < 0 {
-				log.Fatalf("Failed to parse '%s' as metadata key=value.", kv)
+				fs.Fatalf(nil, "Failed to parse '%s' as metadata key=value.", kv)
 			}
 			ci.MetadataSet[strings.ToLower(kv[:equal])] = kv[equal+1:]
 		}
@@ -181,23 +180,23 @@ func SetFlags(ci *fs.ConfigInfo) {
 		if value, ok := parseDSCP(dscp); ok {
 			ci.TrafficClass = value << 2
 		} else {
-			log.Fatalf("--dscp: Invalid DSCP name: %v", dscp)
+			fs.Fatalf(nil, "--dscp: Invalid DSCP name: %v", dscp)
 		}
 	}
 
 	// Process --config path
 	if err := config.SetConfigPath(configPath); err != nil {
-		log.Fatalf("--config: Failed to set %q as config path: %v", configPath, err)
+		fs.Fatalf(nil, "--config: Failed to set %q as config path: %v", configPath, err)
 	}
 
 	// Process --cache-dir path
 	if err := config.SetCacheDir(cacheDir); err != nil {
-		log.Fatalf("--cache-dir: Failed to set %q as cache dir: %v", cacheDir, err)
+		fs.Fatalf(nil, "--cache-dir: Failed to set %q as cache dir: %v", cacheDir, err)
 	}
 
 	// Process --temp-dir path
 	if err := config.SetTempDir(tempDir); err != nil {
-		log.Fatalf("--temp-dir: Failed to set %q as temp dir: %v", tempDir, err)
+		fs.Fatalf(nil, "--temp-dir: Failed to set %q as temp dir: %v", tempDir, err)
 	}
 
 	// Process --multi-thread-streams - set whether multi-thread-streams was set
@@ -206,7 +205,7 @@ func SetFlags(ci *fs.ConfigInfo) {
 
 	// Reload any changes
 	if err := ci.Reload(context.Background()); err != nil {
-		log.Fatalf("Failed to reload config changes: %v", err)
+		fs.Fatalf(nil, "Failed to reload config changes: %v", err)
 	}
 }
 
diff --git a/fs/config/flags/flags.go b/fs/config/flags/flags.go
index db76f5d71c253..cc2ed5cd067a2 100644
--- a/fs/config/flags/flags.go
+++ b/fs/config/flags/flags.go
@@ -3,7 +3,6 @@
 package flags
 
 import (
-	"log"
 	"os"
 	"regexp"
 	"strings"
@@ -74,7 +73,7 @@ func (gs *Groups) Include(groupsString string) *Groups {
 	for _, groupName := range strings.Split(groupsString, ",") {
 		_, ok := All.ByName[groupName]
 		if !ok {
-			log.Fatalf("Couldn't find group %q in command annotation", groupName)
+			fs.Fatalf(nil, "Couldn't find group %q in command annotation", groupName)
 		}
 		want[groupName] = true
 	}
@@ -137,7 +136,7 @@ func installFlag(flags *pflag.FlagSet, name string, groupsString string) {
 	// Find flag
 	flag := flags.Lookup(name)
 	if flag == nil {
-		log.Fatalf("Couldn't find flag --%q", name)
+		fs.Fatalf(nil, "Couldn't find flag --%q", name)
 	}
 
 	// Read default from environment if possible
@@ -145,7 +144,7 @@ func installFlag(flags *pflag.FlagSet, name string, groupsString string) {
 	if envValue, envFound := os.LookupEnv(envKey); envFound {
 		err := flags.Set(name, envValue)
 		if err != nil {
-			log.Fatalf("Invalid value when setting --%s from environment variable %s=%q: %v", name, envKey, envValue, err)
+			fs.Fatalf(nil, "Invalid value when setting --%s from environment variable %s=%q: %v", name, envKey, envValue, err)
 		}
 		fs.Debugf(nil, "Setting --%s %q from environment variable %s=%q", name, flag.Value, envKey, envValue)
 		flag.DefValue = envValue
@@ -159,7 +158,7 @@ func installFlag(flags *pflag.FlagSet, name string, groupsString string) {
 			}
 			group, ok := All.ByName[groupName]
 			if !ok {
-				log.Fatalf("Couldn't find group %q for flag --%s", groupName, name)
+				fs.Fatalf(nil, "Couldn't find group %q for flag --%s", groupName, name)
 			}
 			group.Add(flag)
 		}
diff --git a/fs/config/obscure/obscure.go b/fs/config/obscure/obscure.go
index 68aa9cc5ff87a..713e4d24878ca 100644
--- a/fs/config/obscure/obscure.go
+++ b/fs/config/obscure/obscure.go
@@ -9,8 +9,9 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	"log"
 	"math"
+
+	"github.com/rclone/rclone/fs"
 )
 
 // crypt internals
@@ -66,7 +67,7 @@ func Obscure(x string) (string, error) {
 func MustObscure(x string) string {
 	out, err := Obscure(x)
 	if err != nil {
-		log.Fatalf("Obscure failed: %v", err)
+		fs.Fatalf(nil, "Obscure failed: %v", err)
 	}
 	return out
 }
@@ -92,7 +93,7 @@ func Reveal(x string) (string, error) {
 func MustReveal(x string) string {
 	out, err := Reveal(x)
 	if err != nil {
-		log.Fatalf("Reveal failed: %v", err)
+		fs.Fatalf(nil, "Reveal failed: %v", err)
 	}
 	return out
 }
diff --git a/fs/config/ui.go b/fs/config/ui.go
index b08e9481c290c..fcee469b13ff8 100644
--- a/fs/config/ui.go
+++ b/fs/config/ui.go
@@ -7,7 +7,6 @@ import (
 	"context"
 	"errors"
 	"fmt"
-	"log"
 	"os"
 	"sort"
 	"strconv"
@@ -29,7 +28,7 @@ var ReadLine = func() string {
 	buf := bufio.NewReader(os.Stdin)
 	line, err := buf.ReadString('\n')
 	if err != nil {
-		log.Fatalf("Failed to read line: %v", err)
+		fs.Fatalf(nil, "Failed to read line: %v", err)
 	}
 	return strings.TrimSpace(line)
 }
@@ -233,7 +232,7 @@ func ChoosePassword(defaultValue string, required bool) string {
 			bits := ChooseNumber("Bits", 64, 1024)
 			password, err = Password(bits)
 			if err != nil {
-				log.Fatalf("Failed to make password: %v", err)
+				fs.Fatalf(nil, "Failed to make password: %v", err)
 			}
 			fmt.Printf("Your password is: %s\n", password)
 			fmt.Printf("Use this password? Please note that an obscured version of this \npassword (and not the " +
@@ -297,7 +296,7 @@ func ChooseRemote() string {
 func mustFindByName(name string) *fs.RegInfo {
 	fsType := GetValue(name, "type")
 	if fsType == "" {
-		log.Fatalf("Couldn't find type of fs for %q", name)
+		fs.Fatalf(nil, "Couldn't find type of fs for %q", name)
 	}
 	return fs.MustFind(fsType)
 }
@@ -654,7 +653,7 @@ func ShowConfigLocation() {
 func ShowConfig() {
 	str, err := LoadedData().Serialize()
 	if err != nil {
-		log.Fatalf("Failed to serialize config: %v", err)
+		fs.Fatalf(nil, "Failed to serialize config: %v", err)
 	}
 	if str == "" {
 		str = "; empty config\n"
diff --git a/fs/filter/filter.go b/fs/filter/filter.go
index 61c1a2556b53e..ebaa681191752 100644
--- a/fs/filter/filter.go
+++ b/fs/filter/filter.go
@@ -5,7 +5,6 @@ import (
 	"context"
 	"errors"
 	"fmt"
-	"log"
 	"path"
 	"strings"
 	"time"
@@ -190,7 +189,7 @@ func NewFilter(opt *Options) (f *Filter, err error) {
 	if f.Opt.MaxAge.IsSet() {
 		f.ModTimeFrom = time.Now().Add(-time.Duration(f.Opt.MaxAge))
 		if !f.ModTimeTo.IsZero() && f.ModTimeTo.Before(f.ModTimeFrom) {
-			log.Fatalf("filter: --min-age %q can't be larger than --max-age %q", opt.MinAge, opt.MaxAge)
+			fs.Fatalf(nil, "filter: --min-age %q can't be larger than --max-age %q", opt.MinAge, opt.MaxAge)
 		}
 		fs.Debugf(nil, "--max-age %v to %v", f.Opt.MaxAge, f.ModTimeFrom)
 	}
diff --git a/fs/fshttp/http.go b/fs/fshttp/http.go
index 5a3fea0739f83..9d405df1df23f 100644
--- a/fs/fshttp/http.go
+++ b/fs/fshttp/http.go
@@ -6,7 +6,6 @@ import (
 	"context"
 	"crypto/tls"
 	"crypto/x509"
-	"log"
 	"net"
 	"net/http"
 	"net/http/cookiejar"
@@ -71,17 +70,17 @@ func NewTransportCustom(ctx context.Context, customize func(*http.Transport)) ht
 	// Load client certs
 	if ci.ClientCert != "" || ci.ClientKey != "" {
 		if ci.ClientCert == "" || ci.ClientKey == "" {
-			log.Fatalf("Both --client-cert and --client-key must be set")
+			fs.Fatalf(nil, "Both --client-cert and --client-key must be set")
 		}
 		cert, err := tls.LoadX509KeyPair(ci.ClientCert, ci.ClientKey)
 		if err != nil {
-			log.Fatalf("Failed to load --client-cert/--client-key pair: %v", err)
+			fs.Fatalf(nil, "Failed to load --client-cert/--client-key pair: %v", err)
 		}
 		if cert.Leaf == nil {
 			// Leaf is always the first certificate
 			cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0])
 			if err != nil {
-				log.Fatalf("Failed to parse the certificate")
+				fs.Fatalf(nil, "Failed to parse the certificate")
 			}
 		}
 		t.TLSClientConfig.Certificates = []tls.Certificate{cert}
@@ -95,11 +94,11 @@ func NewTransportCustom(ctx context.Context, customize func(*http.Transport)) ht
 		for _, cert := range ci.CaCert {
 			caCert, err := os.ReadFile(cert)
 			if err != nil {
-				log.Fatalf("Failed to read --ca-cert file %q : %v", cert, err)
+				fs.Fatalf(nil, "Failed to read --ca-cert file %q : %v", cert, err)
 			}
 			ok := caCertPool.AppendCertsFromPEM(caCert)
 			if !ok {
-				log.Fatalf("Failed to add certificates from --ca-cert file %q", cert)
+				fs.Fatalf(nil, "Failed to add certificates from --ca-cert file %q", cert)
 			}
 		}
 		t.TLSClientConfig.RootCAs = caCertPool
@@ -303,7 +302,7 @@ func (t *Transport) reloadCertificates() {
 
 	cert, err := tls.LoadX509KeyPair(t.clientCert, t.clientKey)
 	if err != nil {
-		log.Fatalf("Failed to load --client-cert/--client-key pair: %v", err)
+		fs.Fatalf(nil, "Failed to load --client-cert/--client-key pair: %v", err)
 	}
 	// Check if we need to parse the certificate again, we need it
 	// for checking the expiration date
@@ -311,7 +310,7 @@ func (t *Transport) reloadCertificates() {
 		// Leaf is always the first certificate
 		cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0])
 		if err != nil {
-			log.Fatalf("Failed to parse the certificate")
+			fs.Fatalf(nil, "Failed to parse the certificate")
 		}
 	}
 	t.TLSClientConfig.Certificates = []tls.Certificate{cert}
diff --git a/fs/log.go b/fs/log.go
index 40ccdbb27f8ac..4b3abf6111d98 100644
--- a/fs/log.go
+++ b/fs/log.go
@@ -60,8 +60,8 @@ var LogPrintPid = false
 // InstallJSONLogger is a hook that --use-json-log calls
 var InstallJSONLogger = func(logLevel LogLevel) {}
 
-// LogPrint sends the text to the logger of level
-var LogPrint = func(level LogLevel, text string) {
+// LogOutput sends the text to the logger of level
+var LogOutput = func(level LogLevel, text string) {
 	text = fmt.Sprintf("%-6s: %s", level, text)
 	if LogPrintPid {
 		text = fmt.Sprintf("[%d] %s", os.Getpid(), text)
@@ -108,42 +108,82 @@ func (j LogValueItem) String() string {
 	return fmt.Sprint(j.value)
 }
 
-// LogPrintf produces a log string from the arguments passed in
-func LogPrintf(level LogLevel, o interface{}, text string, args ...interface{}) {
-	out := fmt.Sprintf(text, args...)
+func logLogrus(level LogLevel, text string, fields logrus.Fields) {
+	switch level {
+	case LogLevelDebug:
+		logrus.WithFields(fields).Debug(text)
+	case LogLevelInfo:
+		logrus.WithFields(fields).Info(text)
+	case LogLevelNotice, LogLevelWarning:
+		logrus.WithFields(fields).Warn(text)
+	case LogLevelError:
+		logrus.WithFields(fields).Error(text)
+	case LogLevelCritical:
+		logrus.WithFields(fields).Fatal(text)
+	case LogLevelEmergency, LogLevelAlert:
+		logrus.WithFields(fields).Panic(text)
+	}
+}
 
-	if GetConfig(context.TODO()).UseJSONLog {
-		fields := logrus.Fields{}
-		if o != nil {
-			fields = logrus.Fields{
-				"object":     fmt.Sprintf("%+v", o),
-				"objectType": fmt.Sprintf("%T", o),
-			}
+func logLogrusWithObject(level LogLevel, o interface{}, text string, fields logrus.Fields) {
+	if o != nil {
+		if fields == nil {
+			fields = logrus.Fields{}
 		}
-		for _, arg := range args {
-			if item, ok := arg.(LogValueItem); ok {
-				fields[item.key] = item.value
-			}
-		}
-		switch level {
-		case LogLevelDebug:
-			logrus.WithFields(fields).Debug(out)
-		case LogLevelInfo:
-			logrus.WithFields(fields).Info(out)
-		case LogLevelNotice, LogLevelWarning:
-			logrus.WithFields(fields).Warn(out)
-		case LogLevelError:
-			logrus.WithFields(fields).Error(out)
-		case LogLevelCritical:
-			logrus.WithFields(fields).Fatal(out)
-		case LogLevelEmergency, LogLevelAlert:
-			logrus.WithFields(fields).Panic(out)
+		fields["object"] = fmt.Sprintf("%+v", o)
+		fields["objectType"] = fmt.Sprintf("%T", o)
+	}
+	logLogrus(level, text, fields)
+}
+
+func logJSON(level LogLevel, o interface{}, text string) {
+	logLogrusWithObject(level, o, text, nil)
+}
+
+func logJSONf(level LogLevel, o interface{}, text string, args ...interface{}) {
+	text = fmt.Sprintf(text, args...)
+	fields := logrus.Fields{}
+	for _, arg := range args {
+		if item, ok := arg.(LogValueItem); ok {
+			fields[item.key] = item.value
 		}
+	}
+	logLogrusWithObject(level, o, text, fields)
+}
+
+func logPlain(level LogLevel, o interface{}, text string) {
+	if o != nil {
+		text = fmt.Sprintf("%v: %s", o, text)
+	}
+	LogOutput(level, text)
+}
+
+func logPlainf(level LogLevel, o interface{}, text string, args ...interface{}) {
+	logPlain(level, o, fmt.Sprintf(text, args...))
+}
+
+// LogPrint produces a log string from the arguments passed in
+func LogPrint(level LogLevel, o interface{}, text string) {
+	if GetConfig(context.TODO()).UseJSONLog {
+		logJSON(level, o, text)
 	} else {
-		if o != nil {
-			out = fmt.Sprintf("%v: %s", o, out)
-		}
-		LogPrint(level, out)
+		logPlain(level, o, text)
+	}
+}
+
+// LogPrintf produces a log string from the arguments passed in
+func LogPrintf(level LogLevel, o interface{}, text string, args ...interface{}) {
+	if GetConfig(context.TODO()).UseJSONLog {
+		logJSONf(level, o, text, args...)
+	} else {
+		logPlainf(level, o, text, args...)
+	}
+}
+
+// LogLevelPrint writes logs at the given level
+func LogLevelPrint(level LogLevel, o interface{}, text string) {
+	if GetConfig(context.TODO()).LogLevel >= level {
+		LogPrint(level, o, text)
 	}
 }
 
@@ -154,12 +194,71 @@ func LogLevelPrintf(level LogLevel, o interface{}, text string, args ...interfac
 	}
 }
 
+// Panic writes alert log output for this Object or Fs and calls panic().
+// It should always be seen by the user.
+func Panic(o interface{}, text string) {
+	if GetConfig(context.TODO()).LogLevel >= LogLevelAlert {
+		LogPrint(LogLevelAlert, o, text)
+	}
+	panic(text)
+}
+
+// Panicf writes alert log output for this Object or Fs and calls panic().
+// It should always be seen by the user.
+func Panicf(o interface{}, text string, args ...interface{}) {
+	if GetConfig(context.TODO()).LogLevel >= LogLevelAlert {
+		LogPrintf(LogLevelAlert, o, text, args...)
+	}
+	panic(fmt.Sprintf(text, args...))
+}
+
+// Fatal writes critical log output for this Object or Fs and calls os.Exit(1).
+// It should always be seen by the user.
+func Fatal(o interface{}, text string) {
+	if GetConfig(context.TODO()).LogLevel >= LogLevelCritical {
+		LogPrint(LogLevelCritical, o, text)
+	}
+	os.Exit(1)
+}
+
+// Fatalf writes critical log output for this Object or Fs and calls os.Exit(1).
+// It should always be seen by the user.
+func Fatalf(o interface{}, text string, args ...interface{}) {
+	if GetConfig(context.TODO()).LogLevel >= LogLevelCritical {
+		LogPrintf(LogLevelCritical, o, text, args...)
+	}
+	os.Exit(1)
+}
+
+// Error writes error log output for this Object or Fs.  It
+// should always be seen by the user.
+func Error(o interface{}, text string) {
+	LogLevelPrint(LogLevelError, o, text)
+}
+
 // Errorf writes error log output for this Object or Fs.  It
 // should always be seen by the user.
 func Errorf(o interface{}, text string, args ...interface{}) {
-	if GetConfig(context.TODO()).LogLevel >= LogLevelError {
-		LogPrintf(LogLevelError, o, text, args...)
-	}
+	LogLevelPrintf(LogLevelError, o, text, args...)
+}
+
+// Print writes log output for this Object or Fs, same as Logf.
+func Print(o interface{}, text string) {
+	LogLevelPrint(LogLevelNotice, o, text)
+}
+
+// Printf writes log output for this Object or Fs, same as Logf.
+func Printf(o interface{}, text string, args ...interface{}) {
+	LogLevelPrintf(LogLevelNotice, o, text, args...)
+}
+
+// Log writes log output for this Object or Fs.  This should be
+// considered to be Notice level logging.  It is the default level.
+// By default rclone should not log very much so only use this for
+// important things the user should see.  The user can filter these
+// out with the -q flag.
+func Log(o interface{}, text string) {
+	LogLevelPrint(LogLevelNotice, o, text)
 }
 
 // Logf writes log output for this Object or Fs.  This should be
@@ -168,26 +267,34 @@ func Errorf(o interface{}, text string, args ...interface{}) {
 // important things the user should see.  The user can filter these
 // out with the -q flag.
 func Logf(o interface{}, text string, args ...interface{}) {
-	if GetConfig(context.TODO()).LogLevel >= LogLevelNotice {
-		LogPrintf(LogLevelNotice, o, text, args...)
-	}
+	LogLevelPrintf(LogLevelNotice, o, text, args...)
+}
+
+// Infoc writes info on transfers for this Object or Fs.  Use this
+// level for logging transfers, deletions and things which should
+// appear with the -v flag.
+// There is name class on "Info", hence the name "Infoc", "c" for constant.
+func Infoc(o interface{}, text string) {
+	LogLevelPrint(LogLevelInfo, o, text)
 }
 
 // Infof writes info on transfers for this Object or Fs.  Use this
 // level for logging transfers, deletions and things which should
 // appear with the -v flag.
 func Infof(o interface{}, text string, args ...interface{}) {
-	if GetConfig(context.TODO()).LogLevel >= LogLevelInfo {
-		LogPrintf(LogLevelInfo, o, text, args...)
-	}
+	LogLevelPrintf(LogLevelInfo, o, text, args...)
+}
+
+// Debug writes debugging output for this Object or Fs.  Use this for
+// debug only.  The user must have to specify -vv to see this.
+func Debug(o interface{}, text string) {
+	LogLevelPrint(LogLevelDebug, o, text)
 }
 
 // Debugf writes debugging output for this Object or Fs.  Use this for
 // debug only.  The user must have to specify -vv to see this.
 func Debugf(o interface{}, text string, args ...interface{}) {
-	if GetConfig(context.TODO()).LogLevel >= LogLevelDebug {
-		LogPrintf(LogLevelDebug, o, text, args...)
-	}
+	LogLevelPrintf(LogLevelDebug, o, text, args...)
 }
 
 // LogDirName returns an object for the logger, logging a root
diff --git a/fs/log/log.go b/fs/log/log.go
index 0877f5d72cb6e..1400d4e7e03fe 100644
--- a/fs/log/log.go
+++ b/fs/log/log.go
@@ -144,7 +144,7 @@ func InitLogging() {
 	if Opt.File != "" {
 		f, err := os.OpenFile(Opt.File, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0640)
 		if err != nil {
-			log.Fatalf("Failed to open log file: %v", err)
+			fs.Fatalf(nil, "Failed to open log file: %v", err)
 		}
 		_, err = f.Seek(0, io.SeekEnd)
 		if err != nil {
@@ -158,7 +158,7 @@ func InitLogging() {
 	// Syslog output
 	if Opt.UseSyslog {
 		if Opt.File != "" {
-			log.Fatalf("Can't use --syslog and --log-file together")
+			fs.Fatalf(nil, "Can't use --syslog and --log-file together")
 		}
 		startSysLog()
 	}
diff --git a/fs/log/redirect_stderr_unix.go b/fs/log/redirect_stderr_unix.go
index f37a892048f39..dd7aa4d6f2faa 100644
--- a/fs/log/redirect_stderr_unix.go
+++ b/fs/log/redirect_stderr_unix.go
@@ -5,9 +5,9 @@
 package log
 
 import (
-	"log"
 	"os"
 
+	"github.com/rclone/rclone/fs"
 	"github.com/rclone/rclone/fs/config"
 	"golang.org/x/sys/unix"
 )
@@ -16,11 +16,11 @@ import (
 func redirectStderr(f *os.File) {
 	passPromptFd, err := unix.Dup(int(os.Stderr.Fd()))
 	if err != nil {
-		log.Fatalf("Failed to duplicate stderr: %v", err)
+		fs.Fatalf(nil, "Failed to duplicate stderr: %v", err)
 	}
 	config.PasswordPromptOutput = os.NewFile(uintptr(passPromptFd), "passPrompt")
 	err = unix.Dup2(int(f.Fd()), int(os.Stderr.Fd()))
 	if err != nil {
-		log.Fatalf("Failed to redirect stderr to file: %v", err)
+		fs.Fatalf(nil, "Failed to redirect stderr to file: %v", err)
 	}
 }
diff --git a/fs/log/redirect_stderr_windows.go b/fs/log/redirect_stderr_windows.go
index f20ede7b0a239..6d6c6abb8217d 100644
--- a/fs/log/redirect_stderr_windows.go
+++ b/fs/log/redirect_stderr_windows.go
@@ -9,9 +9,10 @@
 package log
 
 import (
-	"log"
 	"os"
 	"syscall"
+
+	"github.com/rclone/rclone/fs"
 )
 
 var (
@@ -34,6 +35,6 @@ func setStdHandle(stdhandle int32, handle syscall.Handle) error {
 func redirectStderr(f *os.File) {
 	err := setStdHandle(syscall.STD_ERROR_HANDLE, syscall.Handle(f.Fd()))
 	if err != nil {
-		log.Fatalf("Failed to redirect stderr to file: %v", err)
+		fs.Fatalf(nil, "Failed to redirect stderr to file: %v", err)
 	}
 }
diff --git a/fs/log/syslog.go b/fs/log/syslog.go
index 58b82ac9fdc8e..fd98f9fb61649 100644
--- a/fs/log/syslog.go
+++ b/fs/log/syslog.go
@@ -5,12 +5,13 @@
 package log
 
 import (
-	"log"
 	"runtime"
+
+	"github.com/rclone/rclone/fs"
 )
 
 // Starts syslog if configured, returns true if it was started
 func startSysLog() bool {
-	log.Fatalf("--syslog not supported on %s platform", runtime.GOOS)
+	fs.Fatalf(nil, "--syslog not supported on %s platform", runtime.GOOS)
 	return false
 }
diff --git a/fs/log/syslog_unix.go b/fs/log/syslog_unix.go
index 7d7efe8ee8c06..4fbbd20992dfa 100644
--- a/fs/log/syslog_unix.go
+++ b/fs/log/syslog_unix.go
@@ -42,16 +42,16 @@ var (
 func startSysLog() bool {
 	facility, ok := syslogFacilityMap[Opt.SyslogFacility]
 	if !ok {
-		log.Fatalf("Unknown syslog facility %q - man syslog for list", Opt.SyslogFacility)
+		fs.Fatalf(nil, "Unknown syslog facility %q - man syslog for list", Opt.SyslogFacility)
 	}
 	Me := path.Base(os.Args[0])
 	w, err := syslog.New(syslog.LOG_NOTICE|facility, Me)
 	if err != nil {
-		log.Fatalf("Failed to start syslog: %v", err)
+		fs.Fatalf(nil, "Failed to start syslog: %v", err)
 	}
 	log.SetFlags(0)
 	log.SetOutput(w)
-	fs.LogPrint = func(level fs.LogLevel, text string) {
+	fs.LogOutput = func(level fs.LogLevel, text string) {
 		switch level {
 		case fs.LogLevelEmergency:
 			_ = w.Emerg(text)
diff --git a/fs/log/systemd.go b/fs/log/systemd.go
index 2caf675da1f93..84e9a3bd305cb 100644
--- a/fs/log/systemd.go
+++ b/fs/log/systemd.go
@@ -5,13 +5,14 @@
 package log
 
 import (
-	"log"
 	"runtime"
+
+	"github.com/rclone/rclone/fs"
 )
 
 // Enables systemd logs if configured or if auto-detected
 func startSystemdLog() bool {
-	log.Fatalf("--log-systemd not supported on %s platform", runtime.GOOS)
+	fs.Fatalf(nil, "--log-systemd not supported on %s platform", runtime.GOOS)
 	return false
 }
 
diff --git a/fs/log/systemd_unix.go b/fs/log/systemd_unix.go
index cf78d35a8419a..22c82ad11d888 100644
--- a/fs/log/systemd_unix.go
+++ b/fs/log/systemd_unix.go
@@ -26,7 +26,7 @@ func startSystemdLog() bool {
 	}
 	log.SetFlags(flags)
 	// TODO: Use the native journal.Print approach rather than a custom implementation
-	fs.LogPrint = func(level fs.LogLevel, text string) {
+	fs.LogOutput = func(level fs.LogLevel, text string) {
 		text = fmt.Sprintf("<%s>%-6s: %s", systemdLogPrefix(level), level, text)
 		_ = log.Output(4, text)
 	}
diff --git a/fs/mount_helper.go b/fs/mount_helper.go
index 035b75284c5da..ef81777fc57ea 100644
--- a/fs/mount_helper.go
+++ b/fs/mount_helper.go
@@ -3,7 +3,6 @@ package fs
 import (
 	"errors"
 	"fmt"
-	"log"
 	"os"
 	"path/filepath"
 	"runtime"
@@ -16,7 +15,7 @@ func init() {
 		if args, err := convertMountHelperArgs(os.Args); err == nil {
 			os.Args = args
 		} else {
-			log.Fatalf("Failed to parse command line: %v", err)
+			Fatalf(nil, "Failed to parse command line: %v", err)
 		}
 	}
 }
diff --git a/fs/operations/dedupe.go b/fs/operations/dedupe.go
index e5e356c723b82..3e0f72cd58fa2 100644
--- a/fs/operations/dedupe.go
+++ b/fs/operations/dedupe.go
@@ -5,7 +5,6 @@ package operations
 import (
 	"context"
 	"fmt"
-	"log"
 	"path"
 	"sort"
 	"strings"
@@ -21,7 +20,7 @@ import (
 func dedupeRename(ctx context.Context, f fs.Fs, remote string, objs []fs.Object) {
 	doMove := f.Features().Move
 	if doMove == nil {
-		log.Fatalf("Fs %v doesn't support Move", f)
+		fs.Fatalf(nil, "Fs %v doesn't support Move", f)
 	}
 	ext := path.Ext(remote)
 	base := remote[:len(remote)-len(ext)]
diff --git a/fs/rc/rcserver/rcserver.go b/fs/rc/rcserver/rcserver.go
index 88f82a484b7c3..1a082947d86b8 100644
--- a/fs/rc/rcserver/rcserver.go
+++ b/fs/rc/rcserver/rcserver.go
@@ -7,7 +7,6 @@ import (
 	"encoding/json"
 	"flag"
 	"fmt"
-	"log"
 	"mime"
 	"net/http"
 	"net/url"
@@ -105,7 +104,7 @@ func newServer(ctx context.Context, opt *rc.Options, mux *http.ServeMux) (*Serve
 			if opt.Auth.BasicPass == "" && opt.Auth.HtPasswd == "" {
 				randomPass, err := random.Password(128)
 				if err != nil {
-					log.Fatalf("Failed to make password: %v", err)
+					fs.Fatalf(nil, "Failed to make password: %v", err)
 				}
 				opt.Auth.BasicPass = randomPass
 				fs.Infof(nil, "No password specified. Using random password: %s \n", randomPass)
diff --git a/fs/registry.go b/fs/registry.go
index ad9191d430a2c..937d5c1f02150 100644
--- a/fs/registry.go
+++ b/fs/registry.go
@@ -6,7 +6,6 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
-	"log"
 	"reflect"
 	"regexp"
 	"sort"
@@ -408,7 +407,7 @@ func Find(name string) (*RegInfo, error) {
 func MustFind(name string) *RegInfo {
 	fs, err := Find(name)
 	if err != nil {
-		log.Fatalf("Failed to find remote: %v", err)
+		Fatalf(nil, "Failed to find remote: %v", err)
 	}
 	return fs
 }
@@ -434,7 +433,7 @@ func RegisterGlobalOptions(oi OptionsInfo) {
 	if oi.Opt != nil && oi.Options != nil {
 		err := oi.Check()
 		if err != nil {
-			log.Fatalf("%v", err)
+			Fatalf(nil, "%v", err)
 		}
 	}
 	// Load the default values into the options.
@@ -446,7 +445,7 @@ func RegisterGlobalOptions(oi OptionsInfo) {
 	// again when the flags are ready.
 	err := oi.load()
 	if err != nil {
-		log.Fatalf("Failed to load %q default values: %v", oi.Name, err)
+		Fatalf(nil, "Failed to load %q default values: %v", oi.Name, err)
 	}
 }
 
diff --git a/go.mod b/go.mod
index e860ff31a5cca..1e55be9257f26 100644
--- a/go.mod
+++ b/go.mod
@@ -58,6 +58,7 @@ require (
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2
 	github.com/prometheus/client_golang v1.19.1
 	github.com/putdotio/go-putio/putio v0.0.0-20200123120452-16d982cac2b8
+	github.com/quasilyte/go-ruleguard/dsl v0.3.22
 	github.com/rclone/gofakes3 v0.0.3-0.20240807151802-e80146f8de87
 	github.com/rfjakob/eme v1.1.2
 	github.com/rivo/uniseg v0.4.7
diff --git a/go.sum b/go.sum
index 3b48fdf644d6f..81ff97f66969c 100644
--- a/go.sum
+++ b/go.sum
@@ -511,6 +511,8 @@ github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k
 github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
 github.com/putdotio/go-putio/putio v0.0.0-20200123120452-16d982cac2b8 h1:Y258uzXU/potCYnQd1r6wlAnoMB68BiCkCcCnKx1SH8=
 github.com/putdotio/go-putio/putio v0.0.0-20200123120452-16d982cac2b8/go.mod h1:bSJjRokAHHOhA+XFxplld8w2R/dXLH7Z3BZ532vhFwU=
+github.com/quasilyte/go-ruleguard/dsl v0.3.22 h1:wd8zkOhSNr+I+8Qeciml08ivDt1pSXe60+5DqOpCjPE=
+github.com/quasilyte/go-ruleguard/dsl v0.3.22/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU=
 github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs=
 github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
 github.com/quic-go/quic-go v0.40.1 h1:X3AGzUNFs0jVuO3esAGnTfvdgvL4fq655WaOi1snv1Q=
diff --git a/lib/encoder/internal/gen/main.go b/lib/encoder/internal/gen/main.go
index 5fd8c37bae3bb..dfe55676066a3 100644
--- a/lib/encoder/internal/gen/main.go
+++ b/lib/encoder/internal/gen/main.go
@@ -4,12 +4,12 @@ package main
 import (
 	"flag"
 	"fmt"
-	"log"
 	"math/rand"
 	"os"
 	"strconv"
 	"strings"
 
+	"github.com/rclone/rclone/fs"
 	"github.com/rclone/rclone/lib/encoder"
 )
 
@@ -434,13 +434,13 @@ var testCasesDoubleEdge = []testCase{
 
 func fatal(err error, s ...interface{}) {
 	if err != nil {
-		log.Fatalln(append(s, err))
+		fs.Fatal(nil, fmt.Sprint(append(s, err)))
 	}
 }
 func fatalW(_ int, err error) func(...interface{}) {
 	if err != nil {
 		return func(s ...interface{}) {
-			log.Fatalln(append(s, err))
+			fs.Fatal(nil, fmt.Sprint(append(s, err)))
 		}
 	}
 	return func(s ...interface{}) {}
diff --git a/lib/http/auth.go b/lib/http/auth.go
index 0c1137152e0f2..219b140a8599f 100644
--- a/lib/http/auth.go
+++ b/lib/http/auth.go
@@ -2,8 +2,8 @@ package http
 
 import (
 	"bytes"
+	"fmt"
 	"html/template"
-	"log"
 
 	"github.com/rclone/rclone/fs"
 	"github.com/rclone/rclone/fs/config/flags"
@@ -42,7 +42,7 @@ Use ` + "`--{{ .Prefix }}salt`" + ` to change the password hashing salt from the
 `
 	tmpl, err := template.New("auth help").Parse(help)
 	if err != nil {
-		log.Fatal("Fatal error parsing template", err)
+		fs.Fatal(nil, fmt.Sprint("Fatal error parsing template", err))
 	}
 
 	data := struct {
@@ -53,7 +53,7 @@ Use ` + "`--{{ .Prefix }}salt`" + ` to change the password hashing salt from the
 	buf := &bytes.Buffer{}
 	err = tmpl.Execute(buf, data)
 	if err != nil {
-		log.Fatal("Fatal error executing template", err)
+		fs.Fatal(nil, fmt.Sprint("Fatal error executing template", err))
 	}
 	return buf.String()
 }
diff --git a/lib/http/server.go b/lib/http/server.go
index ca76fcf52ba32..e1e1501d6a500 100644
--- a/lib/http/server.go
+++ b/lib/http/server.go
@@ -9,7 +9,6 @@ import (
 	"errors"
 	"fmt"
 	"html/template"
-	"log"
 	"net"
 	"net/http"
 	"os"
@@ -78,7 +77,7 @@ certificate authority certificate.
 `
 	tmpl, err := template.New("server help").Parse(help)
 	if err != nil {
-		log.Fatal("Fatal error parsing template", err)
+		fs.Fatal(nil, fmt.Sprint("Fatal error parsing template", err))
 	}
 
 	data := struct {
@@ -89,7 +88,7 @@ certificate authority certificate.
 	buf := &bytes.Buffer{}
 	err = tmpl.Execute(buf, data)
 	if err != nil {
-		log.Fatal("Fatal error executing template", err)
+		fs.Fatal(nil, fmt.Sprint("Fatal error executing template", err))
 	}
 	return buf.String()
 }
@@ -199,7 +198,7 @@ func (s instance) serve(wg *sync.WaitGroup) {
 	defer wg.Done()
 	err := s.httpServer.Serve(s.listener)
 	if err != http.ErrServerClosed && err != nil {
-		log.Printf("%s: unexpected error: %s", s.listener.Addr(), err.Error())
+		fs.Logf(nil, "%s: unexpected error: %s", s.listener.Addr(), err.Error())
 	}
 }
 
@@ -497,7 +496,7 @@ func (s *Server) Shutdown() error {
 		expiry := time.Now().Add(gracefulShutdownTime)
 		ctx, cancel := context.WithDeadline(context.Background(), expiry)
 		if err := ii.httpServer.Shutdown(ctx); err != nil {
-			log.Printf("error shutting down server: %s", err)
+			fs.Logf(nil, "error shutting down server: %s", err)
 		}
 		cancel()
 	}
diff --git a/lib/http/template.go b/lib/http/template.go
index e36e44b80fddf..a79c06f63b546 100644
--- a/lib/http/template.go
+++ b/lib/http/template.go
@@ -3,8 +3,8 @@ package http
 import (
 	"bytes"
 	"embed"
+	"fmt"
 	"html/template"
-	"log"
 	"os"
 	"strings"
 	"time"
@@ -57,7 +57,7 @@ be used to render HTML based on specific conditions.
 
 	tmpl, err := template.New("template help").Parse(help)
 	if err != nil {
-		log.Fatal("Fatal error parsing template", err)
+		fs.Fatal(nil, fmt.Sprint("Fatal error parsing template", err))
 	}
 
 	data := struct {
@@ -68,7 +68,7 @@ be used to render HTML based on specific conditions.
 	buf := &bytes.Buffer{}
 	err = tmpl.Execute(buf, data)
 	if err != nil {
-		log.Fatal("Fatal error executing template", err)
+		fs.Fatal(nil, fmt.Sprint("Fatal error executing template", err))
 	}
 	return buf.String()
 }
diff --git a/lib/pool/pool.go b/lib/pool/pool.go
index cf0ed93e2992b..4cb4d3a71e3ce 100644
--- a/lib/pool/pool.go
+++ b/lib/pool/pool.go
@@ -4,10 +4,10 @@ package pool
 
 import (
 	"fmt"
-	"log"
 	"sync"
 	"time"
 
+	"github.com/rclone/rclone/fs"
 	"github.com/rclone/rclone/lib/mmap"
 )
 
@@ -161,7 +161,7 @@ func (bp *Pool) Get() []byte {
 				bp.alloced++
 				break
 			}
-			log.Printf("Failed to get memory for buffer, waiting for %v: %v", waitTime, err)
+			fs.Logf(nil, "Failed to get memory for buffer, waiting for %v: %v", waitTime, err)
 			bp.mu.Unlock()
 			time.Sleep(waitTime)
 			bp.mu.Lock()
@@ -178,7 +178,7 @@ func (bp *Pool) Get() []byte {
 func (bp *Pool) freeBuffer(mem []byte) {
 	err := bp.free(mem)
 	if err != nil {
-		log.Printf("Failed to free memory: %v", err)
+		fs.Logf(nil, "Failed to free memory: %v", err)
 	}
 	bp.alloced--
 }
diff --git a/lib/systemd/notify.go b/lib/systemd/notify.go
index 2373431470930..056a8628d609d 100644
--- a/lib/systemd/notify.go
+++ b/lib/systemd/notify.go
@@ -2,10 +2,10 @@ package systemd
 
 import (
 	"fmt"
-	"log"
 	"sync"
 
 	"github.com/coreos/go-systemd/v22/daemon"
+	"github.com/rclone/rclone/fs"
 	"github.com/rclone/rclone/lib/atexit"
 )
 
@@ -18,13 +18,13 @@ import (
 // It should not be called as a result of rc commands. See #7540.
 func Notify() func() {
 	if _, err := daemon.SdNotify(false, daemon.SdNotifyReady); err != nil {
-		log.Printf("failed to notify ready to systemd: %v", err)
+		fs.Logf(nil, "failed to notify ready to systemd: %v", err)
 	}
 	var finaliseOnce sync.Once
 	finalise := func() {
 		finaliseOnce.Do(func() {
 			if _, err := daemon.SdNotify(false, daemon.SdNotifyStopping); err != nil {
-				log.Printf("failed to notify stopping to systemd: %v", err)
+				fs.Logf(nil, "failed to notify stopping to systemd: %v", err)
 			}
 		})
 	}


Test patch:
diff --git a/backend/cache/cache_internal_test.go b/backend/cache/cache_internal_test.go
index 3bbf466473b18..6092a1a1d8a48 100644
--- a/backend/cache/cache_internal_test.go
+++ b/backend/cache/cache_internal_test.go
@@ -10,7 +10,6 @@ import (
 	goflag "flag"
 	"fmt"
 	"io"
-	"log"
 	"math/rand"
 	"os"
 	"path"
@@ -93,7 +92,7 @@ func TestMain(m *testing.M) {
 	goflag.Parse()
 	var rc int
 
-	log.Printf("Running with the following params: \n remote: %v", remoteName)
+	fs.Logf(nil, "Running with the following params: \n remote: %v", remoteName)
 	runInstance = newRun()
 	rc = m.Run()
 	os.Exit(rc)
@@ -408,7 +407,7 @@ func TestInternalWrappedFsChangeNotSeen(t *testing.T) {
 	// update in the wrapped fs
 	originalSize, err := runInstance.size(t, rootFs, "data.bin")
 	require.NoError(t, err)
-	log.Printf("original size: %v", originalSize)
+	fs.Logf(nil, "original size: %v", originalSize)
 
 	o, err := cfs.UnWrap().NewObject(context.Background(), runInstance.encryptRemoteIfNeeded(t, "data.bin"))
 	require.NoError(t, err)
@@ -425,7 +424,7 @@ func TestInternalWrappedFsChangeNotSeen(t *testing.T) {
 	err = o.Update(context.Background(), bytes.NewReader(data2), objInfo)
 	require.NoError(t, err)
 	require.Equal(t, int64(len(data2)), o.Size())
-	log.Printf("updated size: %v", len(data2))
+	fs.Logf(nil, "updated size: %v", len(data2))
 
 	// get a new instance from the cache
 	if runInstance.wrappedIsExternal {
@@ -485,49 +484,49 @@ func TestInternalMoveWithNotify(t *testing.T) {
 	err = runInstance.retryBlock(func() error {
 		li, err := runInstance.list(t, rootFs, "test")
 		if err != nil {
-			log.Printf("err: %v", err)
+			fs.Logf(nil, "err: %v", err)
 			return err
 		}
 		if len(li) != 2 {
-			log.Printf("not expected listing /test: %v", li)
+			fs.Logf(nil, "not expected listing /test: %v", li)
 			return fmt.Errorf("not expected listing /test: %v", li)
 		}
 
 		li, err = runInstance.list(t, rootFs, "test/one")
 		if err != nil {
-			log.Printf("err: %v", err)
+			fs.Logf(nil, "err: %v", err)
 			return err
 		}
 		if len(li) != 0 {
-			log.Printf("not expected listing /test/one: %v", li)
+			fs.Logf(nil, "not expected listing /test/one: %v", li)
 			return fmt.Errorf("not expected listing /test/one: %v", li)
 		}
 
 		li, err = runInstance.list(t, rootFs, "test/second")
 		if err != nil {
-			log.Printf("err: %v", err)
+			fs.Logf(nil, "err: %v", err)
 			return err
 		}
 		if len(li) != 1 {
-			log.Printf("not expected listing /test/second: %v", li)
+			fs.Logf(nil, "not expected listing /test/second: %v", li)
 			return fmt.Errorf("not expected listing /test/second: %v", li)
 		}
 		if fi, ok := li[0].(os.FileInfo); ok {
 			if fi.Name() != "data.bin" {
-				log.Printf("not expected name: %v", fi.Name())
+				fs.Logf(nil, "not expected name: %v", fi.Name())
 				return fmt.Errorf("not expected name: %v", fi.Name())
 			}
 		} else if di, ok := li[0].(fs.DirEntry); ok {
 			if di.Remote() != "test/second/data.bin" {
-				log.Printf("not expected remote: %v", di.Remote())
+				fs.Logf(nil, "not expected remote: %v", di.Remote())
 				return fmt.Errorf("not expected remote: %v", di.Remote())
 			}
 		} else {
-			log.Printf("unexpected listing: %v", li)
+			fs.Logf(nil, "unexpected listing: %v", li)
 			return fmt.Errorf("unexpected listing: %v", li)
 		}
 
-		log.Printf("complete listing: %v", li)
+		fs.Logf(nil, "complete listing: %v", li)
 		return nil
 	}, 12, time.Second*10)
 	require.NoError(t, err)
@@ -577,43 +576,43 @@ func TestInternalNotifyCreatesEmptyParts(t *testing.T) {
 	err = runInstance.retryBlock(func() error {
 		found = boltDb.HasEntry(path.Join(cfs.Root(), runInstance.encryptRemoteIfNeeded(t, "test")))
 		if !found {
-			log.Printf("not found /test")
+			fs.Logf(nil, "not found /test")
 			return fmt.Errorf("not found /test")
 		}
 		found = boltDb.HasEntry(path.Join(cfs.Root(), runInstance.encryptRemoteIfNeeded(t, "test"), runInstance.encryptRemoteIfNeeded(t, "one")))
 		if !found {
-			log.Printf("not found /test/one")
+			fs.Logf(nil, "not found /test/one")
 			return fmt.Errorf("not found /test/one")
 		}
 		found = boltDb.HasEntry(path.Join(cfs.Root(), runInstance.encryptRemoteIfNeeded(t, "test"), runInstance.encryptRemoteIfNeeded(t, "one"), runInstance.encryptRemoteIfNeeded(t, "test2")))
 		if !found {
-			log.Printf("not found /test/one/test2")
+			fs.Logf(nil, "not found /test/one/test2")
 			return fmt.Errorf("not found /test/one/test2")
 		}
 		li, err := runInstance.list(t, rootFs, "test/one")
 		if err != nil {
-			log.Printf("err: %v", err)
+			fs.Logf(nil, "err: %v", err)
 			return err
 		}
 		if len(li) != 1 {
-			log.Printf("not expected listing /test/one: %v", li)
+			fs.Logf(nil, "not expected listing /test/one: %v", li)
 			return fmt.Errorf("not expected listing /test/one: %v", li)
 		}
 		if fi, ok := li[0].(os.FileInfo); ok {
 			if fi.Name() != "test2" {
-				log.Printf("not expected name: %v", fi.Name())
+				fs.Logf(nil, "not expected name: %v", fi.Name())
 				return fmt.Errorf("not expected name: %v", fi.Name())
 			}
 		} else if di, ok := li[0].(fs.DirEntry); ok {
 			if di.Remote() != "test/one/test2" {
-				log.Printf("not expected remote: %v", di.Remote())
+				fs.Logf(nil, "not expected remote: %v", di.Remote())
 				return fmt.Errorf("not expected remote: %v", di.Remote())
 			}
 		} else {
-			log.Printf("unexpected listing: %v", li)
+			fs.Logf(nil, "unexpected listing: %v", li)
 			return fmt.Errorf("unexpected listing: %v", li)
 		}
-		log.Printf("complete listing /test/one/test2")
+		fs.Logf(nil, "complete listing /test/one/test2")
 		return nil
 	}, 12, time.Second*10)
 	require.NoError(t, err)
@@ -771,24 +770,24 @@ func TestInternalBug2117(t *testing.T) {
 
 	di, err := runInstance.list(t, rootFs, "test/dir1/dir2")
 	require.NoError(t, err)
-	log.Printf("len: %v", len(di))
+	fs.Logf(nil, "len: %v", len(di))
 	require.Len(t, di, 1)
 
 	time.Sleep(time.Second * 30)
 
 	di, err = runInstance.list(t, rootFs, "test/dir1/dir2")
 	require.NoError(t, err)
-	log.Printf("len: %v", len(di))
+	fs.Logf(nil, "len: %v", len(di))
 	require.Len(t, di, 1)
 
 	di, err = runInstance.list(t, rootFs, "test/dir1")
 	require.NoError(t, err)
-	log.Printf("len: %v", len(di))
+	fs.Logf(nil, "len: %v", len(di))
 	require.Len(t, di, 4)
 
 	di, err = runInstance.list(t, rootFs, "test")
 	require.NoError(t, err)
-	log.Printf("len: %v", len(di))
+	fs.Logf(nil, "len: %v", len(di))
 	require.Len(t, di, 4)
 }
 
@@ -829,7 +828,7 @@ func newRun() *run {
 	} else {
 		r.tmpUploadDir = uploadDir
 	}
-	log.Printf("Temp Upload Dir: %v", r.tmpUploadDir)
+	fs.Logf(nil, "Temp Upload Dir: %v", r.tmpUploadDir)
 
 	return r
 }
diff --git a/cmd/bisync/bisync_test.go b/cmd/bisync/bisync_test.go
index ba1e313e5487f..bc359bbd3923b 100644
--- a/cmd/bisync/bisync_test.go
+++ b/cmd/bisync/bisync_test.go
@@ -10,7 +10,6 @@ import (
 	"errors"
 	"flag"
 	"fmt"
-	"log"
 	"os"
 	"path"
 	"path/filepath"
@@ -232,7 +231,7 @@ func TestBisyncRemoteLocal(t *testing.T) {
 		t.Skip("path1 and path2 are the same remote")
 	}
 	_, remote, cleanup, err := fstest.RandomRemote()
-	log.Printf("remote: %v", remote)
+	fs.Logf(nil, "remote: %v", remote)
 	require.NoError(t, err)
 	defer cleanup()
 	testBisync(t, remote, *argRemote2)
@@ -244,7 +243,7 @@ func TestBisyncLocalRemote(t *testing.T) {
 		t.Skip("path1 and path2 are the same remote")
 	}
 	_, remote, cleanup, err := fstest.RandomRemote()
-	log.Printf("remote: %v", remote)
+	fs.Logf(nil, "remote: %v", remote)
 	require.NoError(t, err)
 	defer cleanup()
 	testBisync(t, *argRemote2, remote)
@@ -254,7 +253,7 @@ func TestBisyncLocalRemote(t *testing.T) {
 // (useful for testing server-side copy/move)
 func TestBisyncRemoteRemote(t *testing.T) {
 	_, remote, cleanup, err := fstest.RandomRemote()
-	log.Printf("remote: %v", remote)
+	fs.Logf(nil, "remote: %v", remote)
 	require.NoError(t, err)
 	defer cleanup()
 	testBisync(t, remote, remote)
@@ -450,13 +449,13 @@ func (b *bisyncTest) runTestCase(ctx context.Context, t *testing.T, testCase str
 	for _, dir := range srcDirs {
 		dirs = append(dirs, norm.NFC.String(dir.Remote()))
 	}
-	log.Printf("checking initFs %s", initFs)
+	fs.Logf(nil, "checking initFs %s", initFs)
 	fstest.CheckListingWithPrecision(b.t, initFs, items, dirs, initFs.Precision())
 	checkError(b.t, sync.CopyDir(ctxNoDsStore, b.fs1, initFs, true), "setting up path1")
-	log.Printf("checking Path1 %s", b.fs1)
+	fs.Logf(nil, "checking Path1 %s", b.fs1)
 	fstest.CheckListingWithPrecision(b.t, b.fs1, items, dirs, b.fs1.Precision())
 	checkError(b.t, sync.CopyDir(ctxNoDsStore, b.fs2, initFs, true), "setting up path2")
-	log.Printf("checking path2 %s", b.fs2)
+	fs.Logf(nil, "checking path2 %s", b.fs2)
 	fstest.CheckListingWithPrecision(b.t, b.fs2, items, dirs, b.fs2.Precision())
 
 	// Create log file
@@ -514,21 +513,21 @@ func (b *bisyncTest) runTestCase(ctx context.Context, t *testing.T, testCase str
 	require.NoError(b.t, err, "saving log file %s", savedLog)
 
 	if b.golden && !b.stopped {
-		log.Printf("Store results to golden directory")
+		fs.Logf(nil, "Store results to golden directory")
 		b.storeGolden()
 		return
 	}
 
 	errorCount := 0
 	if b.noCompare {
-		log.Printf("Skip comparing results with golden directory")
+		fs.Logf(nil, "Skip comparing results with golden directory")
 		errorCount = -2
 	} else {
 		errorCount = b.compareResults()
 	}
 
 	if b.noCleanup {
-		log.Printf("Skip cleanup")
+		fs.Logf(nil, "Skip cleanup")
 	} else {
 		b.cleanupCase(ctx)
 	}
@@ -1383,24 +1382,24 @@ func (b *bisyncTest) compareResults() int {
 	const divider = "----------------------------------------------------------"
 
 	if goldenNum != resultNum {
-		log.Print(divider)
-		log.Print(color(terminal.RedFg, "MISCOMPARE - Number of Golden and Results files do not match:"))
-		log.Printf("  Golden count: %d", goldenNum)
-		log.Printf("  Result count: %d", resultNum)
-		log.Printf("  Golden files: %s", strings.Join(goldenFiles, ", "))
-		log.Printf("  Result files: %s", strings.Join(resultFiles, ", "))
+		fs.Log(nil, divider)
+		fs.Log(nil, color(terminal.RedFg, "MISCOMPARE - Number of Golden and Results files do not match:"))
+		fs.Logf(nil, "  Golden count: %d", goldenNum)
+		fs.Logf(nil, "  Result count: %d", resultNum)
+		fs.Logf(nil, "  Golden files: %s", strings.Join(goldenFiles, ", "))
+		fs.Logf(nil, "  Result files: %s", strings.Join(resultFiles, ", "))
 	}
 
 	for _, file := range goldenFiles {
 		if !resultSet.Has(file) {
 			errorCount++
-			log.Printf("  File found in Golden but not in Results:  %s", file)
+			fs.Logf(nil, "  File found in Golden but not in Results:  %s", file)
 		}
 	}
 	for _, file := range resultFiles {
 		if !goldenSet.Has(file) {
 			errorCount++
-			log.Printf("  File found in Results but not in Golden:  %s", file)
+			fs.Logf(nil, "  File found in Results but not in Golden:  %s", file)
 		}
 	}
 
@@ -1433,15 +1432,15 @@ func (b *bisyncTest) compareResults() int {
 		text, err := difflib.GetUnifiedDiffString(diff)
 		require.NoError(b.t, err, "diff failed")
 
-		log.Print(divider)
-		log.Printf(color(terminal.RedFg, "| MISCOMPARE  -Golden vs +Results for  %s"), file)
+		fs.Log(nil, divider)
+		fs.Logf(nil, color(terminal.RedFg, "| MISCOMPARE  -Golden vs +Results for  %s"), file)
 		for _, line := range strings.Split(strings.TrimSpace(text), "\n") {
-			log.Printf("| %s", strings.TrimSpace(line))
+			fs.Logf(nil, "| %s", strings.TrimSpace(line))
 		}
 	}
 
 	if errorCount > 0 {
-		log.Print(divider)
+		fs.Log(nil, divider)
 	}
 	if errorCount == 0 && goldenNum != resultNum {
 		return -1
@@ -1464,7 +1463,7 @@ func (b *bisyncTest) storeGolden() {
 			continue
 		}
 		if fileName == "backupdirs" {
-			log.Printf("skipping: %v", fileName)
+			fs.Logf(nil, "skipping: %v", fileName)
 			continue
 		}
 		goldName := b.toGolden(fileName)
@@ -1489,7 +1488,7 @@ func (b *bisyncTest) storeGolden() {
 			continue
 		}
 		if fileName == "backupdirs" {
-			log.Printf("skipping: %v", fileName)
+			fs.Logf(nil, "skipping: %v", fileName)
 			continue
 		}
 		text := b.mangleResult(b.goldenDir, fileName, true)
@@ -1849,7 +1848,7 @@ func fileType(fileName string) string {
 // logPrintf prints a message to stdout and to the test log
 func (b *bisyncTest) logPrintf(text string, args ...interface{}) {
 	line := fmt.Sprintf(text, args...)
-	log.Print(line)
+	fs.Log(nil, line)
 	if b.logFile != nil {
 		_, err := fmt.Fprintln(b.logFile, line)
 		require.NoError(b.t, err, "writing log file")
diff --git a/cmd/serve/proxy/proxy_test.go b/cmd/serve/proxy/proxy_test.go
index 1488e7406fc2b..539468d447ef5 100644
--- a/cmd/serve/proxy/proxy_test.go
+++ b/cmd/serve/proxy/proxy_test.go
@@ -6,11 +6,11 @@ import (
 	"crypto/rsa"
 	"crypto/sha256"
 	"encoding/base64"
-	"log"
 	"strings"
 	"testing"
 
 	_ "github.com/rclone/rclone/backend/local"
+	"github.com/rclone/rclone/fs"
 	"github.com/rclone/rclone/fs/config/configmap"
 	"github.com/rclone/rclone/fs/config/obscure"
 	"github.com/stretchr/testify/assert"
@@ -149,11 +149,11 @@ func TestRun(t *testing.T) {
 
 	privateKey, privateKeyErr := rsa.GenerateKey(rand.Reader, 2048)
 	if privateKeyErr != nil {
-		log.Fatal("error generating test private key " + privateKeyErr.Error())
+		fs.Fatal(nil, "error generating test private key "+privateKeyErr.Error())
 	}
 	publicKey, publicKeyError := ssh.NewPublicKey(&privateKey.PublicKey)
 	if privateKeyErr != nil {
-		log.Fatal("error generating test public key " + publicKeyError.Error())
+		fs.Fatal(nil, "error generating test public key "+publicKeyError.Error())
 	}
 
 	publicKeyString := base64.StdEncoding.EncodeToString(publicKey.Marshal())
diff --git a/cmd/test/info/base32768.go b/cmd/test/info/base32768.go
index 8f3f61343104f..23949a2e1e9a2 100644
--- a/cmd/test/info/base32768.go
+++ b/cmd/test/info/base32768.go
@@ -5,7 +5,6 @@ package info
 import (
 	"context"
 	"fmt"
-	"log"
 	"os"
 	"path/filepath"
 	"strings"
@@ -25,7 +24,7 @@ func (r *results) checkBase32768() {
 	n := 0
 	dir, err := os.MkdirTemp("", "rclone-base32768-files")
 	if err != nil {
-		log.Printf("Failed to make temp dir: %v", err)
+		fs.Logf(nil, "Failed to make temp dir: %v", err)
 		return
 	}
 	defer func() {
@@ -41,7 +40,7 @@ func (r *results) checkBase32768() {
 		fileName := filepath.Join(dir, fmt.Sprintf("%04d-%s.txt", n, out.String()))
 		err = os.WriteFile(fileName, []byte(fileName), 0666)
 		if err != nil {
-			log.Printf("write %q failed: %v", fileName, err)
+			fs.Logf(nil, "write %q failed: %v", fileName, err)
 			return
 		}
 		n++
@@ -50,7 +49,7 @@ func (r *results) checkBase32768() {
 	// Make a local fs
 	fLocal, err := fs.NewFs(ctx, dir)
 	if err != nil {
-		log.Printf("Failed to make local fs: %v", err)
+		fs.Logf(nil, "Failed to make local fs: %v", err)
 		return
 	}
 
@@ -61,14 +60,14 @@ func (r *results) checkBase32768() {
 	s = fspath.JoinRootPath(s, testDir)
 	fRemote, err := fs.NewFs(ctx, s)
 	if err != nil {
-		log.Printf("Failed to make remote fs: %v", err)
+		fs.Logf(nil, "Failed to make remote fs: %v", err)
 		return
 	}
 
 	defer func() {
 		err := operations.Purge(ctx, r.f, testDir)
 		if err != nil {
-			log.Printf("Failed to purge test directory: %v", err)
+			fs.Logf(nil, "Failed to purge test directory: %v", err)
 			return
 		}
 	}()
@@ -76,7 +75,7 @@ func (r *results) checkBase32768() {
 	// Sync local to remote
 	err = sync.Sync(ctx, fRemote, fLocal, false)
 	if err != nil {
-		log.Printf("Failed to sync remote fs: %v", err)
+		fs.Logf(nil, "Failed to sync remote fs: %v", err)
 		return
 	}
 
@@ -86,7 +85,7 @@ func (r *results) checkBase32768() {
 		Fsrc: fLocal,
 	})
 	if err != nil {
-		log.Printf("Failed to check remote fs: %v", err)
+		fs.Logf(nil, "Failed to check remote fs: %v", err)
 		return
 	}
 
diff --git a/cmd/test/info/info.go b/cmd/test/info/info.go
index c2ac0977d9272..83bcdfee8c5d5 100644
--- a/cmd/test/info/info.go
+++ b/cmd/test/info/info.go
@@ -10,7 +10,6 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
-	"log"
 	"os"
 	"path"
 	"regexp"
@@ -77,7 +76,7 @@ code for each one.
 	Run: func(command *cobra.Command, args []string) {
 		cmd.CheckArgs(1, 1e6, command, args)
 		if !checkNormalization && !checkControl && !checkLength && !checkStreaming && !checkBase32768 && !all {
-			log.Fatalf("no tests selected - select a test or use --all")
+			fs.Fatalf(nil, "no tests selected - select a test or use --all")
 		}
 		if all {
 			checkNormalization = true
@@ -93,7 +92,7 @@ code for each one.
 			fs.Infof(f, "Created temporary directory for test files: %s", tempDirPath)
 			err := f.Mkdir(context.Background(), "")
 			if err != nil {
-				log.Fatalf("couldn't create temporary directory: %v", err)
+				fs.Fatalf(nil, "couldn't create temporary directory: %v", err)
 			}
 
 			cmd.Run(false, false, command, func() error {
diff --git a/cmd/test/info/internal/build_csv/main.go b/cmd/test/info/internal/build_csv/main.go
index fbabe5b85a60e..fa4b15b53c9da 100644
--- a/cmd/test/info/internal/build_csv/main.go
+++ b/cmd/test/info/internal/build_csv/main.go
@@ -7,12 +7,12 @@ import (
 	"flag"
 	"fmt"
 	"io"
-	"log"
 	"os"
 	"sort"
 	"strconv"
 
 	"github.com/rclone/rclone/cmd/test/info/internal"
+	"github.com/rclone/rclone/fs"
 )
 
 func main() {
@@ -24,21 +24,21 @@ func main() {
 	for _, fn := range args {
 		f, err := os.Open(fn)
 		if err != nil {
-			log.Fatalf("Unable to open %q: %s", fn, err)
+			fs.Fatalf(nil, "Unable to open %q: %s", fn, err)
 		}
 		var remote internal.InfoReport
 		dec := json.NewDecoder(f)
 		err = dec.Decode(&remote)
 		if err != nil {
-			log.Fatalf("Unable to decode %q: %s", fn, err)
+			fs.Fatalf(nil, "Unable to decode %q: %s", fn, err)
 		}
 		if remote.ControlCharacters == nil {
-			log.Printf("Skipping remote %s: no ControlCharacters", remote.Remote)
+			fs.Logf(nil, "Skipping remote %s: no ControlCharacters", remote.Remote)
 		} else {
 			remotes = append(remotes, remote)
 		}
 		if err := f.Close(); err != nil {
-			log.Fatalf("Closing %q failed: %s", fn, err)
+			fs.Fatalf(nil, "Closing %q failed: %s", fn, err)
 		}
 	}
 
@@ -117,11 +117,11 @@ func main() {
 	} else {
 		f, err := os.Create(*fOut)
 		if err != nil {
-			log.Fatalf("Unable to create %q: %s", *fOut, err)
+			fs.Fatalf(nil, "Unable to create %q: %s", *fOut, err)
 		}
 		defer func() {
 			if err := f.Close(); err != nil {
-				log.Fatalln("Error writing csv:", err)
+				fs.Fatal(nil, fmt.Sprint("Error writing csv:", err))
 			}
 		}()
 		writer = f
@@ -130,9 +130,9 @@ func main() {
 	w := csv.NewWriter(writer)
 	err := w.WriteAll(records)
 	if err != nil {
-		log.Fatalln("Error writing csv:", err)
+		fs.Fatal(nil, fmt.Sprint("Error writing csv:", err))
 	} else if err := w.Error(); err != nil {
-		log.Fatalln("Error writing csv:", err)
+		fs.Fatal(nil, fmt.Sprint("Error writing csv:", err))
 	}
 }
 
diff --git a/cmd/test/makefiles/makefiles.go b/cmd/test/makefiles/makefiles.go
index f48778cb03830..3027d78ae61c5 100644
--- a/cmd/test/makefiles/makefiles.go
+++ b/cmd/test/makefiles/makefiles.go
@@ -4,7 +4,6 @@ package makefiles
 
 import (
 	"io"
-	"log"
 	"math"
 	"math/rand"
 	"os"
@@ -117,7 +116,7 @@ var makefileCmd = &cobra.Command{
 		var size fs.SizeSuffix
 		err := size.Set(args[0])
 		if err != nil {
-			log.Fatalf("Failed to parse size %q: %v", args[0], err)
+			fs.Fatalf(nil, "Failed to parse size %q: %v", args[0], err)
 		}
 		start := time.Now()
 		fs.Logf(nil, "Creating %d files of size %v.", len(args[1:]), size)
@@ -148,7 +147,7 @@ func commonInit() {
 	}
 	randSource = rand.New(rand.NewSource(seed))
 	if bool2int(zero)+bool2int(sparse)+bool2int(ascii)+bool2int(pattern)+bool2int(chargen) > 1 {
-		log.Fatal("Can only supply one of --zero, --sparse, --ascii, --pattern or --chargen")
+		fs.Fatal(nil, "Can only supply one of --zero, --sparse, --ascii, --pattern or --chargen")
 	}
 	switch {
 	case zero, sparse:
@@ -276,12 +275,12 @@ func (d *dir) list(path string, output []string) []string {
 func writeFile(dir, name string, size int64) {
 	err := file.MkdirAll(dir, 0777)
 	if err != nil {
-		log.Fatalf("Failed to make directory %q: %v", dir, err)
+		fs.Fatalf(nil, "Failed to make directory %q: %v", dir, err)
 	}
 	path := filepath.Join(dir, name)
 	fd, err := os.Create(path)
 	if err != nil {
-		log.Fatalf("Failed to open file %q: %v", path, err)
+		fs.Fatalf(nil, "Failed to open file %q: %v", path, err)
 	}
 	if sparse {
 		err = fd.Truncate(size)
@@ -289,11 +288,11 @@ func writeFile(dir, name string, size int64) {
 		_, err = io.CopyN(fd, source, size)
 	}
 	if err != nil {
-		log.Fatalf("Failed to write %v bytes to file %q: %v", size, path, err)
+		fs.Fatalf(nil, "Failed to write %v bytes to file %q: %v", size, path, err)
 	}
 	err = fd.Close()
 	if err != nil {
-		log.Fatalf("Failed to close file %q: %v", path, err)
+		fs.Fatalf(nil, "Failed to close file %q: %v", path, err)
 	}
 	fs.Infof(path, "Written file size %v", fs.SizeSuffix(size))
 }
diff --git a/cmdtest/cmdtest_test.go b/cmdtest/cmdtest_test.go
index 6598389599662..c29167342c90e 100644
--- a/cmdtest/cmdtest_test.go
+++ b/cmdtest/cmdtest_test.go
@@ -6,13 +6,13 @@
 package cmdtest
 
 import (
-	"log"
 	"os"
 	"os/exec"
 	"path/filepath"
 	"strings"
 	"testing"
 
+	"github.com/rclone/rclone/fs"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )
@@ -26,14 +26,14 @@ func TestMain(m *testing.M) {
 		// started by Go test => execute tests
 		err := os.Setenv(rcloneTestMain, "true")
 		if err != nil {
-			log.Fatalf("Unable to set %s: %s", rcloneTestMain, err.Error())
+			fs.Fatalf(nil, "Unable to set %s: %s", rcloneTestMain, err.Error())
 		}
 		os.Exit(m.Run())
 	} else {
 		// started by func rcloneExecMain => call rclone main in cmdtest.go
 		err := os.Unsetenv(rcloneTestMain)
 		if err != nil {
-			log.Fatalf("Unable to unset %s: %s", rcloneTestMain, err.Error())
+			fs.Fatalf(nil, "Unable to unset %s: %s", rcloneTestMain, err.Error())
 		}
 		main()
 	}
@@ -47,7 +47,7 @@ const rcloneTestMain = "RCLONE_TEST_MAIN"
 func rcloneExecMain(env string, args ...string) (string, error) {
 	_, found := os.LookupEnv(rcloneTestMain)
 	if !found {
-		log.Fatalf("Unexpected execution path: %s is missing.", rcloneTestMain)
+		fs.Fatalf(nil, "Unexpected execution path: %s is missing.", rcloneTestMain)
 	}
 	// make a call to self to execute rclone main in a predefined environment (enters TestMain above)
 	command := exec.Command(os.Args[0], args...)
diff --git a/fs/hash/hash_test.go b/fs/hash/hash_test.go
index 403a0d6e70d80..9215e9b9e814c 100644
--- a/fs/hash/hash_test.go
+++ b/fs/hash/hash_test.go
@@ -2,10 +2,11 @@ package hash_test
 
 import (
 	"bytes"
+	"fmt"
 	"io"
-	"log"
 	"testing"
 
+	"github.com/rclone/rclone/fs"
 	"github.com/rclone/rclone/fs/hash"
 	"github.com/spf13/pflag"
 	"github.com/stretchr/testify/assert"
@@ -24,7 +25,7 @@ func TestHashSet(t *testing.T) {
 	assert.Len(t, a, 0)
 
 	h = h.Add(hash.MD5)
-	log.Println(h)
+	fs.Log(nil, fmt.Sprint(h))
 	assert.Equal(t, 1, h.Count())
 	assert.Equal(t, hash.MD5, h.GetOne())
 	a = h.Array()
diff --git a/fstest/fstest.go b/fstest/fstest.go
index 032a5085339f7..69a05d76a0f0f 100644
--- a/fstest/fstest.go
+++ b/fstest/fstest.go
@@ -10,7 +10,6 @@ import (
 	"flag"
 	"fmt"
 	"io"
-	"log"
 	"os"
 	"path"
 	"path/filepath"
@@ -99,7 +98,7 @@ func NewItem(Path, Content string, modTime time.Time) Item {
 	buf := bytes.NewBufferString(Content)
 	_, err := io.Copy(hash, buf)
 	if err != nil {
-		log.Fatalf("Failed to create item: %v", err)
+		fs.Fatalf(nil, "Failed to create item: %v", err)
 	}
 	i.Hashes = hash.Sums()
 	return i
@@ -398,7 +397,7 @@ func CompareItems(t *testing.T, entries fs.DirEntries, items []Item, expectedDir
 func Time(timeString string) time.Time {
 	t, err := time.Parse(time.RFC3339Nano, timeString)
 	if err != nil {
-		log.Fatalf("Failed to parse time %q: %v", timeString, err)
+		fs.Fatalf(nil, "Failed to parse time %q: %v", timeString, err)
 	}
 	return t
 }
@@ -433,7 +432,7 @@ func RandomRemoteName(remoteName string) (string, string, error) {
 		}
 		leafName = "rclone-test-" + random.String(12)
 		if !MatchTestRemote.MatchString(leafName) {
-			log.Fatalf("%q didn't match the test remote name regexp", leafName)
+			fs.Fatalf(nil, "%q didn't match the test remote name regexp", leafName)
 		}
 		remoteName += leafName
 	}
@@ -467,7 +466,7 @@ func RandomRemote() (fs.Fs, string, func(), error) {
 		if parentRemote != nil {
 			Purge(parentRemote)
 			if err != nil {
-				log.Printf("Failed to purge %v: %v", parentRemote, err)
+				fs.Logf(nil, "Failed to purge %v: %v", parentRemote, err)
 			}
 		}
 	}
@@ -499,7 +498,7 @@ func Purge(f fs.Fs) {
 				fs.Debugf(f, "Purge object %q", obj.Remote())
 				err = obj.Remove(ctx)
 				if err != nil {
-					log.Printf("purge failed to remove %q: %v", obj.Remote(), err)
+					fs.Logf(nil, "purge failed to remove %q: %v", obj.Remote(), err)
 				}
 			})
 			entries.ForDir(func(dir fs.Directory) {
@@ -513,12 +512,12 @@ func Purge(f fs.Fs) {
 			fs.Debugf(f, "Purge dir %q", dir)
 			err := f.Rmdir(ctx, dir)
 			if err != nil {
-				log.Printf("purge failed to rmdir %q: %v", dir, err)
+				fs.Logf(nil, "purge failed to rmdir %q: %v", dir, err)
 			}
 		}
 	}
 	if err != nil {
-		log.Printf("purge failed: %v", err)
+		fs.Logf(nil, "purge failed: %v", err)
 	}
 }
 
diff --git a/fstest/test_all/clean.go b/fstest/test_all/clean.go
index 788061cf3851a..0e386dcaf7b53 100644
--- a/fstest/test_all/clean.go
+++ b/fstest/test_all/clean.go
@@ -5,7 +5,6 @@ package main
 import (
 	"context"
 	"fmt"
-	"log"
 	"regexp"
 
 	"github.com/rclone/rclone/fs"
@@ -27,7 +26,7 @@ func cleanFs(ctx context.Context, remote string, cleanup bool) error {
 	}
 	var lastErr error
 	if cleanup {
-		log.Printf("%q - running cleanup", remote)
+		fs.Logf(nil, "%q - running cleanup", remote)
 		err = operations.CleanUp(ctx, f)
 		if err != nil {
 			lastErr = err
@@ -43,10 +42,10 @@ func cleanFs(ctx context.Context, remote string, cleanup bool) error {
 		fullPath := fspath.JoinRootPath(remote, dirPath)
 		if MatchTestRemote.MatchString(dirPath) {
 			if *dryRun {
-				log.Printf("Not Purging %s - -dry-run", fullPath)
+				fs.Logf(nil, "Not Purging %s - -dry-run", fullPath)
 				return nil
 			}
-			log.Printf("Purging %s", fullPath)
+			fs.Logf(nil, "Purging %s", fullPath)
 			dir, err := fs.NewFs(context.Background(), fullPath)
 			if err != nil {
 				err = fmt.Errorf("NewFs failed: %w", err)
@@ -75,11 +74,11 @@ func cleanRemotes(conf *Config) error {
 	var lastError error
 	for _, backend := range conf.Backends {
 		remote := backend.Remote
-		log.Printf("%q - Cleaning", remote)
+		fs.Logf(nil, "%q - Cleaning", remote)
 		err := cleanFs(context.Background(), remote, backend.CleanUp)
 		if err != nil {
 			lastError = err
-			log.Printf("Failed to purge %q: %v", remote, err)
+			fs.Logf(nil, "Failed to purge %q: %v", remote, err)
 		}
 	}
 	return lastError
diff --git a/fstest/test_all/config.go b/fstest/test_all/config.go
index 45920e25eaf34..82eab084db107 100644
--- a/fstest/test_all/config.go
+++ b/fstest/test_all/config.go
@@ -4,7 +4,6 @@ package main
 
 import (
 	"fmt"
-	"log"
 	"os"
 	"path"
 
@@ -65,7 +64,7 @@ func (b *Backend) MakeRuns(t *Test) (runs []*Run) {
 	maxSize := fs.SizeSuffix(0)
 	if b.MaxFile != "" {
 		if err := maxSize.Set(b.MaxFile); err != nil {
-			log.Printf("Invalid maxfile value %q: %v", b.MaxFile, err)
+			fs.Logf(nil, "Invalid maxfile value %q: %v", b.MaxFile, err)
 		}
 	}
 	fastlists := []bool{false}
@@ -152,11 +151,11 @@ func (c *Config) filterBackendsByRemotes(remotes []string) {
 			}
 		}
 		if !found {
-			log.Printf("Remote %q not found - inserting with default flags", name)
+			fs.Logf(nil, "Remote %q not found - inserting with default flags", name)
 			// Lookup which backend
 			fsInfo, _, _, _, err := fs.ConfigFs(name)
 			if err != nil {
-				log.Fatalf("couldn't find remote %q: %v", name, err)
+				fs.Fatalf(nil, "couldn't find remote %q: %v", name, err)
 			}
 			newBackends = append(newBackends, Backend{Backend: fsInfo.FileName(), Remote: name})
 		}
diff --git a/fstest/test_all/report.go b/fstest/test_all/report.go
index 0dd4167d55490..c03e226bf9ea2 100644
--- a/fstest/test_all/report.go
+++ b/fstest/test_all/report.go
@@ -4,7 +4,6 @@ import (
 	"encoding/json"
 	"fmt"
 	"html/template"
-	"log"
 	"os"
 	"os/exec"
 	"path"
@@ -77,7 +76,7 @@ func NewReport() *Report {
 	r.LogDir = path.Join(*outputDir, r.DateTime)
 	err = file.MkdirAll(r.LogDir, 0777)
 	if err != nil {
-		log.Fatalf("Failed to make log directory: %v", err)
+		fs.Fatalf(nil, "Failed to make log directory: %v", err)
 	}
 
 	// Online version
@@ -132,15 +131,15 @@ func (r *Report) Title() string {
 
 // LogSummary writes the summary to the log file
 func (r *Report) LogSummary() {
-	log.Printf("Logs in %q", r.LogDir)
+	fs.Logf(nil, "Logs in %q", r.LogDir)
 
 	// Summarise results
-	log.Printf("SUMMARY")
-	log.Println(r.Title())
+	fs.Logf(nil, "SUMMARY")
+	fs.Log(nil, r.Title())
 	if !r.AllPassed() {
 		for _, t := range r.Failed {
-			log.Printf("  * %s", toShell(t.nextCmdLine()))
-			log.Printf("    * Failed tests: %v", t.FailedTests)
+			fs.Logf(nil, "  * %s", toShell(t.nextCmdLine()))
+			fs.Logf(nil, "    * Failed tests: %v", t.FailedTests)
 		}
 	}
 }
@@ -149,11 +148,11 @@ func (r *Report) LogSummary() {
 func (r *Report) LogJSON() {
 	out, err := json.MarshalIndent(r, "", "\t")
 	if err != nil {
-		log.Fatalf("Failed to marshal data for index.json: %v", err)
+		fs.Fatalf(nil, "Failed to marshal data for index.json: %v", err)
 	}
 	err = os.WriteFile(path.Join(r.LogDir, "index.json"), out, 0666)
 	if err != nil {
-		log.Fatalf("Failed to write index.json: %v", err)
+		fs.Fatalf(nil, "Failed to write index.json: %v", err)
 	}
 }
 
@@ -162,17 +161,17 @@ func (r *Report) LogHTML() {
 	r.IndexHTML = path.Join(r.LogDir, "index.html")
 	out, err := os.Create(r.IndexHTML)
 	if err != nil {
-		log.Fatalf("Failed to open index.html: %v", err)
+		fs.Fatalf(nil, "Failed to open index.html: %v", err)
 	}
 	defer func() {
 		err := out.Close()
 		if err != nil {
-			log.Fatalf("Failed to close index.html: %v", err)
+			fs.Fatalf(nil, "Failed to close index.html: %v", err)
 		}
 	}()
 	err = reportTemplate.Execute(out, r)
 	if err != nil {
-		log.Fatalf("Failed to execute template: %v", err)
+		fs.Fatalf(nil, "Failed to execute template: %v", err)
 	}
 	_ = open.Start("file://" + r.IndexHTML)
 }
@@ -282,19 +281,19 @@ func (r *Report) EmailHTML() {
 	if *emailReport == "" || r.IndexHTML == "" {
 		return
 	}
-	log.Printf("Sending email summary to %q", *emailReport)
+	fs.Logf(nil, "Sending email summary to %q", *emailReport)
 	cmdLine := []string{"mail", "-a", "Content-Type: text/html", *emailReport, "-s", "rclone integration tests: " + r.Title()}
 	cmd := exec.Command(cmdLine[0], cmdLine[1:]...)
 	in, err := os.Open(r.IndexHTML)
 	if err != nil {
-		log.Fatalf("Failed to open index.html: %v", err)
+		fs.Fatalf(nil, "Failed to open index.html: %v", err)
 	}
 	cmd.Stdin = in
 	cmd.Stdout = os.Stdout
 	cmd.Stderr = os.Stderr
 	err = cmd.Run()
 	if err != nil {
-		log.Fatalf("Failed to send email: %v", err)
+		fs.Fatalf(nil, "Failed to send email: %v", err)
 	}
 	_ = in.Close()
 }
@@ -302,14 +301,14 @@ func (r *Report) EmailHTML() {
 // uploadTo uploads a copy of the report online to the dir given
 func (r *Report) uploadTo(uploadDir string) {
 	dst := path.Join(*uploadPath, uploadDir)
-	log.Printf("Uploading results to %q", dst)
+	fs.Logf(nil, "Uploading results to %q", dst)
 	cmdLine := []string{"rclone", "sync", "--stats-log-level", "NOTICE", r.LogDir, dst}
 	cmd := exec.Command(cmdLine[0], cmdLine[1:]...)
 	cmd.Stdout = os.Stdout
 	cmd.Stderr = os.Stderr
 	err := cmd.Run()
 	if err != nil {
-		log.Fatalf("Failed to upload results: %v", err)
+		fs.Fatalf(nil, "Failed to upload results: %v", err)
 	}
 }
 
diff --git a/fstest/test_all/run.go b/fstest/test_all/run.go
index f156b7f187948..40560d6e5b741 100644
--- a/fstest/test_all/run.go
+++ b/fstest/test_all/run.go
@@ -8,7 +8,6 @@ import (
 	"fmt"
 	"go/build"
 	"io"
-	"log"
 	"os"
 	"os/exec"
 	"path"
@@ -94,10 +93,10 @@ func (rs Runs) Less(i, j int) bool {
 
 // dumpOutput prints the error output
 func (r *Run) dumpOutput() {
-	log.Println("------------------------------------------------------------")
-	log.Printf("---- %q ----", r.CmdString)
-	log.Println(string(r.output))
-	log.Println("------------------------------------------------------------")
+	fs.Log(nil, "------------------------------------------------------------")
+	fs.Logf(nil, "---- %q ----", r.CmdString)
+	fs.Log(nil, string(r.output))
+	fs.Log(nil, "------------------------------------------------------------")
 }
 
 // trie for storing runs
@@ -180,7 +179,7 @@ func (r *Run) findFailures() {
 	}
 	r.FailedTests = newTests
 	if len(r.FailedTests) == 0 && ignored > 0 {
-		log.Printf("%q - Found %d ignored errors only - marking as good", r.CmdString, ignored)
+		fs.Logf(nil, "%q - Found %d ignored errors only - marking as good", r.CmdString, ignored)
 		r.err = nil
 		r.dumpOutput()
 		return
@@ -191,10 +190,10 @@ func (r *Run) findFailures() {
 		r.RunFlag = ""
 	}
 	if r.passed() && len(r.FailedTests) != 0 {
-		log.Printf("%q - Expecting no errors but got: %v", r.CmdString, r.FailedTests)
+		fs.Logf(nil, "%q - Expecting no errors but got: %v", r.CmdString, r.FailedTests)
 		r.dumpOutput()
 	} else if !r.passed() && len(r.FailedTests) == 0 {
-		log.Printf("%q - Expecting errors but got none: %v", r.CmdString, r.FailedTests)
+		fs.Logf(nil, "%q - Expecting errors but got none: %v", r.CmdString, r.FailedTests)
 		r.dumpOutput()
 		r.FailedTests = oldFailedTests
 	}
@@ -214,23 +213,23 @@ func (r *Run) trial() {
 	CmdLine := r.nextCmdLine()
 	CmdString := toShell(CmdLine)
 	msg := fmt.Sprintf("%q - Starting (try %d/%d)", CmdString, r.Try, *maxTries)
-	log.Println(msg)
+	fs.Log(nil, msg)
 	logName := path.Join(r.LogDir, r.TrialName)
 	out, err := os.Create(logName)
 	if err != nil {
-		log.Fatalf("Couldn't create log file: %v", err)
+		fs.Fatalf(nil, "Couldn't create log file: %v", err)
 	}
 	defer func() {
 		err := out.Close()
 		if err != nil {
-			log.Fatalf("Failed to close log file: %v", err)
+			fs.Fatalf(nil, "Failed to close log file: %v", err)
 		}
 	}()
 	_, _ = fmt.Fprintln(out, msg)
 
 	// Early exit if --try-run
 	if *dryRun {
-		log.Printf("Not executing as --dry-run: %v", CmdLine)
+		fs.Logf(nil, "Not executing as --dry-run: %v", CmdLine)
 		_, _ = fmt.Fprintln(out, "--dry-run is set - not running")
 		return
 	}
@@ -238,7 +237,7 @@ func (r *Run) trial() {
 	// Start the test server if required
 	finish, err := testserver.Start(r.Remote)
 	if err != nil {
-		log.Printf("%s: Failed to start test server: %v", r.Remote, err)
+		fs.Logf(nil, "%s: Failed to start test server: %v", r.Remote, err)
 		_, _ = fmt.Fprintf(out, "%s: Failed to start test server: %v\n", r.Remote, err)
 		r.err = err
 		return
@@ -263,7 +262,7 @@ func (r *Run) trial() {
 	} else {
 		msg = fmt.Sprintf("%q - Finished ERROR in %v (try %d/%d): %v: Failed %v", CmdString, duration, r.Try, *maxTries, r.err, r.FailedTests)
 	}
-	log.Println(msg)
+	fs.Log(nil, msg)
 	_, _ = fmt.Fprintln(out, msg)
 }
 
@@ -304,23 +303,23 @@ func (r *Run) PackagePath() string {
 func (r *Run) MakeTestBinary() {
 	binary := r.BinaryPath()
 	binaryName := r.BinaryName()
-	log.Printf("%s: Making test binary %q", r.Path, binaryName)
+	fs.Logf(nil, "%s: Making test binary %q", r.Path, binaryName)
 	CmdLine := []string{"go", "test", "-c"}
 	if *race {
 		CmdLine = append(CmdLine, "-race")
 	}
 	if *dryRun {
-		log.Printf("Not executing: %v", CmdLine)
+		fs.Logf(nil, "Not executing: %v", CmdLine)
 		return
 	}
 	cmd := exec.Command(CmdLine[0], CmdLine[1:]...)
 	cmd.Dir = r.Path
 	err := cmd.Run()
 	if err != nil {
-		log.Fatalf("Failed to make test binary: %v", err)
+		fs.Fatalf(nil, "Failed to make test binary: %v", err)
 	}
 	if _, err := os.Stat(binary); err != nil {
-		log.Fatalf("Couldn't find test binary %q", binary)
+		fs.Fatalf(nil, "Couldn't find test binary %q", binary)
 	}
 }
 
@@ -332,7 +331,7 @@ func (r *Run) RemoveTestBinary() {
 	binary := r.BinaryPath()
 	err := os.Remove(binary) // Delete the binary when finished
 	if err != nil {
-		log.Printf("Error removing test binary %q: %v", binary, err)
+		fs.Logf(nil, "Error removing test binary %q: %v", binary, err)
 	}
 }
 
@@ -428,7 +427,7 @@ func (r *Run) Run(LogDir string, result chan<- *Run) {
 	for r.Try = 1; r.Try <= *maxTries; r.Try++ {
 		r.TrialName = r.Name() + ".txt"
 		r.TrialNames = append(r.TrialNames, r.TrialName)
-		log.Printf("Starting run with log %q", r.TrialName)
+		fs.Logf(nil, "Starting run with log %q", r.TrialName)
 		r.trial()
 		if r.passed() || r.NoRetries {
 			break
diff --git a/fstest/test_all/test_all.go b/fstest/test_all/test_all.go
index 79263f0e418b2..f1b40c34d98d1 100644
--- a/fstest/test_all/test_all.go
+++ b/fstest/test_all/test_all.go
@@ -12,7 +12,7 @@ Make TesTrun have a []string of flags to try - that then makes it generic
 
 import (
 	"flag"
-	"log"
+	"fmt"
 	"math/rand"
 	"os"
 	"path"
@@ -21,6 +21,7 @@ import (
 	"time"
 
 	_ "github.com/rclone/rclone/backend/all" // import all fs
+	"github.com/rclone/rclone/fs"
 	"github.com/rclone/rclone/fs/config/configfile"
 	"github.com/rclone/rclone/lib/pacer"
 )
@@ -68,8 +69,8 @@ func main() {
 	flag.Parse()
 	conf, err := NewConfig(*configFile)
 	if err != nil {
-		log.Println("test_all should be run from the root of the rclone source code")
-		log.Fatal(err)
+		fs.Log(nil, "test_all should be run from the root of the rclone source code")
+		fs.Fatal(nil, fmt.Sprint(err))
 	}
 	configfile.Install()
 
@@ -91,7 +92,7 @@ func main() {
 	if *clean {
 		err := cleanRemotes(conf)
 		if err != nil {
-			log.Fatalf("Failed to clean: %v", err)
+			fs.Fatalf(nil, "Failed to clean: %v", err)
 		}
 		return
 	}
@@ -100,7 +101,7 @@ func main() {
 	for _, remote := range conf.Backends {
 		names = append(names, remote.Remote)
 	}
-	log.Printf("Testing remotes: %s", strings.Join(names, ", "))
+	fs.Logf(nil, "Testing remotes: %s", strings.Join(names, ", "))
 
 	// Runs we will do for this test in random order
 	runs := conf.MakeRuns()
diff --git a/vfs/test_vfs/test_vfs.go b/vfs/test_vfs/test_vfs.go
index 4725a7bd7ec0c..19d57a670e11d 100644
--- a/vfs/test_vfs/test_vfs.go
+++ b/vfs/test_vfs/test_vfs.go
@@ -7,7 +7,6 @@ import (
 	"flag"
 	"fmt"
 	"io"
-	"log"
 	"math"
 	"math/rand"
 	"os"
@@ -16,6 +15,7 @@ import (
 	"sync/atomic"
 	"time"
 
+	"github.com/rclone/rclone/fs"
 	"github.com/rclone/rclone/lib/file"
 	"github.com/rclone/rclone/lib/random"
 )
@@ -93,13 +93,13 @@ func (t *Test) randomTest() {
 // logf logs things - not shown unless -v
 func (t *Test) logf(format string, a ...interface{}) {
 	if *verbose {
-		log.Printf(t.prefix+format, a...)
+		fs.Logf(nil, t.prefix+format, a)
 	}
 }
 
 // errorf logs errors
 func (t *Test) errorf(format string, a ...interface{}) {
-	log.Printf(t.prefix+"ERROR: "+format, a...)
+	fs.Logf(nil, t.prefix+"ERROR: "+format, a)
 }
 
 // list test
@@ -286,7 +286,7 @@ func main() {
 	flag.Parse()
 	args := flag.Args()
 	if len(args) != 1 {
-		log.Fatalf("%s: Syntax [opts] <directory>", os.Args[0])
+		fs.Fatalf(nil, "%s: Syntax [opts] <directory>", os.Args[0])
 	}
 	dir := args[0]
 	_ = file.MkdirAll(dir, 0777)
diff --git a/vfs/vfstest/fs.go b/vfs/vfstest/fs.go
index 97436d38ea76c..1c74f54dc8aaf 100644
--- a/vfs/vfstest/fs.go
+++ b/vfs/vfstest/fs.go
@@ -8,7 +8,6 @@ import (
 	"flag"
 	"fmt"
 	"io"
-	"log"
 	"os"
 	"os/exec"
 	"path"
@@ -69,7 +68,7 @@ func RunTests(t *testing.T, useVFS bool, minimumRequiredCacheMode vfscommon.Cach
 		if test.writeBack > 0 {
 			what += fmt.Sprintf(",WriteBack=%v", test.writeBack)
 		}
-		log.Printf("Starting test run with %s", what)
+		fs.Logf(nil, "Starting test run with %s", what)
 		ok := t.Run(what, func(t *testing.T) {
 			t.Run("TestTouchAndDelete", TestTouchAndDelete)
 			t.Run("TestRenameOpenHandle", TestRenameOpenHandle)
@@ -100,7 +99,7 @@ func RunTests(t *testing.T, useVFS bool, minimumRequiredCacheMode vfscommon.Cach
 			t.Run("TestWriteFileDup", TestWriteFileDup)
 			t.Run("TestWriteFileAppend", TestWriteFileAppend)
 		})
-		log.Printf("Finished test run with %s (ok=%v)", what, ok)
+		fs.Logf(nil, "Finished test run with %s (ok=%v)", what, ok)
 		run.Finalise()
 		if !ok {
 			break
@@ -146,12 +145,12 @@ func newRun(useVFS bool, vfsOpt *vfscommon.Options, mountFn mountlib.MountFn) *R
 	var err error
 	r.fremote, r.fremoteName, r.cleanRemote, err = fstest.RandomRemote()
 	if err != nil {
-		log.Fatalf("Failed to open remote %q: %v", *fstest.RemoteName, err)
+		fs.Fatalf(nil, "Failed to open remote %q: %v", *fstest.RemoteName, err)
 	}
 
 	err = r.fremote.Mkdir(context.Background(), "")
 	if err != nil {
-		log.Fatalf("Failed to open mkdir %q: %v", *fstest.RemoteName, err)
+		fs.Fatalf(nil, "Failed to open mkdir %q: %v", *fstest.RemoteName, err)
 	}
 
 	r.startMountSubProcess()
@@ -176,14 +175,14 @@ func (r *Run) Finalise() {
 		r.sendMountCommand("exit")
 		_, err := r.cmd.Process.Wait()
 		if err != nil {
-			log.Fatalf("mount sub process failed: %v", err)
+			fs.Fatalf(nil, "mount sub process failed: %v", err)
 		}
 	}
 	r.cleanRemote()
 	if !r.useVFS {
 		err := os.RemoveAll(r.mountPath)
 		if err != nil {
-			log.Printf("Failed to clean mountPath %q: %v", r.mountPath, err)
+			fs.Logf(nil, "Failed to clean mountPath %q: %v", r.mountPath, err)
 		}
 	}
 }
diff --git a/vfs/vfstest/submount.go b/vfs/vfstest/submount.go
index d8e8fb6ff6581..ac6400cdff89f 100644
--- a/vfs/vfstest/submount.go
+++ b/vfs/vfstest/submount.go
@@ -7,7 +7,6 @@ import (
 	"flag"
 	"fmt"
 	"io"
-	"log"
 	"os"
 	"os/exec"
 	"runtime"
@@ -48,7 +47,7 @@ func (r *Run) startMountSubProcess() {
 	}
 	r.os = realOs{}
 	r.mountPath = findMountPath()
-	log.Printf("startMountSubProcess %q (%q) %q", r.fremote, r.fremoteName, r.mountPath)
+	fs.Logf(nil, "startMountSubProcess %q (%q) %q", r.fremote, r.fremoteName, r.mountPath)
 
 	opt := runMountOpt{
 		MountPoint: r.mountPath,
@@ -59,7 +58,7 @@ func (r *Run) startMountSubProcess() {
 
 	opts, err := json.Marshal(&opt)
 	if err != nil {
-		log.Fatal(err)
+		fs.Fatal(nil, fmt.Sprint(err))
 	}
 
 	// Re-run this executable with a new option -run-mount
@@ -68,32 +67,32 @@ func (r *Run) startMountSubProcess() {
 	r.cmd.Stderr = os.Stderr
 	r.out, err = r.cmd.StdinPipe()
 	if err != nil {
-		log.Fatal(err)
+		fs.Fatal(nil, fmt.Sprint(err))
 	}
 	r.in, err = r.cmd.StdoutPipe()
 	if err != nil {
-		log.Fatal(err)
+		fs.Fatal(nil, fmt.Sprint(err))
 	}
 	err = r.cmd.Start()
 	if err != nil {
-		log.Fatal("startMountSubProcess failed", err)
+		fs.Fatal(nil, fmt.Sprint("startMountSubProcess failed", err))
 	}
 	r.scanner = bufio.NewScanner(r.in)
 
 	// Wait it for startup
-	log.Print("Waiting for mount to start")
+	fs.Log(nil, "Waiting for mount to start")
 	for r.scanner.Scan() {
 		rx := strings.TrimSpace(r.scanner.Text())
 		if rx == "STARTED" {
 			break
 		}
-		log.Printf("..Mount said: %s", rx)
+		fs.Logf(nil, "..Mount said: %s", rx)
 	}
 	if r.scanner.Err() != nil {
-		log.Printf("scanner err %v", r.scanner.Err())
+		fs.Logf(nil, "scanner err %v", r.scanner.Err())
 	}
 
-	log.Printf("startMountSubProcess: end")
+	fs.Logf(nil, "startMountSubProcess: end")
 }
 
 // Find a free path to run the mount on
@@ -101,7 +100,7 @@ func findMountPath() string {
 	if runtime.GOOS != "windows" {
 		mountPath, err := os.MkdirTemp("", "rclonefs-mount")
 		if err != nil {
-			log.Fatalf("Failed to create mount dir: %v", err)
+			fs.Fatalf(nil, "Failed to create mount dir: %v", err)
 		}
 		return mountPath
 	}
@@ -110,7 +109,7 @@ func findMountPath() string {
 	letter := file.FindUnusedDriveLetter()
 	drive := ""
 	if letter == 0 {
-		log.Fatalf("Couldn't find free drive letter for test")
+		fs.Fatalf(nil, "Couldn't find free drive letter for test")
 	} else {
 		drive = string(letter) + ":"
 	}
@@ -128,36 +127,36 @@ func isSubProcess() bool {
 // It reads commands from standard input and writes results to
 // standard output.
 func startMount(mountFn mountlib.MountFn, useVFS bool, opts string) {
-	log.Print("startMount")
+	fs.Log(nil, "startMount")
 	ctx := context.Background()
 
 	var opt runMountOpt
 	err := json.Unmarshal([]byte(opts), &opt)
 	if err != nil {
-		log.Fatalf("Unmarshal failed: %v", err)
+		fs.Fatalf(nil, "Unmarshal failed: %v", err)
 	}
 
 	fstest.Initialise()
 
 	f, err := cache.Get(ctx, opt.Remote)
 	if err != nil {
-		log.Fatalf("Failed to open remote %q: %v", opt.Remote, err)
+		fs.Fatalf(nil, "Failed to open remote %q: %v", opt.Remote, err)
 	}
 
 	err = f.Mkdir(ctx, "")
 	if err != nil {
-		log.Fatalf("Failed to mkdir %q: %v", opt.Remote, err)
+		fs.Fatalf(nil, "Failed to mkdir %q: %v", opt.Remote, err)
 	}
 
-	log.Printf("startMount: Mounting %q on %q with %q", opt.Remote, opt.MountPoint, opt.VFSOpt.CacheMode)
+	fs.Logf(nil, "startMount: Mounting %q on %q with %q", opt.Remote, opt.MountPoint, opt.VFSOpt.CacheMode)
 	mnt := mountlib.NewMountPoint(mountFn, opt.MountPoint, f, &opt.MountOpt, &opt.VFSOpt)
 
 	_, err = mnt.Mount()
 	if err != nil {
-		log.Fatalf("mount FAILED %q: %v", opt.Remote, err)
+		fs.Fatalf(nil, "mount FAILED %q: %v", opt.Remote, err)
 	}
 	defer umount(mnt)
-	log.Printf("startMount: mount OK")
+	fs.Logf(nil, "startMount: mount OK")
 	fmt.Println("STARTED") // signal to parent all is good
 
 	// Read commands from stdin
@@ -172,7 +171,7 @@ func startMount(mountFn mountlib.MountFn, useVFS bool, opts string) {
 
 	err = scanner.Err()
 	if err != nil {
-		log.Fatalf("scanner failed %q: %v", opt.Remote, err)
+		fs.Fatalf(nil, "scanner failed %q: %v", opt.Remote, err)
 	}
 }
 
@@ -221,17 +220,17 @@ func (r *Run) sendMountCommand(args ...string) {
 	} else {
 		_, err := io.WriteString(r.out, tx+"\n")
 		if err != nil {
-			log.Fatalf("WriteString err %v", err)
+			fs.Fatalf(nil, "WriteString err %v", err)
 		}
 		if !r.scanner.Scan() {
-			log.Fatalf("Mount has gone away")
+			fs.Fatalf(nil, "Mount has gone away")
 		}
 		rx = strings.Trim(r.scanner.Text(), "\r\n")
 	}
 	in := strings.Split(rx, "\t")
 	// log.Printf("Answer is %q", in)
 	if in[0] != "OK" {
-		log.Fatalf("Error from mount: %q", in[1:])
+		fs.Fatalf(nil, "Error from mount: %q", in[1:])
 	}
 }
 
@@ -254,25 +253,25 @@ func umount(mnt *mountlib.MountPoint) {
 			log.Printf("fusermount failed: %v", err)
 		}
 	*/
-	log.Printf("Unmounting %q", mnt.MountPoint)
+	fs.Logf(nil, "Unmounting %q", mnt.MountPoint)
 	err := mnt.Unmount()
 	if err != nil {
-		log.Printf("signal to umount failed - retrying: %v", err)
+		fs.Logf(nil, "signal to umount failed - retrying: %v", err)
 		time.Sleep(3 * time.Second)
 		err = mnt.Unmount()
 	}
 	if err != nil {
-		log.Fatalf("signal to umount failed: %v", err)
+		fs.Fatalf(nil, "signal to umount failed: %v", err)
 	}
-	log.Printf("Waiting for umount")
+	fs.Logf(nil, "Waiting for umount")
 	err = <-mnt.ErrChan
 	if err != nil {
-		log.Fatalf("umount failed: %v", err)
+		fs.Fatalf(nil, "umount failed: %v", err)
 	}
 
 	// Cleanup the VFS cache - umount has called Shutdown
 	err = mnt.VFS.CleanUp()
 	if err != nil {
-		log.Printf("Failed to cleanup the VFS cache: %v", err)
+		fs.Logf(nil, "Failed to cleanup the VFS cache: %v", err)
 	}
 }