diff --git a/check/check.go b/check/check.go
index a659622d..c98a391e 100644
--- a/check/check.go
+++ b/check/check.go
@@ -18,7 +18,6 @@ package check
 
 import (
 	"fmt"
-	"os"
 
 	"github.com/arduino/arduino-check/check/checkconfigurations"
 	"github.com/arduino/arduino-check/check/checkdata"
@@ -33,15 +32,14 @@ import (
 
 // RunChecks runs all checks for the given project and outputs the results.
 func RunChecks(project project.Type) {
-	feedback.Printf("Checking %s in %s\n", project.ProjectType, project.Path)
+	feedback.Printf("\nChecking %s in %s\n", project.ProjectType, project.Path)
 
 	checkdata.Initialize(project, configuration.SchemasPath())
 
 	for _, checkConfiguration := range checkconfigurations.Configurations() {
 		runCheck, err := shouldRun(checkConfiguration, project)
 		if err != nil {
-			feedback.Errorf("Error while determining whether to run check: %v", err)
-			os.Exit(1)
+			panic(err)
 		}
 
 		if !runCheck {
@@ -55,15 +53,9 @@ func RunChecks(project project.Type) {
 		checkResult, checkOutput := checkConfiguration.CheckFunction()
 		reportText := result.Results.Record(project, checkConfiguration, checkResult, checkOutput)
 		if (checkResult == checkresult.Fail) || configuration.Verbose() {
-			feedback.Print(reportText)
+			feedback.Println(reportText)
 		}
 	}
-
-	// Checks are finished for this project, so summarize its check results in the report.
-	result.Results.AddProjectSummary(project)
-
-	// Print the project check results summary.
-	feedback.Print(result.Results.ProjectSummaryText(project))
 }
 
 // shouldRun returns whether a given check should be run for the given project under the current tool configuration.
diff --git a/check/checkdata/schema/parsevalidationresult.go b/check/checkdata/schema/parsevalidationresult.go
index f358ef54..b1db7af5 100644
--- a/check/checkdata/schema/parsevalidationresult.go
+++ b/check/checkdata/schema/parsevalidationresult.go
@@ -189,10 +189,10 @@ func validationErrorSchemaPointerValueMatch(
 	schemasPath *paths.Path,
 ) bool {
 	marshalledSchemaPointerValue, err := json.Marshal(schemaPointerValue(schemaURL, schemaPointer, schemasPath))
-	logrus.Tracef("Checking schema pointer value: %s match with regexp: %s", marshalledSchemaPointerValue, schemaPointerValueRegexp)
 	if err != nil {
 		panic(err)
 	}
+	logrus.Tracef("Checking schema pointer value: %s match with regexp: %s", marshalledSchemaPointerValue, schemaPointerValueRegexp)
 	return schemaPointerValueRegexp.Match(marshalledSchemaPointerValue)
 }
 
diff --git a/command/command.go b/command/command.go
index cb6826b9..13e453b1 100644
--- a/command/command.go
+++ b/command/command.go
@@ -67,6 +67,12 @@ func ArduinoCheck(rootCommand *cobra.Command, cliArguments []string) {
 
 	for _, project := range projects {
 		check.RunChecks(project)
+
+		// Checks are finished for this project, so summarize its check results in the report.
+		result.Results.AddProjectSummary(project)
+
+		// Print the project check results summary.
+		feedback.Printf("\n%s\n", result.Results.ProjectSummaryText(project))
 	}
 
 	// All projects have been checked, so summarize their check results in the report.
@@ -75,7 +81,7 @@ func ArduinoCheck(rootCommand *cobra.Command, cliArguments []string) {
 	if configuration.OutputFormat() == outputformat.Text {
 		if len(projects) > 1 {
 			// There are multiple projects, print the summary of check results for all projects.
-			fmt.Print(result.Results.SummaryText())
+			fmt.Printf("\n%s\n", result.Results.SummaryText())
 		}
 	} else {
 		// Print the complete JSON formatted report.
diff --git a/configuration/configuration.go b/configuration/configuration.go
index 2e373a4a..a9554d23 100644
--- a/configuration/configuration.go
+++ b/configuration/configuration.go
@@ -56,12 +56,15 @@ func Initialize(flags *pflag.FlagSet, projectPaths []string) error {
 		}
 	}
 
+	logrus.SetOutput(defaultLogOutput)
+
 	if logFormatString, ok := os.LookupEnv("ARDUINO_CHECK_LOG_FORMAT"); ok {
 		logFormat, err := logFormatFromString(logFormatString)
 		if err != nil {
 			return fmt.Errorf("--log-format flag value %s not valid", logFormatString)
 		}
 		logrus.SetFormatter(logFormat)
+		logrus.SetOutput(os.Stderr) // Enable log output.
 	}
 
 	if logLevelString, ok := os.LookupEnv("ARDUINO_CHECK_LOG_LEVEL"); ok {
@@ -70,8 +73,7 @@ func Initialize(flags *pflag.FlagSet, projectPaths []string) error {
 			return fmt.Errorf("--log-level flag value %s not valid", logLevelString)
 		}
 		logrus.SetLevel(logLevel)
-	} else {
-		logrus.SetLevel(defaultLogLevel)
+		logrus.SetOutput(os.Stderr) // Enable log output.
 	}
 
 	superprojectTypeFilterString, _ := flags.GetString("project-type")
diff --git a/configuration/configuration_test.go b/configuration/configuration_test.go
index 9ea49f30..b44e0b6f 100644
--- a/configuration/configuration_test.go
+++ b/configuration/configuration_test.go
@@ -120,7 +120,6 @@ func TestInitializeLogFormat(t *testing.T) {
 
 func TestInitializeLogLevel(t *testing.T) {
 	require.Nil(t, Initialize(test.ConfigurationFlags(), projectPaths))
-	assert.Equal(t, defaultLogLevel, logrus.GetLevel(), "Default level")
 
 	os.Setenv("ARDUINO_CHECK_LOG_LEVEL", "foo")
 	assert.Error(t, Initialize(test.ConfigurationFlags(), projectPaths), "Invalid level")
diff --git a/configuration/defaults.go b/configuration/defaults.go
index a4cc1e3b..7f2f3d5d 100644
--- a/configuration/defaults.go
+++ b/configuration/defaults.go
@@ -18,9 +18,10 @@ package configuration
 // The default configuration settings.
 
 import (
+	"io/ioutil"
+
 	"github.com/arduino/arduino-check/configuration/checkmode"
 	"github.com/arduino/arduino-check/project/projecttype"
-	"github.com/sirupsen/logrus"
 )
 
 // Default check modes for each superproject type.
@@ -60,4 +61,4 @@ var defaultCheckModes = map[projecttype.Type]map[checkmode.Type]bool{
 	},
 }
 
-var defaultLogLevel = logrus.FatalLevel
+var defaultLogOutput = ioutil.Discard // Default to no log output.
diff --git a/result/feedback/feedback.go b/result/feedback/feedback.go
index bf603aa1..da79d815 100644
--- a/result/feedback/feedback.go
+++ b/result/feedback/feedback.go
@@ -18,43 +18,56 @@ package feedback
 
 import (
 	"fmt"
+	"os"
 
 	"github.com/arduino/arduino-check/configuration"
 	"github.com/arduino/arduino-check/result/outputformat"
 	"github.com/sirupsen/logrus"
 )
 
+// VerbosePrintln behaves like Println but only prints when verbosity is enabled.
+func VerbosePrintln(v ...interface{}) {
+	VerbosePrint(v...)
+	VerbosePrint("\n")
+}
+
 // VerbosePrintf behaves like Printf but only prints when verbosity is enabled.
 func VerbosePrintf(format string, v ...interface{}) {
 	VerbosePrint(fmt.Sprintf(format, v...))
 }
 
 // VerbosePrint behaves like Print but only prints when verbosity is enabled.
-func VerbosePrint(message string) {
+func VerbosePrint(v ...interface{}) {
 	if configuration.Verbose() && (configuration.OutputFormat() == outputformat.Text) {
-		Printf(message)
+		Print(v...)
 	}
 }
 
+// Println behaves like fmt.Println but only prints when output format is set to `text`.
+func Println(v ...interface{}) {
+	Print(v...)
+	Print("\n")
+}
+
 // Printf behaves like fmt.Printf but only prints when output format is set to `text`.
 func Printf(format string, v ...interface{}) {
 	Print(fmt.Sprintf(format, v...))
 }
 
 // Print behaves like fmt.Print but only prints when output format is set to `text`.
-func Print(message string) {
+func Print(v ...interface{}) {
 	if configuration.OutputFormat() == outputformat.Text {
-		fmt.Printf(message)
+		fmt.Print(v...)
 	}
 }
 
-// Errorf behaves like fmt.Printf but also logs the error.
+// Errorf behaves like fmt.Printf but adds a newline and also logs the error.
 func Errorf(format string, v ...interface{}) {
 	Error(fmt.Sprintf(format, v...))
 }
 
-// Error behaves like fmt.Print but also logs the error.
-func Error(errorMessage string) {
-	fmt.Printf(errorMessage)
-	logrus.Error(fmt.Sprint(errorMessage))
+// Error behaves like fmt.Print but adds a newline and also logs the error.
+func Error(v ...interface{}) {
+	fmt.Fprintln(os.Stderr, v...)
+	logrus.Error(fmt.Sprint(v...))
 }
diff --git a/result/result.go b/result/result.go
index 61ff2709..c8b72b4f 100644
--- a/result/result.go
+++ b/result/result.go
@@ -21,7 +21,6 @@ import (
 	"encoding/json"
 	"fmt"
 	"html/template"
-	"os"
 
 	"github.com/arduino/arduino-check/check/checkconfigurations"
 	"github.com/arduino/arduino-check/check/checklevel"
@@ -29,7 +28,6 @@ import (
 	"github.com/arduino/arduino-check/configuration"
 	"github.com/arduino/arduino-check/configuration/checkmode"
 	"github.com/arduino/arduino-check/project"
-	"github.com/arduino/arduino-check/result/feedback"
 	"github.com/arduino/go-paths-helper"
 )
 
@@ -95,11 +93,10 @@ func (results *Type) Initialize() {
 func (results *Type) Record(checkedProject project.Type, checkConfiguration checkconfigurations.Type, checkResult checkresult.Type, checkOutput string) string {
 	checkLevel, err := checklevel.CheckLevel(checkConfiguration, checkResult)
 	if err != nil {
-		feedback.Errorf("Error while determining check level: %v", err)
-		os.Exit(1)
+		panic(fmt.Errorf("Error while determining check level: %v", err))
 	}
 
-	summaryText := fmt.Sprintf("Check %s result: %s\n", checkConfiguration.ID, checkResult)
+	summaryText := fmt.Sprintf("Check %s result: %s", checkConfiguration.ID, checkResult)
 
 	checkMessage := ""
 	if checkResult == checkresult.Fail {
@@ -112,7 +109,7 @@ func (results *Type) Record(checkedProject project.Type, checkConfiguration chec
 
 	// Add explanation of check result if present.
 	if checkMessage != "" {
-		summaryText += fmt.Sprintf("%s: %s\n", checkLevel, checkMessage)
+		summaryText += fmt.Sprintf("\n%s: %s", checkLevel, checkMessage)
 	}
 
 	reportExists, projectReportIndex := results.getProjectReportIndex(checkedProject.Path)
@@ -187,7 +184,7 @@ func (results Type) ProjectSummaryText(checkedProject project.Type) string {
 	}
 
 	projectSummaryReport := results.Projects[projectReportIndex].Summary
-	return fmt.Sprintf("\nFinished checking project. Results:\nWarning count: %v\nError count: %v\nChecks passed: %v\n\n", projectSummaryReport.WarningCount, projectSummaryReport.ErrorCount, projectSummaryReport.Pass)
+	return fmt.Sprintf("Finished checking project. Results:\nWarning count: %v\nError count: %v\nChecks passed: %v", projectSummaryReport.WarningCount, projectSummaryReport.ErrorCount, projectSummaryReport.Pass)
 }
 
 // AddSummary summarizes the check results for all projects and adds it to the report.
@@ -212,7 +209,7 @@ func (results *Type) AddSummary() {
 
 // SummaryText returns a text summary of the cumulative check results.
 func (results Type) SummaryText() string {
-	return fmt.Sprintf("Finished checking projects. Results:\nWarning count: %v\nError count: %v\nChecks passed: %v\n", results.Summary.WarningCount, results.Summary.ErrorCount, results.Summary.Pass)
+	return fmt.Sprintf("Finished checking projects. Results:\nWarning count: %v\nError count: %v\nChecks passed: %v", results.Summary.WarningCount, results.Summary.ErrorCount, results.Summary.Pass)
 }
 
 // JSONReport returns a JSON formatted report of checks on all projects.
diff --git a/result/result_test.go b/result/result_test.go
index bab43d5a..3892cac4 100644
--- a/result/result_test.go
+++ b/result/result_test.go
@@ -75,9 +75,9 @@ func TestRecord(t *testing.T) {
 	checkConfiguration := checkconfigurations.Configurations()[0]
 	checkOutput := "foo"
 	summaryText := results.Record(checkedProject, checkConfiguration, checkresult.Fail, checkOutput)
-	assert.Equal(t, fmt.Sprintf("Check %s result: %s\n%s: %s\n", checkConfiguration.ID, checkresult.Fail, checklevel.Error, message(checkConfiguration.MessageTemplate, checkOutput)), summaryText)
+	assert.Equal(t, fmt.Sprintf("Check %s result: %s\n%s: %s", checkConfiguration.ID, checkresult.Fail, checklevel.Error, message(checkConfiguration.MessageTemplate, checkOutput)), summaryText)
 	summaryText = results.Record(checkedProject, checkConfiguration, checkresult.NotRun, checkOutput)
-	assert.Equal(t, fmt.Sprintf("Check %s result: %s\n%s: %s\n", checkConfiguration.ID, checkresult.NotRun, checklevel.Notice, checkOutput), summaryText, "Non-fail result should not use message")
+	assert.Equal(t, fmt.Sprintf("Check %s result: %s\n%s: %s", checkConfiguration.ID, checkresult.NotRun, checklevel.Notice, checkOutput), summaryText, "Non-fail result should not use message")
 	summaryText = results.Record(checkedProject, checkConfiguration, checkresult.Pass, "")
 	assert.Equal(t, "", "", summaryText, "Non-failure result with no check function output should result in an empty summary")
 
@@ -198,7 +198,7 @@ func TestAddProjectSummary(t *testing.T) {
 		assert.Equal(t, testTable.expectedPass, results.Projects[0].Summary.Pass)
 		assert.Equal(t, testTable.expectedWarningCount, results.Projects[0].Summary.WarningCount)
 		assert.Equal(t, testTable.expectedErrorCount, results.Projects[0].Summary.ErrorCount)
-		assert.Equal(t, fmt.Sprintf("\nFinished checking project. Results:\nWarning count: %v\nError count: %v\nChecks passed: %v\n\n", testTable.expectedWarningCount, testTable.expectedErrorCount, testTable.expectedPass), results.ProjectSummaryText(checkedProject))
+		assert.Equal(t, fmt.Sprintf("Finished checking project. Results:\nWarning count: %v\nError count: %v\nChecks passed: %v", testTable.expectedWarningCount, testTable.expectedErrorCount, testTable.expectedPass), results.ProjectSummaryText(checkedProject))
 	}
 }
 
@@ -281,7 +281,7 @@ func TestAddSummary(t *testing.T) {
 		assert.Equal(t, testTable.expectedPass, results.Passed())
 		assert.Equal(t, testTable.expectedWarningCount, results.Summary.WarningCount)
 		assert.Equal(t, testTable.expectedErrorCount, results.Summary.ErrorCount)
-		assert.Equal(t, fmt.Sprintf("Finished checking projects. Results:\nWarning count: %v\nError count: %v\nChecks passed: %v\n", testTable.expectedWarningCount, testTable.expectedErrorCount, testTable.expectedPass), results.SummaryText())
+		assert.Equal(t, fmt.Sprintf("Finished checking projects. Results:\nWarning count: %v\nError count: %v\nChecks passed: %v", testTable.expectedWarningCount, testTable.expectedErrorCount, testTable.expectedPass), results.SummaryText())
 	}
 }