diff --git a/lib/src/io.dart b/lib/src/io.dart
index 968f3546fbc4bcf77b02797f0ab46dc99357435c..93e096b9dfc51cf45c9fdb97b875342ddefa3c31 100644
--- a/lib/src/io.dart
+++ b/lib/src/io.dart
@@ -605,6 +605,16 @@ Future<String> consumeStringInputStream(StringInputStream stream) {
   return completer.future;
 }
 
+/// Wrap an InputStream in a ListInputStream. This eagerly drains the [source]
+/// input stream. This is useful for spawned processes which will not exit until
+/// their output streams have been drained.
+/// TODO(rnystrom): Get rid of this once #7218 is fixed.
+InputStream wrapInputStream(InputStream source) {
+  var sink = new ListInputStream();
+  pipeInputToInput(source, sink);
+  return sink;
+}
+
 /// Spawns and runs the process located at [executable], passing in [args].
 /// Returns a [Future] that will complete with the results of the process after
 /// it has ended.
diff --git a/lib/src/log.dart b/lib/src/log.dart
index 6ca1fe01cc8ffa17e5341195a92ef920df5e9349..029f72cf6e5693b8b35583b140d8af17866e8794 100644
--- a/lib/src/log.dart
+++ b/lib/src/log.dart
@@ -8,7 +8,7 @@ library log;
 import 'dart:io';
 import 'io.dart';
 
-typedef LogFn(Level level, message);
+typedef LogFn(Entry entry);
 final Map<Level, LogFn> _loggers = new Map<Level, LogFn>();
 
 /// The list of recorded log messages. Will only be recorded if
@@ -49,9 +49,9 @@ class Level {
 /// A single log entry.
 class Entry {
   final Level level;
-  final String message;
+  final List<String> lines;
 
-  Entry(this.level, this.message);
+  Entry(this.level, this.lines);
 }
 
 /// Logs [message] at [Level.ERROR].
@@ -73,12 +73,13 @@ void fine(message) => write(Level.FINE, message);
 void write(Level level, message) {
   if (_loggers.isEmpty) showNormal();
 
+  var lines = message.toString().split(NEWLINE_PATTERN);
+  var entry = new Entry(level, lines);
+
   var logFn = _loggers[level];
-  if (logFn != null) logFn(level, message);
+  if (logFn != null) logFn(entry);
 
-  if (_transcript != null) {
-    _transcript.add(new Entry(level, '$message'));
-  }
+  if (_transcript != null) _transcript.add(entry);
 }
 
 /// Logs an asynchronous IO operation. Logs [startMessage] before the operation
@@ -154,7 +155,7 @@ void dumpTranscript() {
 
   stderr.writeString('---- Log transcript ----\n');
   for (var entry in _transcript) {
-    _logToStderrWithLabel(entry.level, entry.message);
+    _logToStderrWithLabel(entry);
   }
   stderr.writeString('---- End log transcript ----\n');
 }
@@ -188,31 +189,40 @@ void showAll() {
 }
 
 /// Log function that prints the message to stdout.
-void _logToStdout(Level level, message) {
-  print('$message');
+void _logToStdout(Entry entry) {
+  _logToStream(stdout, entry, showLabel: false);
 }
 
 /// Log function that prints the message to stdout with the level name.
-void _logToStdoutWithLabel(Level level, message) {
-  print(_splitAndPrefix(level, message));
+void _logToStdoutWithLabel(Entry entry) {
+  _logToStream(stdout, entry, showLabel: true);
 }
 
 /// Log function that prints the message to stderr.
-void _logToStderr(Level level, message) {
-  stderr.writeString('$message\n');
+void _logToStderr(Entry entry) {
+  _logToStream(stderr, entry, showLabel: false);
 }
 
 /// Log function that prints the message to stderr with the level name.
-void _logToStderrWithLabel(Level level, message) {
-  stderr.writeString(_splitAndPrefix(level, message));
-  stderr.writeString('\n');
-}
-
-/// Add the level prefix to the first line of [message] and prefix subsequent
-/// lines with "|".
-String _splitAndPrefix(Level level, message) {
-  // TODO(rnystrom): We're doing lots of splitting and joining in here. If that
-  // becomes a performance problem, we can optimize this to write directly to
-  // stdout/stderr a line at a time.
-  return "$level: ${Strings.join(message.toString().split('\n'), '\n    | ')}";
+void _logToStderrWithLabel(Entry entry) {
+  _logToStream(stderr, entry, showLabel: true);
+}
+
+void _logToStream(OutputStream stream, Entry entry, {bool showLabel}) {
+  bool firstLine = true;
+  for (var line in entry.lines) {
+    if (showLabel) {
+      if (firstLine) {
+        stream.writeString(entry.level.name);
+        stream.writeString(': ');
+      } else {
+        stream.writeString('    | ');
+      }
+    }
+
+    stream.writeString(line);
+    stream.writeString('\n');
+
+    firstLine = false;
+  }
 }
diff --git a/test/test_pub.dart b/test/test_pub.dart
index ba0ba0e86202c2b299e48abb88e1e2f3bf466eb9..61afc27b73c1eb5168a717b8244615d010a5a43e 100644
--- a/test/test_pub.dart
+++ b/test/test_pub.dart
@@ -1268,7 +1268,7 @@ Future<Pair<List<String>, List<String>>> schedulePackageValidation(
   });
 }
 
-/// A matcher that matches a Pair.	
+/// A matcher that matches a Pair.
 Matcher pairOf(Matcher firstMatcher, Matcher lastMatcher) =>
    new _PairMatcher(firstMatcher, lastMatcher);
 
@@ -1334,8 +1334,8 @@ class ScheduledProcess {
   /// Wraps a [Process] [Future] in a scheduled process.
   ScheduledProcess(this.name, Future<Process> process)
     : _process = process,
-      _stdout = process.transform((p) => new StringInputStream(p.stdout)),
-      _stderr = process.transform((p) => new StringInputStream(p.stderr)) {
+      _stdout = process.transform((p) => _wrapStream(p.stdout)),
+      _stderr = process.transform((p) => _wrapStream(p.stderr)) {
 
     _schedule((_) {
       if (!_endScheduled) {
@@ -1466,6 +1466,12 @@ class ScheduledProcess {
     });
   }
 
+  /// Wraps [source] and ensures it gets eagerly drained. We do this to make
+  /// sure a process will exit even if we don't care about its output.
+  static Future<StringInputStream> _wrapStream(InputStream source) {
+    return new StringInputStream(wrapInputStream(source));
+  }
+
   /// Prints the remaining data in the process's stdout and stderr streams.
   /// Prints nothing if the straems are empty.
   Future _printStreams() {