git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

commit89a809bff131c5e5e03ee4a341273ed429092c1d
parent72dc2f8488
authorIsaac Freund <[email protected]>
date2020-06-17 01:25
code: improve logging functions

this implements a modified version of the logging interface proposed
here: https://github.com/ziglang/zig/pull/5348

 river/Config.zig            |   1 -
 river/Control.zig           |   1 -
 river/Cursor.zig            |   8 +--
 river/InputManager.zig      |   6 +-
 river/Keyboard.zig          |   6 +-
 river/LayerSurface.zig      |  10 +--
 river/Output.zig            |  30 ++++-----
 river/OutputStatus.zig      |   4 +-
 river/Root.zig              |  18 +++---
 river/SeatStatus.zig        |   1 -
 river/Server.zig            |  28 +++++----
 river/StatusManager.zig     |  12 ++--
 river/View.zig              |  10 +--
 river/XdgToplevel.zig       |   4 +-
 river/XwaylandUnmanaged.zig |   1 -
 river/XwaylandView.zig      |   1 -
 river/log.zig               | 149 ++++++++++++++++++++++++++++++++++++++------
 river/main.zig              |   9 ++-
 18 files changed, 207 insertions(+), 92 deletions(-)

diff --git a/river/Config.zig b/river/Config.zig
index 9791019..4d0fd9e 100644
--- a/river/Config.zig
+++ b/river/Config.zig
@@ -22,7 +22,6 @@ const std = @import("std");
 const c = @import("c.zig");
 const util = @import("util.zig");
 
-const Log = @import("log.zig").Log;
 const Server = @import("Server.zig");
 const Mapping = @import("Mapping.zig");
 
diff --git a/river/Control.zig b/river/Control.zig
index 5a8657a..5e56e83 100644
--- a/river/Control.zig
+++ b/river/Control.zig
@@ -23,7 +23,6 @@ const c = @import("c.zig");
 const command = @import("command.zig");
 const util = @import("util.zig");
 
-const Log = @import("log.zig").Log;
 const Seat = @import("Seat.zig");
 const Server = @import("Server.zig");
 
diff --git a/river/Cursor.zig b/river/Cursor.zig
index b716da7..18bb9bf 100644
--- a/river/Cursor.zig
+++ b/river/Cursor.zig
@@ -21,10 +21,10 @@ const build_options = @import("build_options");
 const std = @import("std");
 
 const c = @import("c.zig");
+const log = @import("log.zig");
 const util = @import("util.zig");
 
 const LayerSurface = @import("LayerSurface.zig");
-const Log = @import("log.zig").Log;
 const Output = @import("Output.zig");
 const Seat = @import("Seat.zig");
 const View = @import("View.zig");
@@ -89,7 +89,7 @@ pub fn init(self: *Self, seat: *Seat) !void {
             }
         }
     } else {
-        Log.Error.log("Failed to load an xcursor theme", .{});
+        log.err(.cursor, "failed to load an xcursor theme", .{});
     }
 
     self.mode = CursorMode.Passthrough;
@@ -235,7 +235,7 @@ fn handleRequestSetCursor(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C
         // provided surface as the cursor image. It will set the hardware cursor
         // on the output that it's currently on and continue to do so as the
         // cursor moves between outputs.
-        Log.Debug.log("Focused client set cursor", .{});
+        log.debug(.cursor, "focused client set cursor", .{});
         c.wlr_cursor_set_surface(
             self.wlr_cursor,
             event.surface,
@@ -259,7 +259,7 @@ fn processMotion(self: Self, time: u32) void {
             const wlr_seat = self.seat.wlr_seat;
             const focus_change = wlr_seat.pointer_state.focused_surface != wlr_surface;
             if (focus_change) {
-                Log.Debug.log("Pointer notify enter at ({},{})", .{ sx, sy });
+                log.debug(.cursor, "pointer notify enter at ({},{})", .{ sx, sy });
                 c.wlr_seat_pointer_notify_enter(wlr_seat, wlr_surface, sx, sy);
             } else {
                 // The enter event contains coordinates, so we only need to notify
diff --git a/river/InputManager.zig b/river/InputManager.zig
index 7c1337c..e9264a0 100644
--- a/river/InputManager.zig
+++ b/river/InputManager.zig
@@ -20,9 +20,9 @@ const Self = @This();
 const std = @import("std");
 
 const c = @import("c.zig");
+const log = @import("log.zig");
 const util = @import("util.zig");
 
-const Log = @import("log.zig").Log;
 const Seat = @import("Seat.zig");
 const Server = @import("Server.zig");
 
@@ -102,7 +102,7 @@ pub fn inputAllowed(self: Self, wlr_surface: *c.wlr_surface) bool {
 fn handleInhibitActivate(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void {
     const self = @fieldParentPtr(Self, "listen_inhibit_activate", listener.?);
 
-    Log.Debug.log("Input inhibitor activated", .{});
+    log.debug(.input_manager, "input inhibitor activated", .{});
 
     // Clear focus of all seats
     var seat_it = self.seats.first;
@@ -116,7 +116,7 @@ fn handleInhibitActivate(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C)
 fn handleInhibitDeactivate(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void {
     const self = @fieldParentPtr(Self, "listen_inhibit_deactivate", listener.?);
 
-    Log.Debug.log("Input inhibitor deactivated", .{});
+    log.debug(.input_manager, "input inhibitor deactivated", .{});
 
     self.exclusive_client = null;
 
diff --git a/river/Keyboard.zig b/river/Keyboard.zig
index 8f57370..12b043f 100644
--- a/river/Keyboard.zig
+++ b/river/Keyboard.zig
@@ -20,9 +20,9 @@ const Self = @This();
 const std = @import("std");
 
 const c = @import("c.zig");
+const log = @import("log.zig");
 const util = @import("util.zig");
 
-const Log = @import("log.zig").Log;
 const Seat = @import("Seat.zig");
 
 seat: *Seat,
@@ -163,12 +163,12 @@ fn handleModifiers(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void
 /// Returns true if the keysym was handled.
 fn handleBuiltinMapping(self: Self, keysym: c.xkb_keysym_t) bool {
     if (keysym >= c.XKB_KEY_XF86Switch_VT_1 and keysym <= c.XKB_KEY_XF86Switch_VT_12) {
-        Log.Debug.log("Switch VT keysym received", .{});
+        log.debug(.keyboard, "switch VT keysym received", .{});
         const wlr_backend = self.seat.input_manager.server.wlr_backend;
         if (c.river_wlr_backend_is_multi(wlr_backend)) {
             if (c.river_wlr_backend_get_session(wlr_backend)) |session| {
                 const vt = keysym - c.XKB_KEY_XF86Switch_VT_1 + 1;
-                Log.Debug.log("Switching to VT {}", .{vt});
+                log.notice(.server, "switching to VT {}", .{vt});
                 _ = c.wlr_session_change_vt(session, vt);
             }
         }
diff --git a/river/LayerSurface.zig b/river/LayerSurface.zig
index 6fa6822..e74b24b 100644
--- a/river/LayerSurface.zig
+++ b/river/LayerSurface.zig
@@ -20,10 +20,10 @@ const Self = @This();
 const std = @import("std");
 
 const c = @import("c.zig");
+const log = @import("log.zig");
 const util = @import("util.zig");
 
 const Box = @import("Box.zig");
-const Log = @import("log.zig").Log;
 const Output = @import("Output.zig");
 const XdgPopup = @import("XdgPopup.zig");
 
@@ -79,7 +79,7 @@ fn handleDestroy(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void {
     const self = @fieldParentPtr(Self, "listen_destroy", listener.?);
     const output = self.output;
 
-    Log.Debug.log("Layer surface '{}' destroyed", .{self.wlr_layer_surface.namespace});
+    log.debug(.layer_shell, "layer surface '{}' destroyed", .{self.wlr_layer_surface.namespace});
 
     // Remove listeners active the entire lifetime of the layer surface
     c.wl_list_remove(&self.listen_destroy.link);
@@ -94,7 +94,7 @@ fn handleMap(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void {
     const self = @fieldParentPtr(Self, "listen_map", listener.?);
     const wlr_layer_surface = self.wlr_layer_surface;
 
-    Log.Debug.log("Layer surface '{}' mapped.", .{wlr_layer_surface.namespace});
+    log.debug(.layer_shell, "layer surface '{}' mapped", .{wlr_layer_surface.namespace});
 
     // Add listeners that are only active while mapped
     self.listen_commit.notify = handleCommit;
@@ -115,7 +115,7 @@ fn handleMap(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void {
 fn handleUnmap(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void {
     const self = @fieldParentPtr(Self, "listen_unmap", listener.?);
 
-    Log.Debug.log("Layer surface '{}' unmapped.", .{self.wlr_layer_surface.namespace});
+    log.debug(.layer_shell, "layer surface '{}' unmapped", .{self.wlr_layer_surface.namespace});
 
     // This is a bit ugly: we need to use the wlr bool here since surfaces
     // may be closed during the inital configure which we preform
@@ -164,7 +164,7 @@ fn handleCommit(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void {
     const wlr_layer_surface = self.wlr_layer_surface;
 
     if (self.wlr_layer_surface.output == null) {
-        Log.Error.log("Layer surface committed with null output", .{});
+        log.err(.layer_shell, "layer surface committed with null output", .{});
         return;
     }
 
diff --git a/river/Output.zig b/river/Output.zig
index 67c70e7..290530d 100644
--- a/river/Output.zig
+++ b/river/Output.zig
@@ -21,12 +21,12 @@ const Self = @This();
 const std = @import("std");
 
 const c = @import("c.zig");
+const log = @import("log.zig");
 const render = @import("render.zig");
 const util = @import("util.zig");
 
 const Box = @import("Box.zig");
 const LayerSurface = @import("LayerSurface.zig");
-const Log = @import("log.zig").Log;
 const Root = @import("Root.zig");
 const View = @import("View.zig");
 const ViewStack = @import("view_stack.zig").ViewStack;
@@ -288,11 +288,11 @@ fn layoutExternal(self: *Self, visible_count: u32, output_tags: u32) !void {
         box.height -= delta_size;
         if (box.width < minimum_size) {
             box.width = minimum_size;
-            Log.Info.log("Window configuration hits minimum view width.", .{});
+            log.notice(.layout, "window hits minimum view width.", .{});
         }
         if (box.height < minimum_size) {
             box.height = minimum_size;
-            Log.Info.log("Window configuration hits minimum view height.", .{});
+            log.notice(.layout, "window hits minimum view height.", .{});
         }
         try view_boxen.append(box);
     }
@@ -338,12 +338,12 @@ pub fn arrangeViews(self: *Self) void {
 
     layoutExternal(self, visible_count, output_tags) catch |err| {
         switch (err) {
-            LayoutError.BadExitCode => Log.Error.log("Layout command exited with non-zero return code.", .{}),
-            LayoutError.BadWindowConfiguration => Log.Error.log("Invalid window configuration.", .{}),
-            LayoutError.ConfigurationMismatch => Log.Error.log("Mismatch between amount of window configurations and visible windows.", .{}),
-            else => Log.Error.log("Encountered unexpected error while trying to use external layout.", .{}),
+            LayoutError.BadExitCode => log.err(.layout, "layout command exited with non-zero return code", .{}),
+            LayoutError.BadWindowConfiguration => log.err(.layout, "invalid window configuration", .{}),
+            LayoutError.ConfigurationMismatch => log.err(.layout, "mismatch between window configuration and visible window counts", .{}),
+            else => log.err(.layout, "'{}' error while trying to use external layout", .{err}),
         }
-        Log.Error.log("Falling back to internal layout", .{});
+        log.err(.layout, "falling back to internal layout", .{});
         layoutFull(self, visible_count, output_tags);
     };
 }
@@ -461,8 +461,9 @@ fn arrangeLayer(
                 new_box.width = bounds.width -
                     (current_state.margin.left + current_state.margin.right);
             } else {
-                Log.Error.log(
-                    "Protocol Error: layer surface '{}' requested width 0 without anchoring to opposite edges.",
+                log.err(
+                    .layer_shell,
+                    "protocol error: layer surface '{}' requested width 0 without anchoring to opposite edges",
                     .{layer_surface.wlr_layer_surface.namespace},
                 );
                 c.wlr_layer_surface_v1_close(layer_surface.wlr_layer_surface);
@@ -490,8 +491,9 @@ fn arrangeLayer(
                 new_box.height = bounds.height -
                     (current_state.margin.top + current_state.margin.bottom);
             } else {
-                Log.Error.log(
-                    "Protocol Error: layer surface '{}' requested height 0 without anchoring to opposite edges.",
+                log.err(
+                    .layer_shell,
+                    "protocol error: layer surface '{}' requested height 0 without anchoring to opposite edges",
                     .{layer_surface.wlr_layer_surface.namespace},
                 );
                 c.wlr_layer_surface_v1_close(layer_surface.wlr_layer_surface);
@@ -559,7 +561,7 @@ fn arrangeLayer(
         }
 
         // Tell the client to assume the new size
-        Log.Debug.log("send configure, {} x {}", .{ layer_surface.box.width, layer_surface.box.height });
+        log.debug(.layer_shell, "send configure, {} x {}", .{ layer_surface.box.width, layer_surface.box.height });
         c.wlr_layer_surface_v1_configure(
             layer_surface.wlr_layer_surface,
             layer_surface.box.width,
@@ -574,7 +576,7 @@ fn handleDestroy(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void {
     const self = @fieldParentPtr(Self, "listen_destroy", listener.?);
     const root = self.root;
 
-    Log.Debug.log("Output {} destroyed", .{self.wlr_output.name});
+    log.debug(.server, "output '{}' destroyed", .{self.wlr_output.name});
 
     // Use the first output in the list that is not the one being destroyed.
     // If there is no other real output, use the noop output.
diff --git a/river/OutputStatus.zig b/river/OutputStatus.zig
index 6b09dd1..129570a 100644
--- a/river/OutputStatus.zig
+++ b/river/OutputStatus.zig
@@ -20,9 +20,9 @@ const Self = @This();
 const std = @import("std");
 
 const c = @import("c.zig");
+const log = @import("log.zig");
 const util = @import("util.zig");
 
-const Log = @import("log.zig").Log;
 const Output = @import("Output.zig");
 const View = @import("View.zig");
 const ViewStack = @import("view_stack.zig").ViewStack;
@@ -64,7 +64,7 @@ pub fn sendViewTags(self: Self) void {
     while (it.next()) |node|
         view_tags.append(node.view.current_tags) catch {
         c.wl_resource_post_no_memory(self.wl_resource);
-        Log.Error.log("out of memory", .{});
+        log.crit(.river_status, "out of memory", .{});
         return;
     };
 
diff --git a/river/Root.zig b/river/Root.zig
index 70a1426..dc1a92a 100644
--- a/river/Root.zig
+++ b/river/Root.zig
@@ -21,9 +21,9 @@ const std = @import("std");
 const build_options = @import("build_options");
 
 const c = @import("c.zig");
+const log = @import("log.zig");
 const util = @import("util.zig");
 
-const Log = @import("log.zig").Log;
 const Output = @import("Output.zig");
 const Server = @import("Server.zig");
 const View = @import("View.zig");
@@ -156,21 +156,22 @@ fn startTransaction(self: *Self) void {
     }
 
     if (self.pending_configures > 0) {
-        Log.Debug.log(
-            "Started transaction with {} pending configures.",
+        log.debug(
+            .transaction,
+            "started transaction with {} pending configure(s)",
             .{self.pending_configures},
         );
 
         // Set timeout to 200ms
         if (c.wl_event_source_timer_update(self.transaction_timer, 200) < 0) {
-            Log.Error.log("failed to update timer.", .{});
+            log.err(.transaction, "failed to update timer", .{});
             self.commitTransaction();
         }
     } else {
         // No views need configures, clear the current timer in case we are
         // interrupting another transaction and commit.
         if (c.wl_event_source_timer_update(self.transaction_timer, 0) < 0)
-            Log.Error.log("error disarming timer", .{});
+            log.err(.transaction, "error disarming timer", .{});
         self.commitTransaction();
     }
 }
@@ -178,7 +179,7 @@ fn startTransaction(self: *Self) void {
 fn handleTimeout(data: ?*c_void) callconv(.C) c_int {
     const self = util.voidCast(Self, data.?);
 
-    Log.Error.log("Transaction timed out. Some imperfect frames may be shown.", .{});
+    log.err(.transaction, "time out occurred, some imperfect frames may be shown", .{});
 
     self.commitTransaction();
 
@@ -190,7 +191,7 @@ pub fn notifyConfigured(self: *Self) void {
     if (self.pending_configures == 0) {
         // Disarm the timer, as we didn't timeout
         if (c.wl_event_source_timer_update(self.transaction_timer, 0) == -1)
-            Log.Error.log("Error disarming timer", .{});
+            log.err(.transaction, "error disarming timer", .{});
         self.commitTransaction();
     }
 }
@@ -212,7 +213,8 @@ fn commitTransaction(self: *Self) void {
 
         // If there were pending focused tags, make them the current focus
         if (output.pending_focused_tags) |tags| {
-            Log.Debug.log(
+            log.debug(
+                .output,
                 "changing current focus: {b:0>10} to {b:0>10}",
                 .{ output.current_focused_tags, tags },
             );
diff --git a/river/SeatStatus.zig b/river/SeatStatus.zig
index 1f6b408..230a196 100644
--- a/river/SeatStatus.zig
+++ b/river/SeatStatus.zig
@@ -22,7 +22,6 @@ const std = @import("std");
 const c = @import("c.zig");
 const util = @import("util.zig");
 
-const Log = @import("log.zig").Log;
 const Seat = @import("Seat.zig");
 const Output = @import("Output.zig");
 const View = @import("View.zig");
diff --git a/river/Server.zig b/river/Server.zig
index 6f99d18..3d4307a 100644
--- a/river/Server.zig
+++ b/river/Server.zig
@@ -21,6 +21,7 @@ const build_options = @import("build_options");
 const std = @import("std");
 
 const c = @import("c.zig");
+const log = @import("log.zig");
 const util = @import("util.zig");
 
 const Config = @import("Config.zig");
@@ -28,7 +29,6 @@ const Control = @import("Control.zig");
 const DecorationManager = @import("DecorationManager.zig");
 const InputManager = @import("InputManager.zig");
 const LayerSurface = @import("LayerSurface.zig");
-const Log = @import("log.zig").Log;
 const Output = @import("Output.zig");
 const Root = @import("Root.zig");
 const StatusManager = @import("StatusManager.zig");
@@ -181,7 +181,7 @@ pub fn run(self: Self) void {
 fn handleNewOutput(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void {
     const self = @fieldParentPtr(Self, "listen_new_output", listener.?);
     const wlr_output = util.voidCast(c.wlr_output, data.?);
-    Log.Debug.log("New output {}", .{wlr_output.name});
+    log.debug(.server, "new output {}", .{wlr_output.name});
     self.root.addOutput(wlr_output);
 }
 
@@ -192,11 +192,11 @@ fn handleNewXdgSurface(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) v
     const wlr_xdg_surface = util.voidCast(c.wlr_xdg_surface, data.?);
 
     if (wlr_xdg_surface.role == .WLR_XDG_SURFACE_ROLE_POPUP) {
-        Log.Debug.log("New xdg_popup", .{});
+        log.debug(.server, "new xdg_popup", .{});
         return;
     }
 
-    Log.Debug.log("New xdg_toplevel", .{});
+    log.debug(.server, "new xdg_toplevel", .{});
 
     // The View will add itself to the output's view stack on map
     const output = self.input_manager.default_seat.focused_output;
@@ -209,7 +209,8 @@ fn handleNewLayerSurface(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C)
     const self = @fieldParentPtr(Self, "listen_new_layer_surface", listener.?);
     const wlr_layer_surface = util.voidCast(c.wlr_layer_surface_v1, data.?);
 
-    Log.Debug.log(
+    log.debug(
+        .server,
         "New layer surface: namespace {}, layer {}, anchor {}, size {}x{}, margin ({},{},{},{}), exclusive_zone {}",
         .{
             wlr_layer_surface.namespace,
@@ -230,14 +231,16 @@ fn handleNewLayerSurface(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C)
     if (wlr_layer_surface.output == null) {
         if (self.root.outputs.first) |node| {
             const output = &node.data;
-            Log.Debug.log(
-                "New layer surface had null output, assigning it to output {}",
+            log.debug(
+                .server,
+                "new layer surface had null output, assigning it to output '{}'",
                 .{output.wlr_output.name},
             );
             wlr_layer_surface.output = output.wlr_output;
         } else {
-            Log.Error.log(
-                "No output available for layer surface '{}'",
+            log.err(
+                .server,
+                "no output available for layer surface '{}'",
                 .{wlr_layer_surface.namespace},
             );
             c.wlr_layer_surface_v1_close(wlr_layer_surface);
@@ -256,7 +259,7 @@ fn handleNewXwaylandSurface(listener: ?*c.wl_listener, data: ?*c_void) callconv(
     const wlr_xwayland_surface = util.voidCast(c.wlr_xwayland_surface, data.?);
 
     if (wlr_xwayland_surface.override_redirect) {
-        Log.Debug.log("New unmanaged xwayland surface", .{});
+        log.debug(.server, "new unmanaged xwayland surface", .{});
         // The unmanged surface will add itself to the list of unmanaged views
         // in Root when it is mapped.
         const node = util.allocator.create(std.TailQueue(XwaylandUnmanaged).Node) catch unreachable;
@@ -264,8 +267,9 @@ fn handleNewXwaylandSurface(listener: ?*c.wl_listener, data: ?*c_void) callconv(
         return;
     }
 
-    Log.Debug.log(
-        "New xwayland surface: title '{}', class '{}'",
+    log.debug(
+        .server,
+        "new xwayland surface: title '{}', class '{}'",
         .{ wlr_xwayland_surface.title, wlr_xwayland_surface.class },
     );
 
diff --git a/river/StatusManager.zig b/river/StatusManager.zig
index 0fe3d6e..c6de81a 100644
--- a/river/StatusManager.zig
+++ b/river/StatusManager.zig
@@ -20,9 +20,9 @@ const Self = @This();
 const std = @import("std");
 
 const c = @import("c.zig");
+const log = @import("log.zig");
 const util = @import("util.zig");
 
-const Log = @import("log.zig").Log;
 const Output = @import("Output.zig");
 const OutputStatus = @import("OutputStatus.zig");
 const Seat = @import("Seat.zig");
@@ -72,7 +72,7 @@ fn bind(wl_client: ?*c.wl_client, data: ?*c_void, version: u32, id: u32) callcon
         id,
     ) orelse {
         c.wl_client_post_no_memory(wl_client);
-        Log.Error.log("out of memory\n", .{});
+        log.crit(.river_status, "out of memory", .{});
         return;
     };
     c.wl_resource_set_implementation(wl_resource, &implementation, self, null);
@@ -95,7 +95,7 @@ fn getRiverOutputStatus(
 
     const node = util.allocator.create(std.SinglyLinkedList(OutputStatus).Node) catch {
         c.wl_client_post_no_memory(wl_client);
-        Log.Error.log("out of memory\n", .{});
+        log.crit(.river_status, "out of memory", .{});
         return;
     };
 
@@ -107,7 +107,7 @@ fn getRiverOutputStatus(
     ) orelse {
         c.wl_client_post_no_memory(wl_client);
         util.allocator.destroy(node);
-        Log.Error.log("out of memory\n", .{});
+        log.crit(.river_status, "out of memory", .{});
         return;
     };
 
@@ -128,7 +128,7 @@ fn getRiverSeatStatus(
 
     const node = util.allocator.create(std.SinglyLinkedList(SeatStatus).Node) catch {
         c.wl_client_post_no_memory(wl_client);
-        Log.Error.log("out of memory\n", .{});
+        log.crit(.river_status, "out of memory", .{});
         return;
     };
 
@@ -140,7 +140,7 @@ fn getRiverSeatStatus(
     ) orelse {
         c.wl_client_post_no_memory(wl_client);
         util.allocator.destroy(node);
-        Log.Error.log("out of memory\n", .{});
+        log.crit(.river_status, "out of memory", .{});
         return;
     };
 
diff --git a/river/View.zig b/river/View.zig
index 0ff2344..12226ec 100644
--- a/river/View.zig
+++ b/river/View.zig
@@ -21,10 +21,10 @@ const build_options = @import("build_options");
 const std = @import("std");
 
 const c = @import("c.zig");
+const log = @import("log.zig");
 const util = @import("util.zig");
 
 const Box = @import("Box.zig");
-const Log = @import("log.zig").Log;
 const Output = @import("Output.zig");
 const Root = @import("Root.zig");
 const ViewStack = @import("view_stack.zig").ViewStack;
@@ -138,7 +138,7 @@ pub fn configure(self: Self) void {
             .xwayland_view => |xwayland_view| xwayland_view.configure(pending_box),
         }
     } else {
-        Log.Error.log("Configure called on a View with no pending box", .{});
+        log.err(.transaction, "configure called on a View with no pending box", .{});
     }
 }
 
@@ -155,7 +155,7 @@ pub fn dropSavedBuffers(self: *Self) void {
 
 pub fn saveBuffers(self: *Self) void {
     if (self.saved_buffers.items.len > 0) {
-        Log.Error.log("view already has buffers saved, overwriting", .{});
+        log.err(.transaction, "view already has buffers saved, overwriting", .{});
         self.saved_buffers.items.len = 0;
     }
 
@@ -270,7 +270,7 @@ pub fn getTitle(self: Self) [*:0]const u8 {
 pub fn map(self: *Self) void {
     const root = self.output.root;
 
-    Log.Debug.log("View '{}' mapped", .{self.getTitle()});
+    log.debug(.server, "view '{}' mapped", .{self.getTitle()});
 
     // Add the view to the stack of its output
     const node = @fieldParentPtr(ViewStack(Self).Node, "view", self);
@@ -294,7 +294,7 @@ pub fn map(self: *Self) void {
 pub fn unmap(self: *Self) void {
     const root = self.output.root;
 
-    Log.Debug.log("View '{}' unmapped", .{self.getTitle()});
+    log.debug(.server, "view '{}' unmapped", .{self.getTitle()});
 
     self.wlr_surface = null;
 
diff --git a/river/XdgToplevel.zig b/river/XdgToplevel.zig
index a217742..ee9b113 100644
--- a/river/XdgToplevel.zig
+++ b/river/XdgToplevel.zig
@@ -20,10 +20,10 @@ const Self = @This();
 const std = @import("std");
 
 const c = @import("c.zig");
+const log = @import("log.zig");
 const util = @import("util.zig");
 
 const Box = @import("Box.zig");
-const Log = @import("log.zig").Log;
 const View = @import("View.zig");
 const ViewStack = @import("view_stack.zig").ViewStack;
 const XdgPopup = @import("XdgPopup.zig");
@@ -235,7 +235,7 @@ fn handleCommit(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void {
     } else {
         // TODO: handle unexpected change in dimensions
         if (!std.meta.eql(view.surface_box, new_box))
-            Log.Error.log("View changed size unexpectedly", .{});
+            log.err(.xdg_shell, "view changed size unexpectedly", .{});
         view.surface_box = new_box;
     }
 }
diff --git a/river/XwaylandUnmanaged.zig b/river/XwaylandUnmanaged.zig
index f21f064..fe6f366 100644
--- a/river/XwaylandUnmanaged.zig
+++ b/river/XwaylandUnmanaged.zig
@@ -23,7 +23,6 @@ const c = @import("c.zig");
 const util = @import("util.zig");
 
 const Box = @import("Box.zig");
-const Log = @import("log.zig").Log;
 const Root = @import("Root.zig");
 
 root: *Root,
diff --git a/river/XwaylandView.zig b/river/XwaylandView.zig
index 171aba2..9195d83 100644
--- a/river/XwaylandView.zig
+++ b/river/XwaylandView.zig
@@ -22,7 +22,6 @@ const std = @import("std");
 const c = @import("c.zig");
 
 const Box = @import("Box.zig");
-const Log = @import("log.zig").Log;
 const View = @import("View.zig");
 const ViewStack = @import("view_stack.zig").ViewStack;
 const XdgPopup = @import("XdgPopup.zig");
diff --git a/river/log.zig b/river/log.zig
index 30d2564..1874630 100644
--- a/river/log.zig
+++ b/river/log.zig
@@ -17,25 +17,138 @@
 
 const std = @import("std");
 
-pub const Log = enum {
-    const Self = @This();
-
-    Silent = 0,
-    Error = 1,
-    Info = 2,
-    Debug = 3,
-
-    var verbosity = Self.Error;
+pub const Level = enum {
+    /// Emergency: a condition that cannot be handled, usually followed by a
+    /// panic.
+    emerg,
+    /// Alert: a condition that should be corrected immediately (e.g. database
+    /// corruption).
+    alert,
+    /// Critical: A bug has been detected or something has gone wrong and it
+    /// will have an effect on the operation of the program.
+    crit,
+    /// Error: A bug has been detected or something has gone wrong but it is
+    /// recoverable.
+    err,
+    /// Warning: it is uncertain if something has gone wrong or not, but the
+    /// circumstances would be worth investigating.
+    warn,
+    /// Notice: non-error but significant conditions.
+    notice,
+    /// Informational: general messages about the state of the program.
+    info,
+    /// Debug: messages only useful for debugging.
+    debug,
+};
 
-    pub fn init(_verbosity: Self) void {
-        verbosity = _verbosity;
-    }
+/// The default log level is based on build mode. Note that in ReleaseSmall
+/// builds the default level is emerg but no messages will be stored/logged
+/// to save space.
+pub var level: Level = switch (std.builtin.mode) {
+    .Debug => .debug,
+    .ReleaseSafe => .notice,
+    .ReleaseFast => .err,
+    .ReleaseSmall => .emerg,
+};
 
-    fn log(level: Self, comptime format: []const u8, args: var) void {
-        if (@enumToInt(level) <= @enumToInt(verbosity)) {
-            // TODO: log the time since start in the same format as wlroots
-            // TODO: use color if logging to a tty
-            std.debug.warn("[{}] " ++ format ++ "\n", .{@tagName(level)} ++ args);
+fn log(
+    comptime message_level: Level,
+    comptime scope: @TypeOf(.foobar),
+    comptime format: []const u8,
+    args: var,
+) void {
+    if (@enumToInt(message_level) <= @enumToInt(level)) {
+        // Don't store/log messages in release small mode to save space
+        if (std.builtin.mode != .ReleaseSmall) {
+            const stderr = std.debug.getStderrStream();
+            stderr.print(@tagName(message_level) ++ ": (" ++ @tagName(scope) ++ ") " ++
+                format ++ "\n", args) catch return;
         }
     }
-};
+}
+
+/// Log an emergency message to stderr. This log level is intended to be used
+/// for conditions that cannot be handled and is usually followed by a panic.
+pub fn emerg(
+    comptime scope: @TypeOf(.foobar),
+    comptime format: []const u8,
+    args: var,
+) void {
+    @setCold(true);
+    log(.emerg, scope, format, args);
+}
+
+/// Log an alert message to stderr. This log level is intended to be used for
+/// conditions that should be corrected immediately (e.g. database corruption).
+pub fn alert(
+    comptime scope: @TypeOf(.foobar),
+    comptime format: []const u8,
+    args: var,
+) void {
+    @setCold(true);
+    log(.alert, scope, format, args);
+}
+
+/// Log a critical message to stderr. This log level is intended to be used
+/// when a bug has been detected or something has gone wrong and it will have
+/// an effect on the operation of the program.
+pub fn crit(
+    comptime scope: @TypeOf(.foobar),
+    comptime format: []const u8,
+    args: var,
+) void {
+    @setCold(true);
+    log(.crit, scope, format, args);
+}
+
+/// Log an error message to stderr. This log level is intended to be used when
+/// a bug has been detected or something has gone wrong but it is recoverable.
+pub fn err(
+    comptime scope: @TypeOf(.foobar),
+    comptime format: []const u8,
+    args: var,
+) void {
+    @setCold(true);
+    log(.err, scope, format, args);
+}
+
+/// Log a warning message to stderr. This log level is intended to be used if
+/// it is uncertain whether something has gone wrong or not, but the
+/// circumstances would be worth investigating.
+pub fn warn(
+    comptime scope: @TypeOf(.foobar),
+    comptime format: []const u8,
+    args: var,
+) void {
+    log(.warn, scope, format, args);
+}
+
+/// Log a notice message to stderr. This log level is intended to be used for
+/// non-error but significant conditions.
+pub fn notice(
+    comptime scope: @TypeOf(.foobar),
+    comptime format: []const u8,
+    args: var,
+) void {
+    log(.notice, scope, format, args);
+}
+
+/// Log an info message to stderr. This log level is intended to be used for
+/// general messages about the state of the program.
+pub fn info(
+    comptime scope: @TypeOf(.foobar),
+    comptime format: []const u8,
+    args: var,
+) void {
+    log(.info, scope, format, args);
+}
+
+/// Log a debug message to stderr. This log level is intended to be used for
+/// messages which are only useful for debugging.
+pub fn debug(
+    comptime scope: @TypeOf(.foobar),
+    comptime format: []const u8,
+    args: var,
+) void {
+    log(.debug, scope, format, args);
+}
diff --git a/river/main.zig b/river/main.zig
index 0e95b5c..2275efe 100644
--- a/river/main.zig
+++ b/river/main.zig
@@ -18,9 +18,9 @@
 const std = @import("std");
 
 const c = @import("c.zig");
+const log = @import("log.zig");
 const util = @import("util.zig");
 
-const Log = @import("log.zig").Log;
 const Server = @import("Server.zig");
 
 const usage: []const u8 =
@@ -58,10 +58,9 @@ pub fn main() !void {
         }
     }
 
-    Log.init(Log.Debug);
     c.wlr_log_init(.WLR_ERROR, null);
 
-    Log.Info.log("Initializing server", .{});
+    log.info(.server, "initializing", .{});
 
     var server: Server = undefined;
     try server.init();
@@ -76,9 +75,9 @@ pub fn main() !void {
         try std.ChildProcess.spawn(child);
     }
 
-    Log.Info.log("Running server...", .{});
+    log.info(.server, "running...", .{});
 
     server.run();
 
-    Log.Info.log("Shutting down server", .{});
+    log.info(.server, "shutting down", .{});
 }