diff --git a/.gitignore b/.gitignore
index 24ce8a56d0ce51aef8ad57676f5696a1ee93fc98..77ff3f55133d90f05da9c19e195a91cb034d9ad2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,4 @@
 Cargo.lock
 **/*.rs.bk
 src/discord_token
+ucc-bot.log
diff --git a/Cargo.toml b/Cargo.toml
index f1c6474bd731c2b1e33e7dd1ea2810218a03be87..4c930736b6124340524cbe8fb1340f93914d844e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -8,3 +8,5 @@ edition = "2018"
 serenity = "0.6.3"
 rand = "0.7.0"
 lazy_static = "1.3.0"
+log = "0.4.8"
+simplelog = "0.6.0"
diff --git a/src/main.rs b/src/main.rs
index e5d7a1c1187b20f10c71727f9b0041d641bccac1..08263f879495c35e3ec1b8037bb7af93bb91db6d 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,6 +1,12 @@
 #[macro_use]
 extern crate lazy_static;
 
+#[macro_use]
+extern crate log;
+extern crate simplelog;
+use simplelog::*;
+use std::fs::File;
+
 use serenity::{
     model::{channel, channel::Message, gateway::Ready, guild::Member},
     prelude::*,
@@ -15,7 +21,7 @@ macro_rules! e {
     ($error: literal, $x:expr) => {
         match $x {
             Ok(_) => (),
-            Err(why) => eprintln!($error, why),
+            Err(why) => error!($error, why),
         }
     };
 }
@@ -102,11 +108,20 @@ impl EventHandler for Handler {
     //
     // In this case, just print what the current user's username is.
     fn ready(&self, _: Context, ready: Ready) {
-        println!("{} is connected!", ready.user.name);
+        info!("{} is connected!", ready.user.name);
     }
 }
 
 fn main() {
+    CombinedLogger::init(vec![
+        TermLogger::new(LevelFilter::Info, Config::default(), TerminalMode::Mixed).unwrap(),
+        WriteLogger::new(
+            LevelFilter::Info,
+            Config::default(),
+            File::create("ucc-bot.log").unwrap(),
+        ),
+    ])
+    .unwrap();
     // Configure the client with your Discord bot token in the environment.
     let token = config::DISCORD_TOKEN;
 
@@ -120,6 +135,6 @@ fn main() {
     // Shards will automatically attempt to reconnect, and will perform
     // exponential backoff until it reconnects.
     if let Err(why) = client.start() {
-        println!("Client error: {:?}", why);
+        error!("Client error: {:?}", why);
     }
 }
diff --git a/src/user_management.rs b/src/user_management.rs
index be715e3e7f4d55c694f140a8e8b725dc3f361473..286c244afa511a20e564f367b3465aa34e284d7e 100644
--- a/src/user_management.rs
+++ b/src/user_management.rs
@@ -11,7 +11,7 @@ macro_rules! e {
     ($error: literal, $x:expr) => {
         match $x {
             Ok(_) => (),
-            Err(why) => eprintln!($error, why),
+            Err(why) => error!($error, why),
         }
     };
 }
@@ -24,18 +24,18 @@ pub fn new_member(ctx: &Context, mut new_member: Member) {
     message.push_line("If you're not sure where to start, perhaps you could tell us about your projects, your first computer…");
     message.push_line("You should also know that we follow the Freenode Channel Guidelines: https://freenode.net/changuide, and try to avoid defamatory content");
     if let Err(why) = config::WELCOME_CHANNEL.say(&ctx, message.build()) {
-        println!("Error sending message: {:?}", why);
+        error!("Error sending message: {:?}", why);
     }
 
     let mut message = MessageBuilder::new();
     message.push(format!("Say hi to {} in ", new_member.display_name()));
     message.mention(&config::WELCOME_CHANNEL);
     if let Err(why) = config::MAIN_CHANNEL.say(&ctx, message.build()) {
-        println!("Error sending message: {:?}", why);
+        error!("Error sending message: {:?}", why);
     }
 
     if let Err(why) = new_member.add_role(&ctx.http, config::UNREGISTERED_MEMBER_ROLE) {
-        println!("Error adding user role: {:?}", why);
+        error!("Error adding user role: {:?}", why);
     };
 }
 
diff --git a/src/voting.rs b/src/voting.rs
index 3a6363e9d1d82512d3d6fc994079d25e0f41bc80..f3b9b10b2d79b8d28579e016345bf832e8ff52d2 100644
--- a/src/voting.rs
+++ b/src/voting.rs
@@ -12,7 +12,7 @@ macro_rules! e {
     ($error: literal, $x:expr) => {
         match $x {
             Ok(_) => (),
-            Err(why) => eprintln!($error, why),
+            Err(why) => error!($error, why),
         }
     };
 }
@@ -78,9 +78,9 @@ impl Commands {
 }
 
 fn create_motion(ctx: &Context, msg: &Message, topic: &str) {
-    println!("{} created a motion {}", msg.author.name, topic);
+    info!("{} created a motion {}", msg.author.name, topic);
     if let Err(why) = msg.delete(ctx) {
-        println!("Error deleting motion prompt: {:?}", why);
+        error!("Error deleting motion prompt: {:?}", why);
     }
     match msg.channel_id.send_message(&ctx.http, |m| {
         m.embed(|embed| {
@@ -115,14 +115,14 @@ fn create_motion(ctx: &Context, msg: &Message, topic: &str) {
         m
     }) {
         Err(why) => {
-            println!("Error creating motion: {:?}", why);
+            error!("Error creating motion: {:?}", why);
         }
         Ok(_) => {}
     }
 }
 
 fn create_poll(ctx: &Context, msg: &Message, topic: &str) {
-    println!("{} created a poll {}", msg.author.name, topic);
+    info!("{} created a poll {}", msg.author.name, topic);
     match msg.channel_id.send_message(&ctx.http, |m| {
         m.embed(|embed| {
             embed.author(|a| {
@@ -151,11 +151,11 @@ fn create_poll(ctx: &Context, msg: &Message, topic: &str) {
         m
     }) {
         Err(why) => {
-            println!("Error sending message: {:?}", why);
+            error!("Error sending message: {:?}", why);
         }
         Ok(_) => {
             if let Err(why) = msg.delete(ctx) {
-                println!("Error deleting motion prompt: {:?}", why);
+                error!("Error deleting motion prompt: {:?}", why);
             }
         }
     }
@@ -174,7 +174,7 @@ lazy_static! {
 fn get_cached_motion(ctx: &Context, msg: &Message) -> MotionInfo {
     let mut cached_motions = MOTIONS_CACHE.lock().unwrap();
     if !cached_motions.contains_key(&msg.id) {
-        println!("Initialising representation of motion {:?}", msg.id);
+        info!("Initialising representation of motion {:?}", msg.id);
         let this_motion = MotionInfo {
             votes: {
                 let mut m = HashMap::new();
@@ -204,7 +204,7 @@ fn set_cached_motion(id: &serenity::model::id::MessageId, motion_info: MotionInf
     if let Some(motion) = MOTIONS_CACHE.lock().unwrap().get_mut(id) {
         *motion = motion_info;
     } else {
-        println!("{}", "Couldn't find motion in cache to set");
+        warn!("{}", "Couldn't find motion in cache to set");
     }
 }
 
@@ -250,7 +250,7 @@ fn update_motion(
     let old_embed = msg.embeds[0].clone();
     let topic = old_embed.clone().title.unwrap();
 
-    println!(
+    info!(
         "  {:10} {:6} {} on {}",
         user.name,
         change,
@@ -271,7 +271,7 @@ fn update_motion(
                 format!("{}\n_was_ {}", status, last_status_full),
                 true,
             );
-            println!("Motion to {} now {}", topic, status);
+            info!("Motion to {} now {}", topic, status);
             //
             let mut message = MessageBuilder::new();
             message.push_bold(topic);
@@ -279,7 +279,7 @@ fn update_motion(
             message.push_bold(status);
             message.push_italic(format!(" (was {})", last_status));
             if let Err(why) = config::ANNOUNCEMENT_CHANNEL.say(&ctx.http, message.build()) {
-                println!("Error sending message: {:?}", why);
+                error!("Error sending message: {:?}", why);
             };
         }
     };
@@ -336,7 +336,7 @@ fn update_motion(
             e
         })
     }) {
-        println!("Error updating motion: {:?}", why);
+        error!("Error updating motion: {:?}", why);
     }
 }
 
@@ -357,7 +357,7 @@ pub fn reaction_add(ctx: Context, add_reaction: channel::Reaction) {
                                 {
                                     if a_user.id.0 == user.id.0 {
                                         if let Err(why) = add_reaction.delete(&ctx) {
-                                            println!("Error deleting react: {:?}", why);
+                                            error!("Error deleting react: {:?}", why);
                                         };
                                         return;
                                     }
@@ -367,7 +367,7 @@ pub fn reaction_add(ctx: Context, add_reaction: channel::Reaction) {
                                 .contains(&add_reaction.emoji.as_data().as_str())
                             {
                                 if let Err(why) = add_reaction.delete(&ctx) {
-                                    println!("Error deleting react: {:?}", why);
+                                    error!("Error deleting react: {:?}", why);
                                 };
                                 return;
                             }
@@ -390,21 +390,21 @@ pub fn reaction_add(ctx: Context, add_reaction: channel::Reaction) {
                                     .contains(&add_reaction.emoji.as_data().as_str())
                                 {
                                     if let Err(why) = add_reaction.delete(&ctx) {
-                                        println!("Error deleting react: {:?}", why);
+                                        error!("Error deleting react: {:?}", why);
                                     };
                                     return;
                                 }
                             }
                         }
                         Err(why) => {
-                            println!("Error getting user role: {:?}", why);
+                            error!("Error getting user role: {:?}", why);
                         }
                     }
                 }
             }
         }
         Err(why) => {
-            println!("Error processing react: {:?}", why);
+            error!("Error processing react: {:?}", why);
         }
     }
 }
@@ -427,7 +427,7 @@ pub fn reaction_remove(ctx: Context, removed_reaction: channel::Reaction) {
             }
         }
         Err(why) => {
-            println!("Error getting user role: {:?}", why);
+            error!("Error getting user role: {:?}", why);
         }
     }
 }