]> git.ipfire.org Git - thirdparty/open-vm-tools.git/commitdiff
Implement log file rotation for Tools.
authorVMware, Inc <>
Mon, 22 Mar 2010 19:17:18 +0000 (12:17 -0700)
committerMarcelo Vanzin <mvanzin@vmware.com>
Mon, 22 Mar 2010 19:17:18 +0000 (12:17 -0700)
Extend the Tools file logger to support configurable log rotation.
Add tests to make sure rotation is working as expected, and update
the documentation to reflect the new config options.

Bonus: fix an issue where "double-newlines" (caused by VMware code that
embeds newlines in log messages) weren't being fixed by the logging
code unless the logger requested charset conversion (which is not the
case for file loggers).

Signed-off-by: Marcelo Vanzin <mvanzin@vmware.com>
open-vm-tools/docs/api/services/utils.txt
open-vm-tools/libvmtools/fileLogger.c
open-vm-tools/libvmtools/vmtoolsLog.c

index dbb710f2627d279d45da0bcaad8724d15c53cdf4..2ff91b0de6e20e75d1b860c6da24e36d376ecb5e 100644 (file)
@@ -50,8 +50,15 @@ for each domain are:
      - Valid values: std, outputdebugstring (Win32-only), file, file+
        (same as "file", but appends to existing log file).
      - Default: "std" on Unix, "outputdebugstring" on Windows.
-   - data: optional configuration data for the handler.
-     - Required for the "file" handlers (path to log file).
+
+For file handlers, the following extra configuration information can be
+provided:
+
+   - data: path to the log file, required.
+   - maxOldLogFiles: maximum number of rotated log files to keep around. By
+     default, at most 10 backed up log files will be kept. Value should be >= 1.
+   - maxLogSize: maximum size of each log file, defaults to 10 (MB). A value of
+     0 disables log rotation.
 
 Logging configuration should be under the "[logging]" group in the
 application's configuration file.
@@ -85,10 +92,15 @@ unity.data = /tmp/unity.log
 vmtoolsd.level = none
 @endverbatim
 
-Log file names can contain variable references. Currently, only two variables
-are expanded: @a ${USER} expands to the current user's login name, and @a ${PID}
-expands to the current process's ID. So, for example, @a log.${USER}.${PID}.txt
-would expand to "log.jdoe.1234.txt" for user "jdoe" if the process ID were 1234.
+Log file names can contain variable references. The following variables are
+expanded when determining the path of the log file:
+
+   - @a ${USER}: expands to the current user's login name
+   - @a ${PID}: expands to the current process's ID.
+   - @a ${IDX}: expands to the log file index (for rolling logs).
+
+So, for example, @a log.${USER}.${PID}.txt would expand to "log.jdoe.1234.txt"
+for user "jdoe" if the process ID were 1234.
 
 */
 
index f9d72194c7aea1ee1126bdd09fa80e7be39ee3b2..187f7858c3fe55b9a37ef98687024c8130c7b567 100644 (file)
 #endif
 
 #include "vm_assert.h"
-#include "hostinfo.h"
 
 typedef struct FileLoggerData {
    LogHandlerData    handler;
    FILE             *file;
    gchar            *path;
+   gsize             logSize;
+   gsize             maxSize;
+   guint             maxFiles;
    gboolean          append;
    gboolean          error;
 } FileLoggerData;
 
 
+/*
+ ******************************************************************************
+ * VMFileLoggerGetPath --                                               */ /**
+ *
+ * Parses the given template file name and expands embedded variables, and
+ * places the log index information at the right position.
+ *
+ * The following variables are expanded:
+ *
+ *    - ${USER}:  user's login name.
+ *    - ${PID}:   current process's pid.
+ *    - ${IDX}:   index of the log file (for rotation).
+ *
+ * @param[in] data         Log handler data.
+ * @param[in] index        Index of the log file.
+ *
+ * @return The expanded log file path.
+ *
+ ******************************************************************************
+ */
+
+static gchar *
+VMFileLoggerGetPath(FileLoggerData *data,
+                    gint index)
+{
+   gboolean hasIndex = FALSE;
+   gchar indexStr[11];
+   gchar *logpath;
+   gchar *vars[] = {
+      "${USER}",  NULL,
+      "${PID}",   NULL,
+      "${IDX}",   indexStr,
+   };
+   gchar *tmp;
+   size_t i;
+
+   logpath = g_strdup(data->path);
+   vars[1] = (char *) g_get_user_name();
+   vars[3] = g_strdup_printf("%"FMTPID, getpid());
+   g_snprintf(indexStr, sizeof indexStr, "%d", index);
+
+   for (i = 0; i < ARRAYSIZE(vars); i += 2) {
+      char *last = logpath;
+      char *start;
+      while ((start = strstr(last, vars[i])) != NULL) {
+         gchar *tmp;
+         char *end = start + strlen(vars[i]);
+         size_t offset = (start - last) + strlen(vars[i+1]);
+
+         *start = '\0';
+         tmp = g_strdup_printf("%s%s%s", logpath, vars[i+1], end);
+         g_free(logpath);
+         logpath = tmp;
+         last = logpath + offset;
+
+         /* XXX: ugly, but well... */
+         if (i == 4) {
+            hasIndex = TRUE;
+         }
+      }
+   }
+
+   g_free(vars[3]);
+
+   /*
+    * Always make sure we add the index if it's not 0, since that's used for
+    * backing up old log files.
+    */
+   if (index != 0 && !hasIndex) {
+      char *sep = strrchr(logpath, '.');
+      char *pathsep = strrchr(logpath, '/');
+
+      if (pathsep == NULL) {
+         pathsep = strrchr(logpath, '\\');
+      }
+
+      if (sep != NULL && sep > pathsep) {
+         *sep = '\0';
+         sep++;
+         tmp = g_strdup_printf("%s.%d.%s", logpath, index, sep);
+      } else {
+         tmp = g_strdup_printf("%s.%d", logpath, index);
+      }
+      g_free(logpath);
+      logpath = tmp;
+   }
+
+   return logpath;
+}
+
+
 /*
  ******************************************************************************
  * VMFileLoggerOpen --                                                  */ /**
@@ -52,40 +145,78 @@ typedef struct FileLoggerData {
  * Opens a log file for writing, backing up the existing log file if one is
  * present. Only one old log file is preserved.
  *
- * @param[in] path   Path to log file.
- * @param[in] append Whether to open the log for appending (if TRUE, a backup
- *                   file is not generated).
+ * @param[in] data   Log handler data.
  *
- * @return File pointer for writing to the file (NULL on error).
+ * @return Log file pointer (NULL on error).
  *
  ******************************************************************************
  */
 
 static FILE *
-VMFileLoggerOpen(const gchar *path,
-                 gboolean append)
+VMFileLoggerOpen(FileLoggerData *data)
 {
    FILE *logfile = NULL;
-   gchar *pathLocal;
-
-   ASSERT(path != NULL);
-   pathLocal = VMTOOLS_GET_FILENAME_LOCAL(path, NULL);
-
-   if (!append && g_file_test(pathLocal, G_FILE_TEST_EXISTS)) {
-      /* Back up existing log file. */
-      gchar *bakFile = g_strdup_printf("%s.old", pathLocal);
-      if (!g_file_test(bakFile, G_FILE_TEST_IS_DIR) &&
-          (!g_file_test(bakFile, G_FILE_TEST_EXISTS) ||
-           g_unlink(bakFile) == 0)) {
-         g_rename(pathLocal, bakFile);
-      } else {
-         g_unlink(pathLocal);
+   gchar *path;
+
+   ASSERT(data != NULL);
+   path = VMFileLoggerGetPath(data, 0);
+
+   if (g_file_test(path, G_FILE_TEST_EXISTS)) {
+      struct stat fstats;
+      if (g_stat(path, &fstats) > -1) {
+         data->logSize = (guint) fstats.st_size;
+      }
+
+      if (!data->append || data->logSize >= data->maxSize) {
+         /*
+          * Find the last log file and iterate back, changing the indices as we go,
+          * so that the oldest log file has the highest index (the new log file
+          * will always be index "0"). When not rotating, "maxFiles" is 1, so we
+          * always keep one backup.
+          */
+         gchar *log;
+         guint id;
+         GPtrArray *logfiles = g_ptr_array_new();
+
+         /*
+          * Find the id of the last log file. The pointer array will hold
+          * the names of all existing log files + the name of the last log
+          * file, which may or may not exist.
+          */
+         for (id = 0; id < data->maxFiles; id++) {
+            log = VMFileLoggerGetPath(data, id);
+            g_ptr_array_add(logfiles, log);
+            if (!g_file_test(log, G_FILE_TEST_IS_REGULAR)) {
+               break;
+            }
+         }
+
+         /* Rename the existing log files, increasing their index by 1. */
+         for (id = logfiles->len - 1; id > 0; id--) {
+            gchar *dest = g_ptr_array_index(logfiles, id);
+            gchar *src = g_ptr_array_index(logfiles, id - 1);
+
+            if (!g_file_test(dest, G_FILE_TEST_IS_DIR) &&
+                (!g_file_test(dest, G_FILE_TEST_EXISTS) ||
+                 g_unlink(dest) == 0)) {
+               g_rename(src, dest);
+            } else {
+               g_unlink(src);
+            }
+         }
+
+         /* Cleanup. */
+         for (id = 0; id < logfiles->len; id++) {
+            g_free(g_ptr_array_index(logfiles, id));
+         }
+         g_ptr_array_free(logfiles, TRUE);
+         data->logSize = 0;
+         data->append = FALSE;
       }
-      g_free(bakFile);
    }
 
-   logfile = g_fopen(pathLocal, append ? "a" : "w");
-   VMTOOLS_RELEASE_FILENAME_LOCAL(pathLocal);
+   logfile = g_fopen(path, data->append ? "a" : "w");
+   g_free(path);
    return logfile;
 }
 
@@ -130,7 +261,7 @@ VMFileLoggerLog(const gchar *domain,
          ret = TRUE;
          goto exit;
       } else {
-         data->file = VMFileLoggerOpen(data->path, data->append);
+         data->file = VMFileLoggerOpen(data);
          if (data->file == NULL) {
             data->error = TRUE;
             errfn(domain, G_LOG_LEVEL_WARNING | G_LOG_FLAG_RECURSION,
@@ -141,8 +272,24 @@ VMFileLoggerLog(const gchar *domain,
       }
    }
 
+   /* Write the log file and do log rotation accounting. */
    if (fputs(message, data->file) >= 0) {
-      fflush(data->file);
+      if (data->maxSize > 0) {
+         data->logSize += strlen(message);
+#if defined(_WIN32)
+         /* Account for \r. */
+         data->logSize += 1;
+#endif
+         if (data->logSize >= data->maxSize) {
+            fclose(data->file);
+            data->append = FALSE;
+            data->file = VMFileLoggerOpen(data);
+         } else {
+            fflush(data->file);
+         }
+      } else {
+         fflush(data->file);
+      }
       ret = TRUE;
    }
 
@@ -179,6 +326,7 @@ VMFileLoggerCopy(LogHandlerData *_current,
       g_free(current->path);
       current->file = old->file;
       current->path = old->path;
+      current->logSize = old->logSize;
       old->file = NULL;
       old->path = NULL;
    }
@@ -232,6 +380,7 @@ VMFileLoggerConfig(const gchar *domain,
    FileLoggerData *data = NULL;
    gchar *level;
    gchar key[128];
+   GError *err = NULL;
 
    g_snprintf(key, sizeof key, "%s.level", domain);
    level = g_key_file_get_string(cfg, LOGGING_GROUP, key, NULL);
@@ -242,38 +391,6 @@ VMFileLoggerConfig(const gchar *domain,
       if (logpath == NULL) {
          g_warning("Missing log path for file handler (%s).\n", domain);
          goto exit;
-      } else {
-         /*
-          * Do some variable expansion in the input string. Currently only
-          * ${USER} and ${PID} are expanded.
-          */
-         gchar *vars[] = {
-            "${USER}",  NULL,
-            "${PID}",   NULL
-         };
-         size_t i;
-
-         vars[1] = Hostinfo_GetUser();
-         vars[3] = g_strdup_printf("%"FMTPID, getpid());
-
-         for (i = 0; i < ARRAYSIZE(vars); i += 2) {
-            char *last = logpath;
-            char *start;
-            while ((start = strstr(last, vars[i])) != NULL) {
-               gchar *tmp;
-               char *end = start + strlen(vars[i]);
-               size_t offset = (start - last) + strlen(vars[i+1]);
-
-               *start = '\0';
-               tmp = g_strdup_printf("%s%s%s", logpath, vars[i+1], end);
-               g_free(logpath);
-               logpath = tmp;
-               last = logpath + offset;
-            }
-         }
-
-         vm_free(vars[1]);
-         g_free(vars[3]);
       }
    }
    g_free(level);
@@ -286,9 +403,38 @@ VMFileLoggerConfig(const gchar *domain,
    data->handler.copyfn = VMFileLoggerCopy;
    data->handler.dtor = VMFileLoggerDestroy;
 
-   data->path = logpath;
    data->append = (name != NULL && strcmp(name, "file+") == 0);
 
+   if (logpath != NULL) {
+      data->path = g_filename_from_utf8(logpath, -1, NULL, NULL, NULL);
+      ASSERT(data->path != NULL);
+      g_free(logpath);
+
+      /*
+       * Read the rolling file configuration. By default, log rotation is enabled
+       * with a max file size of 10MB and a maximum of 10 log files kept around.
+       */
+      g_snprintf(key, sizeof key, "%s.maxOldLogFiles", domain);
+      data->maxFiles = (guint) g_key_file_get_integer(cfg, LOGGING_GROUP, key, &err);
+      if (err != NULL) {
+         g_clear_error(&err);
+         data->maxFiles = 10;
+      } else if (data->maxFiles < 1) {
+         data->maxFiles = 1;
+      }
+
+      /* Add 1 to account for the active log file. */
+      data->maxFiles += 1;
+
+      g_snprintf(key, sizeof key, "%s.maxLogSize", domain);
+      data->maxSize = (gsize) g_key_file_get_integer(cfg, LOGGING_GROUP, key, &err);
+      if (err != NULL) {
+         g_clear_error(&err);
+         data->maxSize = 10;
+      }
+      data->maxSize = data->maxSize * 1024 * 1024;
+   }
+
 exit:
    return (data != NULL) ? &data->handler : NULL;
 }
index b2a0b583ce1c5825bdbc6f53eb3a70837a5b785f..12788efefc856e0f03971322a13f03da8ef8e0e2 100644 (file)
@@ -232,23 +232,21 @@ VMToolsLogFormat(const gchar *message,
    }
 
    if (msg != NULL && data->convertToLocal) {
-      size_t msgCurrLen;
-      gchar *msgCurr = g_locale_from_utf8(msg, strlen(msg), NULL, &msgCurrLen, NULL);
-
-      /*
-       * The log messages from glib itself (and probably other libraries based
-       * on glib) do not include a trailing new line. Most of our code does. So
-       * we detect whether the original message already had a new line, and
-       * remove it, to avoid having two newlines when printing our log messages.
-       */
-      if (msgCurr != NULL && msgCurr[msgCurrLen - 2] == '\n') {
-         msgCurr[msgCurrLen - 1] = '\0';
-      }
-
+      gchar *msgCurr = g_locale_from_utf8(msg, len, NULL, &len, NULL);
       g_free(msg);
       msg = msgCurr;
    }
 
+   /*
+    * The log messages from glib itself (and probably other libraries based
+    * on glib) do not include a trailing new line. Most of our code does. So
+    * we detect whether the original message already had a new line, and
+    * remove it, to avoid having two newlines when printing our log messages.
+    */
+   if (msg != NULL && msg[len - 2] == '\n') {
+      msg[len - 1] = '\0';
+   }
+
    return msg;
 }