diff --git a/.run/MainKt.run.xml b/.run/MainKt.run.xml
new file mode 100644
index 0000000000000000000000000000000000000000..9de3e67b91398aaa707c36464c9b7b019c7580fe
--- /dev/null
+++ b/.run/MainKt.run.xml
@@ -0,0 +1,15 @@
+<component name="ProjectRunConfigurationManager">
+  <configuration default="false" name="MainKt" type="JetRunConfigurationType" nameIsGenerated="true">
+    <module name="mailbox.mailbox-cli" />
+    <option name="VM_PARAMETERS" value="" />
+    <option name="PROGRAM_PARAMETERS" value="--debug" />
+    <option name="ALTERNATIVE_JRE_PATH_ENABLED" value="false" />
+    <option name="ALTERNATIVE_JRE_PATH" />
+    <option name="PASS_PARENT_ENVS" value="true" />
+    <option name="MAIN_CLASS_NAME" value="org.briarproject.mailbox.cli.MainKt" />
+    <option name="WORKING_DIRECTORY" value="" />
+    <method v="2">
+      <option name="Make" enabled="true" />
+    </method>
+  </configuration>
+</component>
\ No newline at end of file
diff --git a/mailbox-android/build.gradle b/mailbox-android/build.gradle
index 3825d95d14ac73864a93d231a0628eae1880ce70..23af48a7bdc66968523f48e0362cb2fb9024eaf7 100644
--- a/mailbox-android/build.gradle
+++ b/mailbox-android/build.gradle
@@ -52,6 +52,7 @@ configurations {
 dependencies {
     implementation project(path: ':mailbox-core', configuration: 'default')
 
+    implementation 'com.github.tony19:logback-android:2.0.0'
     implementation 'androidx.appcompat:appcompat:1.3.1'
     implementation "androidx.activity:activity-ktx:1.3.1"
     implementation "androidx.fragment:fragment-ktx:1.3.6"
diff --git a/mailbox-android/src/main/assets/logback.xml b/mailbox-android/src/main/assets/logback.xml
new file mode 100644
index 0000000000000000000000000000000000000000..a84fd707907a6ff4e2887484754d19f7348c7d73
--- /dev/null
+++ b/mailbox-android/src/main/assets/logback.xml
@@ -0,0 +1,13 @@
+<configuration>
+    <appender name="logcat" class="ch.qos.logback.classic.android.LogcatAppender">
+        <tagEncoder>
+            <pattern>%logger{12}</pattern>
+        </tagEncoder>
+        <encoder>
+            <pattern>%msg</pattern>
+        </encoder>
+    </appender>
+    <root level="DEBUG">
+        <appender-ref ref="logcat" />
+    </root>
+</configuration>
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxService.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxService.kt
index 4461d87f8751fc39013749ab88dcdf8282a379a9..57c2f8260b592e0c74ce899bfe6ff4be4d9e3d8c 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxService.kt
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/MailboxService.kt
@@ -12,14 +12,16 @@ import org.briarproject.mailbox.core.lifecycle.LifecycleManager
 import org.briarproject.mailbox.core.lifecycle.LifecycleManager.StartResult
 import org.briarproject.mailbox.core.lifecycle.LifecycleManager.StartResult.ALREADY_RUNNING
 import org.briarproject.mailbox.core.lifecycle.LifecycleManager.StartResult.SUCCESS
-import java.util.logging.Level
-import java.util.logging.Logger
+import org.slf4j.LoggerFactory.getLogger
+
 import javax.inject.Inject
 
 @AndroidEntryPoint
 class MailboxService : Service() {
 
     companion object {
+        private val LOG = getLogger(MailboxService::class.java.name)
+
         fun startService(context: Context) {
             val startIntent = Intent(context, MailboxService::class.java)
             ContextCompat.startForegroundService(context, startIntent)
@@ -31,8 +33,6 @@ class MailboxService : Service() {
         }
     }
 
-    private val LOG = Logger.getLogger(MailboxService::class.java.name)
-
     @Volatile
     internal var started = false
 
@@ -60,8 +60,7 @@ class MailboxService : Service() {
                     LOG.info("Already running")
                     stopSelf()
                 } else {
-                    if (LOG.isLoggable(Level.WARNING))
-                        LOG.warning("Startup failed: $result")
+                    if (LOG.isWarnEnabled) LOG.warn("Startup failed: $result")
                     // TODO: implement this
                     // showStartupFailure(result)
                     stopSelf()
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/system/AndroidWakeLockImpl.java b/mailbox-android/src/main/java/org/briarproject/mailbox/android/system/AndroidWakeLockImpl.java
index 7d6ac25c0fdd04abd6042995c65d37a2a2be5c4f..6eaa65eb325613973ed73079f64338f800836c9c 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/system/AndroidWakeLockImpl.java
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/system/AndroidWakeLockImpl.java
@@ -1,12 +1,12 @@
 package org.briarproject.mailbox.android.system;
 
-import static java.util.logging.Level.FINE;
-import static java.util.logging.Logger.getLogger;
+import static org.briarproject.mailbox.core.util.LogUtils.trace;
+import static org.slf4j.LoggerFactory.getLogger;
 
 import org.briarproject.mailbox.android.api.system.AndroidWakeLock;
+import org.slf4j.Logger;
 
 import java.util.concurrent.atomic.AtomicInteger;
-import java.util.logging.Logger;
 
 import javax.annotation.concurrent.GuardedBy;
 import javax.annotation.concurrent.ThreadSafe;
@@ -19,8 +19,7 @@ import javax.annotation.concurrent.ThreadSafe;
 @ThreadSafe
 class AndroidWakeLockImpl implements AndroidWakeLock {
 
-    private static final Logger LOG =
-            getLogger(AndroidWakeLockImpl.class.getName());
+    private static final Logger LOG = getLogger(AndroidWakeLockImpl.class);
 
     private static final AtomicInteger INSTANCE_ID = new AtomicInteger(0);
 
@@ -40,13 +39,9 @@ class AndroidWakeLockImpl implements AndroidWakeLock {
     public void acquire() {
         synchronized (lock) {
             if (held) {
-                if (LOG.isLoggable(FINE)) {
-                    LOG.fine(tag + " already acquired");
-                }
+                trace(LOG, () -> tag + " already acquired");
             } else {
-                if (LOG.isLoggable(FINE)) {
-                    LOG.fine(tag + " acquiring shared wake lock");
-                }
+                trace(LOG, () -> tag + " acquiring shared wake lock");
                 held = true;
                 sharedWakeLock.acquire();
             }
@@ -57,15 +52,11 @@ class AndroidWakeLockImpl implements AndroidWakeLock {
     public void release() {
         synchronized (lock) {
             if (held) {
-                if (LOG.isLoggable(FINE)) {
-                    LOG.fine(tag + " releasing shared wake lock");
-                }
+                trace(LOG, () -> tag + " releasing shared wake lock");
                 held = false;
                 sharedWakeLock.release();
             } else {
-                if (LOG.isLoggable(FINE)) {
-                    LOG.fine(tag + " already released");
-                }
+                trace(LOG, () -> tag + " already released");
             }
         }
     }
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/android/system/RenewableWakeLock.java b/mailbox-android/src/main/java/org/briarproject/mailbox/android/system/RenewableWakeLock.java
index d4a910cc144bb899053608210e91f6417995bbf4..a39cbf5f53ff7759762707f4ae8a08af2edc74dd 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/android/system/RenewableWakeLock.java
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/android/system/RenewableWakeLock.java
@@ -1,18 +1,19 @@
 package org.briarproject.mailbox.android.system;
 
+import static org.briarproject.mailbox.core.util.LogUtils.info;
+import static org.briarproject.mailbox.core.util.LogUtils.trace;
+import static org.briarproject.mailbox.core.util.LogUtils.warn;
+import static org.slf4j.LoggerFactory.getLogger;
 import static java.util.Objects.requireNonNull;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
-import static java.util.logging.Level.FINE;
-import static java.util.logging.Level.INFO;
-import static java.util.logging.Level.WARNING;
-import static java.util.logging.Logger.getLogger;
 
 import android.os.PowerManager;
 import android.os.PowerManager.WakeLock;
 
+import org.slf4j.Logger;
+
 import java.util.concurrent.Future;
 import java.util.concurrent.ScheduledExecutorService;
-import java.util.logging.Logger;
 
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.GuardedBy;
@@ -21,8 +22,7 @@ import javax.annotation.concurrent.ThreadSafe;
 @ThreadSafe
 class RenewableWakeLock implements SharedWakeLock {
 
-    private static final Logger LOG =
-            getLogger(RenewableWakeLock.class.getName());
+    private static final Logger LOG = getLogger(RenewableWakeLock.class);
 
     private final PowerManager powerManager;
     private final ScheduledExecutorService scheduledExecutorService;
@@ -61,9 +61,7 @@ class RenewableWakeLock implements SharedWakeLock {
         synchronized (lock) {
             refCount++;
             if (refCount == 1) {
-                if (LOG.isLoggable(INFO)) {
-                    LOG.info("Acquiring wake lock " + tag);
-                }
+                info(LOG, () -> "Acquiring wake lock " + tag);
                 wakeLock = powerManager.newWakeLock(levelAndFlags, tag);
                 // We do our own reference counting so we can replace the lock
                 // TODO: Check whether using a ref-counted wake lock affects
@@ -73,26 +71,24 @@ class RenewableWakeLock implements SharedWakeLock {
                 future = scheduledExecutorService.schedule(this::renew,
                         durationMs, MILLISECONDS);
                 acquired = android.os.SystemClock.elapsedRealtime();
-            } else if (LOG.isLoggable(FINE)) {
-                LOG.fine("Wake lock " + tag + " has " + refCount + " holders");
+            } else {
+                trace(LOG, () -> "Wake lock " + tag + " has " + refCount + " holders");
             }
         }
     }
 
     private void renew() {
-        if (LOG.isLoggable(INFO)) LOG.info("Renewing wake lock " + tag);
+        info(LOG, () -> "Renewing wake lock " + tag);
         synchronized (lock) {
             if (wakeLock == null) {
                 LOG.info("Already released");
                 return;
             }
-            if (LOG.isLoggable(FINE)) {
-                LOG.fine("Wake lock " + tag + " has " + refCount + " holders");
-            }
+            trace(LOG, () -> "Wake lock " + tag + " has " + refCount + " holders");
             long now = android.os.SystemClock.elapsedRealtime();
             long expiry = acquired + durationMs + safetyMarginMs;
-            if (now > expiry && LOG.isLoggable(WARNING)) {
-                LOG.warning("Wake lock expired " + (now - expiry) + " ms ago");
+            if (now > expiry) {
+                warn(LOG, () -> "Wake lock expired " + (now - expiry) + " ms ago");
             }
             WakeLock oldWakeLock = wakeLock;
             wakeLock = powerManager.newWakeLock(levelAndFlags, tag);
@@ -110,16 +106,14 @@ class RenewableWakeLock implements SharedWakeLock {
         synchronized (lock) {
             refCount--;
             if (refCount == 0) {
-                if (LOG.isLoggable(INFO)) {
-                    LOG.info("Releasing wake lock " + tag);
-                }
+                info(LOG, () -> "Releasing wake lock " + tag);
                 requireNonNull(future).cancel(false);
                 future = null;
                 requireNonNull(wakeLock).release();
                 wakeLock = null;
                 acquired = 0;
-            } else if (LOG.isLoggable(FINE)) {
-                LOG.fine("Wake lock " + tag + " has " + refCount + " holders");
+            } else {
+                trace(LOG, () -> "Wake lock " + tag + " has " + refCount + " holders");
             }
         }
     }
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/core/tor/AndroidTorModule.kt b/mailbox-android/src/main/java/org/briarproject/mailbox/core/tor/AndroidTorModule.kt
index d1eb0cd88b383bce439f0f99c1b786976b9c75e7..b44156e1409ca3801c2e3c8a39d376c389e9b5de 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/core/tor/AndroidTorModule.kt
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/core/tor/AndroidTorModule.kt
@@ -13,6 +13,8 @@ import org.briarproject.mailbox.core.lifecycle.LifecycleManager
 import org.briarproject.mailbox.core.system.Clock
 import org.briarproject.mailbox.core.system.LocationUtils
 import org.briarproject.mailbox.core.system.ResourceProvider
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory.getLogger
 import java.util.concurrent.Executor
 import javax.inject.Singleton
 
@@ -20,6 +22,10 @@ import javax.inject.Singleton
 @InstallIn(SingletonComponent::class)
 internal class AndroidTorModule {
 
+    companion object {
+        private val LOG: Logger = getLogger(AndroidTorModule::class.java)
+    }
+
     @Provides
     @Singleton
     fun provideResourceProvider(@ApplicationContext ctx: Context): ResourceProvider {
@@ -69,7 +75,7 @@ internal class AndroidTorModule {
                     else -> continue
                 }
             }
-//            LOG.info("Tor is not supported on this architecture")
+            LOG.info("Tor is not supported on this architecture")
             return "" // TODO
         }
 
diff --git a/mailbox-android/src/main/java/org/briarproject/mailbox/core/tor/AndroidTorPlugin.java b/mailbox-android/src/main/java/org/briarproject/mailbox/core/tor/AndroidTorPlugin.java
index d25ee11357e787d1e0108329863e51b9a8a7859e..b23a4d321eafc3e8c3cf3ed4675a9de74f6f1161 100644
--- a/mailbox-android/src/main/java/org/briarproject/mailbox/core/tor/AndroidTorPlugin.java
+++ b/mailbox-android/src/main/java/org/briarproject/mailbox/core/tor/AndroidTorPlugin.java
@@ -1,9 +1,9 @@
 package org.briarproject.mailbox.core.tor;
 
 import static android.os.Build.VERSION.SDK_INT;
+import static org.briarproject.mailbox.core.util.LogUtils.info;
+import static org.slf4j.LoggerFactory.getLogger;
 import static java.util.Arrays.asList;
-import static java.util.logging.Level.INFO;
-import static java.util.logging.Logger.getLogger;
 
 import android.content.Context;
 import android.content.pm.PackageInfo;
@@ -16,6 +16,7 @@ import org.briarproject.mailbox.android.api.system.AndroidWakeLockManager;
 import org.briarproject.mailbox.core.system.Clock;
 import org.briarproject.mailbox.core.system.LocationUtils;
 import org.briarproject.mailbox.core.system.ResourceProvider;
+import org.slf4j.Logger;
 
 import java.io.File;
 import java.io.FileInputStream;
@@ -25,7 +26,6 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 import java.util.concurrent.Executor;
-import java.util.logging.Logger;
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipInputStream;
 
@@ -37,8 +37,7 @@ public class AndroidTorPlugin extends TorPlugin {
     private static final String TOR_LIB_NAME = "libtor.so";
     private static final String OBFS4_LIB_NAME = "libobfs4proxy.so";
 
-    private static final Logger LOG =
-            getLogger(AndroidTorPlugin.class.getName());
+    private static final Logger LOG = getLogger(AndroidTorPlugin.class);
 
     private final Context ctx;
     private final AndroidWakeLock wakeLock;
@@ -152,10 +151,8 @@ public class AndroidTorPlugin extends TorPlugin {
             for (ZipEntry e = zin.getNextEntry(); e != null;
                  e = zin.getNextEntry()) {
                 if (libPaths.contains(e.getName())) {
-                    if (LOG.isLoggable(INFO)) {
-                        LOG.info("Extracting " + e.getName()
-                                + " from " + apk.getAbsolutePath());
-                    }
+                    String ex = e.getName();
+                    info(LOG, () -> "Extracting " + ex + " from " + apk.getAbsolutePath());
                     extract(zin, dest); // Zip input stream will be closed
                     return;
                 }
diff --git a/mailbox-cli/build.gradle b/mailbox-cli/build.gradle
index e0c9c7d51f2858d7f9709579e9a58a236085214c..c2d1a7c948c1857df79b5ef734ed4a0a69dfac98 100644
--- a/mailbox-cli/build.gradle
+++ b/mailbox-cli/build.gradle
@@ -15,7 +15,7 @@ configurations {
 dependencies {
     implementation project(path: ':mailbox-core', configuration: 'default')
 
-    implementation 'org.slf4j:slf4j-simple:1.7.30'
+    implementation "ch.qos.logback:logback-classic:1.2.5"
     implementation 'com.github.ajalt:clikt:2.2.0'
 
     def jna_version = '5.8.0'
diff --git a/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/Main.kt b/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/Main.kt
index 22f7f445ba12c2c1659a721a5d3a9b8dbff308fe..e47a3d7fd7a47b953937453d8b77b877766d4e54 100644
--- a/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/Main.kt
+++ b/mailbox-cli/src/main/java/org/briarproject/mailbox/cli/Main.kt
@@ -1,5 +1,7 @@
 package org.briarproject.mailbox.cli
 
+import ch.qos.logback.classic.Level
+import ch.qos.logback.classic.Logger
 import com.github.ajalt.clikt.core.CliktCommand
 import com.github.ajalt.clikt.parameters.options.counted
 import com.github.ajalt.clikt.parameters.options.flag
@@ -7,8 +9,7 @@ import com.github.ajalt.clikt.parameters.options.option
 import org.briarproject.mailbox.core.CoreEagerSingletons
 import org.briarproject.mailbox.core.JavaCliEagerSingletons
 import org.briarproject.mailbox.core.lifecycle.LifecycleManager
-import org.slf4j.impl.SimpleLogger.DEFAULT_LOG_LEVEL_KEY
-import java.lang.System.setProperty
+import org.slf4j.LoggerFactory.getLogger
 import java.util.logging.Level.ALL
 import java.util.logging.Level.INFO
 import java.util.logging.Level.WARNING
@@ -20,7 +21,7 @@ class Main : CliktCommand(
     help = "Command line interface for the Briar Mailbox"
 ) {
     private val debug by option("--debug", "-d", help = "Enable printing of debug messages").flag(
-        default = true//false
+        default = false
     )
     private val verbosity by option(
         "--verbose",
@@ -39,19 +40,20 @@ class Main : CliktCommand(
 
     override fun run() {
         // logging
-        val levelSlf4j = if (debug) "DEBUG" else when (verbosity) {
-            0 -> "WARN"
-            1 -> "INFO"
-            else -> "DEBUG"
+        val levelSlf4j = if (debug) Level.DEBUG else when (verbosity) {
+            0 -> Level.WARN
+            1 -> Level.INFO
+            else -> Level.DEBUG
         }
         val level = if (debug) ALL else when (verbosity) {
             0 -> WARNING
             1 -> INFO
             else -> ALL
         }
-        setProperty(DEFAULT_LOG_LEVEL_KEY, levelSlf4j)
+        (getLogger(Logger.ROOT_LOGGER_NAME) as Logger).level = levelSlf4j
         LogManager.getLogManager().getLogger("").level = level
 
+        getLogger(this.javaClass).debug("Hello Mailbox")
         println("Hello Mailbox")
 
         val javaCliComponent = DaggerJavaCliComponent.builder().build()
diff --git a/mailbox-cli/src/main/java/org/briarproject/mailbox/core/tor/JavaTorModule.kt b/mailbox-cli/src/main/java/org/briarproject/mailbox/core/tor/JavaTorModule.kt
index 53d14fc7da85d2293c5fe2642618d7ff0eff6275..f8421ef636326b1ef85ff3e94d4ecc6e9b56f51a 100644
--- a/mailbox-cli/src/main/java/org/briarproject/mailbox/core/tor/JavaTorModule.kt
+++ b/mailbox-cli/src/main/java/org/briarproject/mailbox/core/tor/JavaTorModule.kt
@@ -10,6 +10,8 @@ import org.briarproject.mailbox.core.system.Clock
 import org.briarproject.mailbox.core.system.LocationUtils
 import org.briarproject.mailbox.core.system.ResourceProvider
 import org.briarproject.mailbox.core.util.OsUtils.isLinux
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
 import java.io.File
 import java.util.concurrent.Executor
 import javax.inject.Singleton
@@ -18,6 +20,10 @@ import javax.inject.Singleton
 @InstallIn(SingletonComponent::class)
 internal class JavaTorModule {
 
+    companion object {
+        private val LOG: Logger = LoggerFactory.getLogger(JavaTorModule::class.java)
+    }
+
     @Provides
     @Singleton
     fun provideResourceProvider() = ResourceProvider { name, extension ->
@@ -56,9 +62,9 @@ internal class JavaTorModule {
     private val architecture: String
         get() {
             if (isLinux()) {
-//                if (LOG.isLoggable(Level.INFO)) {
-//                    LOG.info("System's os.arch is $arch")
-//                }
+                if (LOG.isInfoEnabled) {
+                    LOG.info("System's os.arch is ${System.getProperty("os.arch")}")
+                }
                 when (System.getProperty("os.arch")) {
                     "amd64" -> {
                         return "linux-x86_64"
@@ -71,7 +77,7 @@ internal class JavaTorModule {
                     }
                 }
             }
-//            LOG.info("Tor is not supported on this architecture")
+            LOG.info("Tor is not supported on this architecture")
             return "" // TODO
         }
 
diff --git a/mailbox-core/build.gradle b/mailbox-core/build.gradle
index 8dd8fb3806806261d5caa70aa58dfbbce997a6ab..7735fd50ab055516de40d8acfbc9f818df092124 100644
--- a/mailbox-core/build.gradle
+++ b/mailbox-core/build.gradle
@@ -21,7 +21,7 @@ dependencies {
     def ktorVersion = '1.6.2'
     implementation "io.ktor:ktor-server-core:$ktorVersion"
     implementation "io.ktor:ktor-server-netty:$ktorVersion"
-    implementation "ch.qos.logback:logback-classic:1.2.5"
+    api "org.slf4j:slf4j-api:1.7.32"
 
     testImplementation "org.junit.jupiter:junit-jupiter-api:$junit_version"
     testImplementation "org.junit.jupiter:junit-jupiter-params:$junit_version"
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManagerImpl.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManagerImpl.java
index 982a62ca34ca354a90e52a6ad8bddc6a15e60726..6bd84052dfbc0304e90b6a3fb6d04699be9d50d3 100644
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManagerImpl.java
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/lifecycle/LifecycleManagerImpl.java
@@ -9,23 +9,22 @@ import static org.briarproject.mailbox.core.lifecycle.LifecycleManager.Lifecycle
 import static org.briarproject.mailbox.core.lifecycle.LifecycleManager.StartResult.ALREADY_RUNNING;
 import static org.briarproject.mailbox.core.lifecycle.LifecycleManager.StartResult.SERVICE_ERROR;
 import static org.briarproject.mailbox.core.lifecycle.LifecycleManager.StartResult.SUCCESS;
+import static org.briarproject.mailbox.core.util.LogUtils.info;
 import static org.briarproject.mailbox.core.util.LogUtils.logDuration;
 import static org.briarproject.mailbox.core.util.LogUtils.logException;
 import static org.briarproject.mailbox.core.util.LogUtils.now;
-import static java.util.logging.Level.FINE;
-import static java.util.logging.Level.INFO;
-import static java.util.logging.Level.WARNING;
-import static java.util.logging.Logger.getLogger;
+import static org.briarproject.mailbox.core.util.LogUtils.trace;
+import static org.slf4j.LoggerFactory.getLogger;
 
 import org.briarproject.mailbox.core.db.DatabaseComponent;
 import org.briarproject.mailbox.core.db.MigrationListener;
+import org.slf4j.Logger;
 
 import java.util.List;
 import java.util.concurrent.CopyOnWriteArrayList;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Semaphore;
-import java.util.logging.Logger;
 
 import javax.annotation.concurrent.ThreadSafe;
 import javax.inject.Inject;
@@ -33,8 +32,7 @@ import javax.inject.Inject;
 @ThreadSafe
 class LifecycleManagerImpl implements LifecycleManager, MigrationListener {
 
-    private static final Logger LOG =
-            getLogger(LifecycleManagerImpl.class.getName());
+    private static final Logger LOG = getLogger(LifecycleManagerImpl.class);
 
     private final DatabaseComponent db;
     private final List<Service> services;
@@ -57,23 +55,19 @@ class LifecycleManagerImpl implements LifecycleManager, MigrationListener {
 
     @Override
     public void registerService(Service s) {
-        if (LOG.isLoggable(INFO))
-            LOG.info("Registering service " + s.getClass().getSimpleName());
+        info(LOG, () -> "Registering service " + s.getClass().getSimpleName());
         services.add(s);
     }
 
     @Override
     public void registerOpenDatabaseHook(OpenDatabaseHook hook) {
-        if (LOG.isLoggable(INFO)) {
-            LOG.info("Registering open database hook "
-                    + hook.getClass().getSimpleName());
-        }
+        info(LOG, () -> "Registering open database hook " + hook.getClass().getSimpleName());
         openDatabaseHooks.add(hook);
     }
 
     @Override
     public void registerForShutdown(ExecutorService e) {
-        LOG.info("Registering executor " + e.getClass().getSimpleName());
+        info(LOG, () -> "Registering executor " + e.getClass().getSimpleName());
         executors.add(e);
     }
 
@@ -87,8 +81,8 @@ class LifecycleManagerImpl implements LifecycleManager, MigrationListener {
             LOG.info("Opening database");
             long start = now();
             boolean reopened = db.open(this);
-            if (reopened) logDuration(LOG, "Reopening database", start);
-            else logDuration(LOG, "Creating database", start);
+            if (reopened) logDuration(LOG, () -> "Reopening database", start);
+            else logDuration(LOG, () -> "Creating database", start);
 
             LOG.info("Starting services");
             state = STARTING_SERVICES;
@@ -97,17 +91,14 @@ class LifecycleManagerImpl implements LifecycleManager, MigrationListener {
             for (Service s : services) {
                 start = now();
                 s.startService();
-                if (LOG.isLoggable(FINE)) {
-                    logDuration(LOG, "Starting service "
-                            + s.getClass().getSimpleName(), start);
-                }
+                logDuration(LOG, () -> "Starting service " + s.getClass().getSimpleName(), start);
             }
 
             state = RUNNING;
             startupLatch.countDown();
             return SUCCESS;
         } catch (ServiceException e) {
-            logException(LOG, WARNING, e);
+            logException(LOG, e);
             return SERVICE_ERROR;
         } finally {
             startStopSemaphore.release();
@@ -129,7 +120,7 @@ class LifecycleManagerImpl implements LifecycleManager, MigrationListener {
         try {
             startStopSemaphore.acquire();
         } catch (InterruptedException e) {
-            LOG.warning("Interrupted while waiting to stop services");
+            LOG.warn("Interrupted while waiting to stop services");
             return;
         }
         try {
@@ -138,24 +129,18 @@ class LifecycleManagerImpl implements LifecycleManager, MigrationListener {
             for (Service s : services) {
                 long start = now();
                 s.stopService();
-                if (LOG.isLoggable(FINE)) {
-                    logDuration(LOG, "Stopping service "
-                            + s.getClass().getSimpleName(), start);
-                }
+                logDuration(LOG, () -> "Stopping service " + s.getClass().getSimpleName(), start);
             }
             for (ExecutorService e : executors) {
-                if (LOG.isLoggable(FINE)) {
-                    LOG.fine("Stopping executor "
-                            + e.getClass().getSimpleName());
-                }
+                trace(LOG, () -> "Stopping executor " + e.getClass().getSimpleName());
                 e.shutdownNow();
             }
             long start = now();
             db.close();
-            logDuration(LOG, "Closing database", start);
+            logDuration(LOG, () -> "Closing database", start);
             shutdownLatch.countDown();
         } catch (ServiceException e) {
-            logException(LOG, WARNING, e);
+            logException(LOG, e);
         } finally {
             startStopSemaphore.release();
         }
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerManager.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerManager.kt
index 8741eb1edb031ffd63cfcc69a9cca35520d8ccb0..335367f3a458d93bb0aec78acd685c1db9adb53b 100644
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerManager.kt
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/server/WebServerManager.kt
@@ -8,7 +8,7 @@ import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.GlobalScope
 import kotlinx.coroutines.launch
 import org.briarproject.mailbox.core.lifecycle.Service
-import java.util.logging.Logger.getLogger
+import org.slf4j.LoggerFactory.getLogger
 import javax.inject.Inject
 import javax.inject.Singleton
 
@@ -17,7 +17,7 @@ class WebServerManager @Inject constructor() : Service {
 
     internal companion object {
         private const val PORT = 8888
-        private val LOG = getLogger(WebServerManager::class.java.name)
+        private val LOG = getLogger(WebServerManager::class.java)
     }
 
     private val server by lazy {
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/TorPlugin.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/TorPlugin.java
index eff73f5e95cf65d410d7c847eaeb59f8281af93d..5adf98ddafa69ffa85ce91326c7bb9d2318840ea 100644
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/TorPlugin.java
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/tor/TorPlugin.java
@@ -10,15 +10,15 @@ import static org.briarproject.mailbox.core.tor.TorPlugin.State.INACTIVE;
 import static org.briarproject.mailbox.core.tor.TorPlugin.State.STARTING_STOPPING;
 import static org.briarproject.mailbox.core.util.IoUtils.copyAndClose;
 import static org.briarproject.mailbox.core.util.IoUtils.tryToClose;
+import static org.briarproject.mailbox.core.util.LogUtils.info;
 import static org.briarproject.mailbox.core.util.LogUtils.logException;
+import static org.briarproject.mailbox.core.util.LogUtils.warn;
 import static org.briarproject.mailbox.core.util.PrivacyUtils.scrubOnion;
+import static org.slf4j.LoggerFactory.getLogger;
 import static java.util.Arrays.asList;
 import static java.util.Collections.singletonList;
 import static java.util.Collections.singletonMap;
 import static java.util.Objects.requireNonNull;
-import static java.util.logging.Level.INFO;
-import static java.util.logging.Level.WARNING;
-import static java.util.logging.Logger.getLogger;
 
 import net.freehaven.tor.control.EventHandler;
 import net.freehaven.tor.control.TorControlConnection;
@@ -29,6 +29,7 @@ import org.briarproject.mailbox.core.lifecycle.ServiceException;
 import org.briarproject.mailbox.core.system.Clock;
 import org.briarproject.mailbox.core.system.LocationUtils;
 import org.briarproject.mailbox.core.system.ResourceProvider;
+import org.slf4j.Logger;
 
 import java.io.EOFException;
 import java.io.File;
@@ -46,7 +47,6 @@ import java.util.Map;
 import java.util.Scanner;
 import java.util.concurrent.Executor;
 import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.logging.Logger;
 import java.util.zip.ZipInputStream;
 
 import javax.annotation.Nullable;
@@ -55,7 +55,7 @@ import javax.annotation.concurrent.ThreadSafe;
 
 abstract class TorPlugin implements Service, EventHandler {
 
-    private static final Logger LOG = getLogger(TorPlugin.class.getName());
+    private static final Logger LOG = getLogger(TorPlugin.class);
 
     private static final String[] EVENTS = {
             "CIRC", "ORCONN", "HS_DESC", "NOTICE", "WARN", "ERR"
@@ -87,14 +87,14 @@ abstract class TorPlugin implements Service, EventHandler {
     protected abstract long getLastUpdateTime();
 
     TorPlugin(Executor ioExecutor,
-                        NetworkManager networkManager,
-                        LocationUtils locationUtils,
-                        Clock clock,
-                        ResourceProvider resourceProvider,
-                        CircumventionProvider circumventionProvider,
-                        Backoff backoff,
-                        String architecture,
-                        File torDirectory) {
+              NetworkManager networkManager,
+              LocationUtils locationUtils,
+              Clock clock,
+              ResourceProvider resourceProvider,
+              CircumventionProvider circumventionProvider,
+              Backoff backoff,
+              String architecture,
+              File torDirectory) {
         this.ioExecutor = ioExecutor;
         this.networkManager = networkManager;
         this.locationUtils = locationUtils;
@@ -126,14 +126,14 @@ abstract class TorPlugin implements Service, EventHandler {
         if (used.getAndSet(true)) throw new IllegalStateException();
         if (!torDirectory.exists()) {
             if (!torDirectory.mkdirs()) {
-                LOG.warning("Could not create Tor directory.");
+                LOG.warn("Could not create Tor directory.");
                 throw new ServiceException();
             }
         }
         // Install or update the assets if necessary
         if (!assetsAreUpToDate()) installAssets();
         if (cookieFile.exists() && !cookieFile.delete())
-            LOG.warning("Old auth cookie not deleted");
+            LOG.warn("Old auth cookie not deleted");
         // Start a new Tor process
         LOG.info("Starting Tor");
         File torFile = getTorExecutableFile();
@@ -152,7 +152,7 @@ abstract class TorPlugin implements Service, EventHandler {
             throw new ServiceException(e);
         }
         // Log the process's standard output until it detaches
-        if (LOG.isLoggable(INFO)) {
+        if (LOG.isInfoEnabled()) {
             Scanner stdout = new Scanner(torProcess.getInputStream());
             Scanner stderr = new Scanner(torProcess.getErrorStream());
             while (stdout.hasNextLine() || stderr.hasNextLine()) {
@@ -170,16 +170,15 @@ abstract class TorPlugin implements Service, EventHandler {
             // Wait for the process to detach or exit
             int exit = torProcess.waitFor();
             if (exit != 0) {
-                if (LOG.isLoggable(WARNING))
-                    LOG.warning("Tor exited with value " + exit);
+                warn(LOG, () -> "Tor exited with value " + exit);
                 throw new ServiceException();
             }
             // Wait for the auth cookie file to be created/updated
             long start = clock.currentTimeMillis();
             while (cookieFile.length() < 32) {
                 if (clock.currentTimeMillis() - start > COOKIE_TIMEOUT_MS) {
-                    LOG.warning("Auth cookie not created");
-                    if (LOG.isLoggable(INFO)) listFiles(torDirectory);
+                    LOG.warn("Auth cookie not created");
+                    if (LOG.isInfoEnabled()) listFiles(torDirectory);
                     throw new ServiceException();
                 }
                 //noinspection BusyWait
@@ -187,7 +186,7 @@ abstract class TorPlugin implements Service, EventHandler {
             }
             LOG.info("Auth cookie created");
         } catch (InterruptedException e) {
-            LOG.warning("Interrupted while starting Tor");
+            LOG.warn("Interrupted while starting Tor");
             Thread.currentThread().interrupt();
             throw new ServiceException();
         }
@@ -232,7 +231,7 @@ abstract class TorPlugin implements Service, EventHandler {
             extract(getGeoIpInputStream(), geoIpFile);
             extract(getConfigInputStream(), configFile);
             if (!doneFile.createNewFile())
-                LOG.warning("Failed to create done file");
+                LOG.warn("Failed to create done file");
         } catch (IOException e) {
             throw new ServiceException(e);
         }
@@ -244,16 +243,14 @@ abstract class TorPlugin implements Service, EventHandler {
     }
 
     protected void installTorExecutable() throws IOException {
-        if (LOG.isLoggable(INFO))
-            LOG.info("Installing Tor binary for " + architecture);
+        info(LOG, () -> "Installing Tor binary for " + architecture);
         File torFile = getTorExecutableFile();
         extract(getTorInputStream(), torFile);
         if (!torFile.setExecutable(true, true)) throw new IOException();
     }
 
     protected void installObfs4Executable() throws IOException {
-        if (LOG.isLoggable(INFO))
-            LOG.info("Installing obfs4proxy binary for " + architecture);
+        info(LOG, () -> "Installing obfs4proxy binary for " + architecture);
         File obfs4File = getObfs4ExecutableFile();
         extract(getObfs4InputStream(), obfs4File);
         if (!obfs4File.setExecutable(true, true)) throw new IOException();
@@ -309,7 +306,7 @@ abstract class TorPlugin implements Service, EventHandler {
             }
             return b;
         } finally {
-            tryToClose(in, LOG, WARNING);
+            tryToClose(in, LOG);
         }
     }
 
@@ -333,23 +330,21 @@ abstract class TorPlugin implements Service, EventHandler {
                 response = controlConnection.addOnion(privKey, portLines);
             }
         } catch (IOException e) {
-            logException(LOG, WARNING, e);
+            logException(LOG, e);
             return;
         }
         if (!response.containsKey(HS_ADDRESS)) {
-            LOG.warning("Tor did not return a hidden service address");
+            LOG.warn("Tor did not return a hidden service address");
             return;
         }
         if (privKey == null && !response.containsKey(HS_PRIVKEY)) {
-            LOG.warning("Tor did not return a private key");
+            LOG.warn("Tor did not return a private key");
             return;
         }
         String onion3 = response.get(HS_ADDRESS);
-        if (LOG.isLoggable(INFO)) {
-            LOG.info("V3 hidden service " + scrubOnion(onion3));
-        }
+        info(LOG, () -> "V3 hidden service " + scrubOnion(onion3));
         // TODO remove
-        LOG.warning("V3 hidden service: http://" + onion3 + ".onion");
+        LOG.warn("V3 hidden service: http://" + onion3 + ".onion");
         if (privKey == null) {
             // TODO Save the hidden service's onion hostname
 //			p.put(PROP_ONION_V3, onion3);
@@ -387,7 +382,7 @@ abstract class TorPlugin implements Service, EventHandler {
     @Override
     public void stopService() {
         ServerSocket ss = state.setStopped();
-        tryToClose(ss, LOG, WARNING);
+        tryToClose(ss, LOG);
         if (controlSocket != null && controlConnection != null) {
             try {
                 LOG.info("Stopping Tor");
@@ -395,7 +390,7 @@ abstract class TorPlugin implements Service, EventHandler {
                 controlConnection.shutdownTor("TERM");
                 controlSocket.close();
             } catch (IOException e) {
-                logException(LOG, WARNING, e);
+                logException(LOG, e);
             }
         }
     }
@@ -415,8 +410,7 @@ abstract class TorPlugin implements Service, EventHandler {
 
     @Override
     public void orConnStatus(String status, String orName) {
-        if (LOG.isLoggable(INFO))
-            LOG.info("OR connection " + status + " " + orName);
+        info(LOG, () -> "OR connection " + status + " " + orName);
         if (status.equals("CLOSED") || status.equals("FAILED")) {
             // Check whether we've lost connectivity
             updateConnectionStatus(networkManager.getNetworkStatus()
@@ -434,7 +428,7 @@ abstract class TorPlugin implements Service, EventHandler {
 
     @Override
     public void message(String severity, String msg) {
-        if (LOG.isLoggable(INFO)) LOG.info(severity + " " + msg);
+        info(LOG, () -> severity + " " + msg);
         if (severity.equals("NOTICE") && msg.startsWith("Bootstrapped 100%")) {
             state.setBootstrapped();
             backoff.reset();
@@ -457,7 +451,7 @@ abstract class TorPlugin implements Service, EventHandler {
             try {
                 if (state.isTorRunning()) enableNetwork(false);
             } catch (IOException ex) {
-                logException(LOG, WARNING, ex);
+                logException(LOG, ex);
             }
         });
     }
@@ -473,7 +467,7 @@ abstract class TorPlugin implements Service, EventHandler {
                     circumventionProvider.isTorProbablyBlocked(country);
             boolean bridgesWork = circumventionProvider.doBridgesWork(country);
 
-            if (LOG.isLoggable(INFO)) {
+            if (LOG.isInfoEnabled()) {
                 LOG.info("Online: " + online + ", wifi: " + wifi
                         + ", IPv6 only: " + ipv6Only);
                 if (country.isEmpty()) LOG.info("Country code unknown");
@@ -512,7 +506,7 @@ abstract class TorPlugin implements Service, EventHandler {
                 }
                 enableNetwork(enableNetwork);
             } catch (IOException e) {
-                logException(LOG, WARNING, e);
+                logException(LOG, e);
             }
         });
     }
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/util/IoUtils.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/util/IoUtils.java
index 116363f427b252ecee8ba8796460627f9c92db5f..5c9befdefb6dda6dbdfd46dfeea83e547e1a0cf8 100644
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/util/IoUtils.java
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/util/IoUtils.java
@@ -1,7 +1,10 @@
 package org.briarproject.mailbox.core.util;
 
 import static org.briarproject.mailbox.core.util.LogUtils.logException;
-import static java.util.logging.Level.WARNING;
+import static org.briarproject.mailbox.core.util.LogUtils.warn;
+import static org.slf4j.LoggerFactory.getLogger;
+
+import org.slf4j.Logger;
 
 import java.io.Closeable;
 import java.io.EOFException;
@@ -11,113 +14,104 @@ import java.io.InputStream;
 import java.io.OutputStream;
 import java.net.ServerSocket;
 import java.net.Socket;
-import java.util.logging.Level;
-import java.util.logging.Logger;
 
 import javax.annotation.Nullable;
 
 public class IoUtils {
 
-	private static final Logger LOG = Logger.getLogger(IoUtils.class.getName());
-
-	public static void deleteFileOrDir(File f) {
-		if (f.isFile()) {
-			delete(f);
-		} else if (f.isDirectory()) {
-			File[] children = f.listFiles();
-			if (children == null) {
-				if (LOG.isLoggable(WARNING)) {
-					LOG.warning("Could not list files in "
-							+ f.getAbsolutePath());
-				}
-			} else {
-				for (File child : children) deleteFileOrDir(child);
-			}
-			delete(f);
-		}
-	}
-
-	private static void delete(File f) {
-		if (!f.delete() && LOG.isLoggable(WARNING))
-			LOG.warning("Could not delete " + f.getAbsolutePath());
-	}
-
-	public static void copyAndClose(InputStream in, OutputStream out) {
-		byte[] buf = new byte[4096];
-		try {
-			while (true) {
-				int read = in.read(buf);
-				if (read == -1) break;
-				out.write(buf, 0, read);
-			}
-			in.close();
-			out.flush();
-			out.close();
-		} catch (IOException e) {
-			tryToClose(in, LOG, WARNING);
-			tryToClose(out, LOG, WARNING);
-		}
-	}
-
-	public static void tryToClose(@Nullable Closeable c, Logger logger,
-			Level level) {
-		try {
-			if (c != null) c.close();
-		} catch (IOException e) {
-			logException(logger, level, e);
-		}
-	}
-
-	public static void tryToClose(@Nullable Socket s, Logger logger,
-			Level level) {
-		try {
-			if (s != null) s.close();
-		} catch (IOException e) {
-			logException(logger, level, e);
-		}
-	}
-
-	public static void tryToClose(@Nullable ServerSocket ss, Logger logger,
-			Level level) {
-		try {
-			if (ss != null) ss.close();
-		} catch (IOException e) {
-			logException(logger, level, e);
-		}
-	}
-
-	public static void read(InputStream in, byte[] b) throws IOException {
-		int offset = 0;
-		while (offset < b.length) {
-			int read = in.read(b, offset, b.length - offset);
-			if (read == -1) throw new EOFException();
-			offset += read;
-		}
-	}
-
-	// Workaround for a bug in Android 7, see
-	// https://android-review.googlesource.com/#/c/271775/
-	public static InputStream getInputStream(Socket s) throws IOException {
-		try {
-			return s.getInputStream();
-		} catch (NullPointerException e) {
-			throw new IOException(e);
-		}
-	}
-
-	// Workaround for a bug in Android 7, see
-	// https://android-review.googlesource.com/#/c/271775/
-	public static OutputStream getOutputStream(Socket s) throws IOException {
-		try {
-			return s.getOutputStream();
-		} catch (NullPointerException e) {
-			throw new IOException(e);
-		}
-	}
-
-	public static boolean isNonEmptyDirectory(File f) {
-		if (!f.isDirectory()) return false;
-		File[] children = f.listFiles();
-		return children != null && children.length > 0;
-	}
+    private static final Logger LOG = getLogger(IoUtils.class);
+
+    public static void deleteFileOrDir(File f) {
+        if (f.isFile()) {
+            delete(f);
+        } else if (f.isDirectory()) {
+            File[] children = f.listFiles();
+            if (children == null) {
+                warn(LOG, () -> "Could not list files in " + f.getAbsolutePath());
+            } else {
+                for (File child : children) deleteFileOrDir(child);
+            }
+            delete(f);
+        }
+    }
+
+    private static void delete(File f) {
+        if (!f.delete()) warn(LOG, () -> "Could not delete " + f.getAbsolutePath());
+    }
+
+    public static void copyAndClose(InputStream in, OutputStream out) {
+        byte[] buf = new byte[4096];
+        try {
+            while (true) {
+                int read = in.read(buf);
+                if (read == -1) break;
+                out.write(buf, 0, read);
+            }
+            in.close();
+            out.flush();
+            out.close();
+        } catch (IOException e) {
+            tryToClose(in, LOG);
+            tryToClose(out, LOG);
+        }
+    }
+
+    public static void tryToClose(@Nullable Closeable c, Logger logger) {
+        try {
+            if (c != null) c.close();
+        } catch (IOException e) {
+            logException(logger, e);
+        }
+    }
+
+    public static void tryToClose(@Nullable Socket s, Logger logger) {
+        try {
+            if (s != null) s.close();
+        } catch (IOException e) {
+            logException(logger, e);
+        }
+    }
+
+    public static void tryToClose(@Nullable ServerSocket ss, Logger logger) {
+        try {
+            if (ss != null) ss.close();
+        } catch (IOException e) {
+            logException(logger, e);
+        }
+    }
+
+    public static void read(InputStream in, byte[] b) throws IOException {
+        int offset = 0;
+        while (offset < b.length) {
+            int read = in.read(b, offset, b.length - offset);
+            if (read == -1) throw new EOFException();
+            offset += read;
+        }
+    }
+
+    // Workaround for a bug in Android 7, see
+    // https://android-review.googlesource.com/#/c/271775/
+    public static InputStream getInputStream(Socket s) throws IOException {
+        try {
+            return s.getInputStream();
+        } catch (NullPointerException e) {
+            throw new IOException(e);
+        }
+    }
+
+    // Workaround for a bug in Android 7, see
+    // https://android-review.googlesource.com/#/c/271775/
+    public static OutputStream getOutputStream(Socket s) throws IOException {
+        try {
+            return s.getOutputStream();
+        } catch (NullPointerException e) {
+            throw new IOException(e);
+        }
+    }
+
+    public static boolean isNonEmptyDirectory(File f) {
+        if (!f.isDirectory()) return false;
+        File[] children = f.listFiles();
+        return children != null && children.length > 0;
+    }
 }
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/util/LogUtils.java b/mailbox-core/src/main/java/org/briarproject/mailbox/core/util/LogUtils.java
deleted file mode 100644
index 3fb3434b8337c028aef87b6b7bc8c69b4ea02e82..0000000000000000000000000000000000000000
--- a/mailbox-core/src/main/java/org/briarproject/mailbox/core/util/LogUtils.java
+++ /dev/null
@@ -1,60 +0,0 @@
-package org.briarproject.mailbox.core.util;
-
-import static java.util.logging.Level.FINE;
-
-import java.io.File;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-public class LogUtils {
-
-    private static final int NANOS_PER_MILLI = 1000 * 1000;
-
-    /**
-     * Returns the elapsed time in milliseconds since some arbitrary
-     * starting time. This is only useful for measuring elapsed time.
-     */
-    public static long now() {
-        return System.nanoTime() / NANOS_PER_MILLI;
-    }
-
-    /**
-     * Logs the duration of a task.
-     *
-     * @param logger the logger to use
-     * @param task   a description of the task
-     * @param start  the start time of the task, as returned by {@link #now()}
-     */
-    public static void logDuration(Logger logger, String task, long start) {
-        if (logger.isLoggable(FINE)) {
-            long duration = now() - start;
-            logger.fine(task + " took " + duration + " ms");
-        }
-    }
-
-    public static void logException(Logger logger, Level level, Throwable t) {
-        if (logger.isLoggable(level)) logger.log(level, t.toString(), t);
-    }
-
-    public static void logFileOrDir(Logger logger, Level level, File f) {
-        if (logger.isLoggable(level)) {
-            if (f.isFile()) {
-                logWithType(logger, level, f, "F");
-            } else if (f.isDirectory()) {
-                logWithType(logger, level, f, "D");
-                File[] children = f.listFiles();
-                if (children != null) {
-                    for (File child : children)
-                        logFileOrDir(logger, level, child);
-                }
-            } else if (f.exists()) {
-                logWithType(logger, level, f, "?");
-            }
-        }
-    }
-
-    private static void logWithType(Logger logger, Level level, File f,
-                                    String type) {
-        logger.log(level, type + " " + f.getAbsolutePath() + " " + f.length());
-    }
-}
diff --git a/mailbox-core/src/main/java/org/briarproject/mailbox/core/util/LogUtils.kt b/mailbox-core/src/main/java/org/briarproject/mailbox/core/util/LogUtils.kt
new file mode 100644
index 0000000000000000000000000000000000000000..398094bb1282c2d214ac5a215df7a695e9bb981d
--- /dev/null
+++ b/mailbox-core/src/main/java/org/briarproject/mailbox/core/util/LogUtils.kt
@@ -0,0 +1,62 @@
+package org.briarproject.mailbox.core.util
+
+import org.slf4j.Logger
+
+object LogUtils {
+
+    private const val NANOS_PER_MILLI = 1000 * 1000
+
+    @JvmStatic
+    fun Logger.trace(msg: () -> String) {
+        if (isTraceEnabled) trace(msg())
+    }
+
+    @JvmStatic
+    fun Logger.debug(msg: () -> String) {
+        if (isDebugEnabled) debug(msg())
+    }
+
+    @JvmStatic
+    fun Logger.info(msg: () -> String) {
+        if (isInfoEnabled) info(msg())
+    }
+
+    @JvmStatic
+    fun Logger.warn(msg: () -> String) {
+        if (isWarnEnabled) warn(msg())
+    }
+
+    @JvmStatic
+    fun Logger.error(msg: () -> String) {
+        if (isErrorEnabled) error(msg())
+    }
+
+    /**
+     * Returns the elapsed time in milliseconds since some arbitrary
+     * starting time. This is only useful for measuring elapsed time.
+     */
+    @JvmStatic
+    fun now(): Long {
+        return System.nanoTime() / NANOS_PER_MILLI
+    }
+
+    /**
+     * Logs the duration of a task.
+     *
+     * @param logger the logger to use
+     * @param task   a description of the task
+     * @param start  the start time of the task, as returned by [.now]
+     */
+    @JvmStatic
+    fun logDuration(logger: Logger, msg: () -> String, start: Long) {
+        logger.trace {
+            val duration = now() - start
+            "${msg()} took $duration ms"
+        }
+    }
+
+    @JvmStatic
+    fun logException(logger: Logger, t: Throwable) {
+        if (logger.isWarnEnabled) logger.warn(t.toString(), t)
+    }
+}