diff --git a/src/cluster/cluster.cpp b/src/cluster/cluster.cpp
index c8aec3d43aa0cf3d68fb9fec54ef5b49430f3341..971d83f6139a615b210a1bdb60077fde51867485 100644
--- a/src/cluster/cluster.cpp
+++ b/src/cluster/cluster.cpp
@@ -17,6 +17,10 @@
 #include "../include/errors.h"
 #endif
 
+#ifndef INCLUDE_LOGGING_H_
+#include "../include/logging.h"
+#endif
+
 #ifndef INCLUDE_CONFIGURATION_H_
 #include "../include/Configuration.h"
 #endif
@@ -50,13 +54,15 @@ using namespace std;
  *  \param output_path: `string` Directory to write the output files in.
  */
 void cluster(string config_file, string data_file, string output_path) {
-  printf("INFO: making legacy configuration...");
+  Logger logger(LOG_INFO);
+  logger.log("INFO: making legacy configuration...", LOG_INFO);
   ScattererConfiguration *sconf = NULL;
   try {
     sconf = ScattererConfiguration::from_dedfb(config_file);
   } catch(const OpenConfigurationFileException &ex) {
-    printf("\nERROR: failed to open scatterer configuration file.\n");
-    printf("FILE: %s\n", ex.what());
+    logger.err("\nERROR: failed to open scatterer configuration file.\n");
+    string message = "FILE: " + string(ex.what()) + "\n";
+    logger.err(message);
     exit(1);
   }
   sconf->write_formatted(output_path + "/c_OEDFB");
@@ -66,12 +72,13 @@ void cluster(string config_file, string data_file, string output_path) {
   try {
     gconf = GeometryConfiguration::from_legacy(data_file);
   } catch (const OpenConfigurationFileException &ex) {
-    printf("\nERROR: failed to open geometry configuration file.\n");
-    printf("FILE: %s\n", ex.what());
+    logger.err("\nERROR: failed to open geometry configuration file.\n");
+    string message = "FILE: " + string(ex.what()) + "\n";
+    logger.err(message);
     if (sconf) delete sconf;
     exit(1);
   }
-  printf(" done.\n");
+  logger.log(" done.\n", LOG_INFO);
   if (sconf->number_of_spheres == gconf->number_of_spheres) {
     // Shortcuts to variables stored in configuration objects
     int nsph = gconf->number_of_spheres;
@@ -279,9 +286,9 @@ void cluster(string config_file, string data_file, string output_path) {
     tppoan.open(tppoan_name.c_str(), ios::out | ios::binary);
     if (tppoan.is_open()) {
 #ifdef USE_LAPACK
-      printf("INFO: using LAPACK calls.\n");
+      logger.log("INFO: using LAPACK calls.\n", LOG_INFO);
 #else
-      printf("INFO: using fall-back lucin() calls.\n");
+      logger.log("INFO: using fall-back lucin() calls.\n", LOG_INFO);
 #endif
       tppoan.write(reinterpret_cast<char *>(&iavm), sizeof(int));
       tppoan.write(reinterpret_cast<char *>(&isam), sizeof(int));
@@ -299,7 +306,9 @@ void cluster(string config_file, string data_file, string output_path) {
 	fprintf(output, " \n");
       }
       for (int jxi488 = 1; jxi488 <= nxi; jxi488++) {
-	printf("INFO: running scale iteration %d of %d...", jxi488, nxi);
+	string message = ("INFO: running scale iteration " + to_string(jxi488)
+			  + " of " + to_string(nxi) + "...");
+	logger.log(message, LOG_INFO);
 	int jaw = 1;
 	fprintf(output, "========== JXI =%3d ====================\n", jxi488);
 	double xi = sconf->scale_vec[jxi488 - 1];
@@ -349,6 +358,7 @@ void cluster(string config_file, string data_file, string output_path) {
 	  }
 	  if (jer != 0) break;
 	} // i132 loop
+	logger.push("DEBUG: " + TOSTRING(cms(am, c1, c1ao, c4, c6);));
 	cms(am, c1, c1ao, c4, c6);
 	invert_matrix(am, ndit, jer, mxndm);
 	if (jer != 0) break; // jxi488 loop: goes to memory clean
@@ -904,11 +914,11 @@ void cluster(string config_file, string data_file, string output_path) {
 	  } // jph484 loop
 	  th += thstp;
 	} // jth486 loop
-	printf(" done.\n");
+	logger.log(" done.\n", LOG_INFO);
       } // jxi488 loop
       tppoan.close();
     } else { // In case TPPOAN could not be opened. Should never happen.
-      printf("\nERROR: failed to open TPPOAN file.\n");
+      logger.err("\nERROR: failed to open TPPOAN file.\n");
     }
     fclose(output);
     // Clean memory
@@ -992,5 +1002,6 @@ void cluster(string config_file, string data_file, string output_path) {
   }
   delete sconf;
   delete gconf;
-  printf("Finished: output written to %s.\n", (output_path + "/c_OCLU").c_str());
+  logger.flush(LOG_INFO);
+  logger.log("Finished: output written to " + output_path + "/c_OCLU\n");
 }
diff --git a/src/include/logging.h b/src/include/logging.h
index 69a3b40b2e8da3897f77f65ac8619fba8a5d75c7..9545af27fbe12d47cc0f6e9d4df24579693d68e3 100644
--- a/src/include/logging.h
+++ b/src/include/logging.h
@@ -7,11 +7,18 @@
 #ifndef INCLUDE_LOGGING_H_
 #define INCLUDE_LOGGING_H_
 
+//! \brief Debug level logging (maximum verbosity).
 #define LOG_DEBG 0
+//! \brief Standard information level logging (default).
 #define LOG_INFO 1
+//! \brief Warning level logging (almost quiet).
 #define LOG_WARN 2
+//! \brief Error level logging (silent, unless in pain).
 #define LOG_ERRO 3
 
+//! \brief Macro to stringize code lines
+#define TOSTRING(ARG) string(#ARG)
+
 /*! \brief Logger class.
  *
  * Loggers are objects used to track the execution of a code, reporting activities
@@ -23,9 +30,16 @@
  */
 class Logger {
  protected:
-  FILE *log_output;
+  //! \brief Pointer to error stream.
   FILE *err_output;
+  //! \brief Pointer to logging stream.
+  FILE *log_output;
+  //! \brief Last logged message.
+  std::string last_message;
+  //! \brief Threshold of logging level.
   int log_threshold;
+  //! \brief Number of identical message repetitions.
+  long repetitions;
 
  public:
   /*! \brief Logger instance constructor.
@@ -47,12 +61,30 @@ class Logger {
    */
   void err(std::string message);
 
+  /*! \brief Print a summary of recurrent messages and clear the stack.
+   *
+   * \param level: `int` The priority level (default is `LOG_DEBG = 0`).
+   */
+  void flush(int level=LOG_DEBG);
+
   /*! \brief Print a message, depending on its logging level.
    *
    * \param message: `string` The message to be printed.
    * \param level: `int` The priority level (default is `LOG_INFO = 1`).
    */
   void log(std::string message, int level=LOG_INFO);
+  
+  /*! \brief Push a recurrent message to the message stack.
+   *
+   * When a long stream of identical messages is expected, it may be more
+   * convenient to put them in a stack. The stack is the error output stream,
+   * so that no memory is consumed. After the call stack is over, or is the
+   * message changes, the stack is flushed, meaning that a summary message
+   * is written to the logging output and the stack counter is reset to 0.
+   *
+   * \param message: `string` The message to be stacked.
+   */
+  void push(std::string message);
 };
 
 #endif
diff --git a/src/libnptm/logging.cpp b/src/libnptm/logging.cpp
index e86763153b14ee143163b16a50371d33ea735847..f27bd256c5e6a1b8326014ca756bfd8764e76b20 100644
--- a/src/libnptm/logging.cpp
+++ b/src/libnptm/logging.cpp
@@ -14,18 +14,42 @@
 using namespace std;
 
 Logger::Logger(int threshold, FILE *logging_output, FILE *error_output) {
+  last_message = "";
   log_threshold = threshold;
   log_output = logging_output;
   err_output = error_output;
+  repetitions = 0;
 }
 
 void Logger::err(std::string message) {
-  fprintf(err_output, "%s\n", message.c_str());
+  fprintf(err_output, "%s", message.c_str());
+}
+
+void Logger::flush(int level) {
+  string summary = "\"" + last_message + "\" issued " + to_string(repetitions);
+  if (repetitions == 1) summary += " time.\n";
+  else summary += " times.\n";
+  if (level == LOG_ERRO) err(summary);
+  else {
+    if (level >= log_threshold) fprintf(log_output, "%s", summary.c_str());
+  }
+  repetitions = 0;
 }
 
 void Logger::log(std::string message, int level) {
   if (level == LOG_ERRO) err(message);
   else {
-    if (level >= log_threshold) fprintf(log_output, "%s\n", message.c_str());
+    if (level >= log_threshold) fprintf(log_output, "%s", message.c_str());
+  }
+}
+
+void Logger::push(std::string message) {
+  if (repetitions > 0) {
+    if (message.compare(last_message) != 0) {
+      flush(LOG_DEBG);
+    }
   }
+  log(message, LOG_DEBG);
+  last_message = message;
+  repetitions++;
 }