繁体   English   中英

立即将 stdout 和 stderr 从应用程序重定向到最大大小的文件

[英]Instantly redirect stdout and stderr from app to file with max size

在我的虚拟机应用程序启动脚本中,我有:

exec /usr/java/latest/bin/java $JAVA_OPTS -jar $JAR_FILE >> /logs/$APP_NAME/startup.out 2>&1 &

这会导致 startup.out 文件的大小出现问题,因为它会重定向整个 stdout 和 stderr。 我不想要它,因为我的应用程序每天都会创建一个日志文件并截断​​它。 这意味着我的 startup.out 文件复制了应用程序中“启动”阶段之外的所有内容。

我只想将启动日志(比如说 3M)重定向到我脚本中的指定文件。 我正在尝试类似的东西:

exec /usr/java/latest/bin/java $JAVA_OPTS -jar $JAR_FILE 2>&1 | head -c3M >> /logs/$APP_NAME/startup.out &

但是head直到它有 3 兆字节才会将日志重定向到文件。

如何立即保存应用程序的前 3 兆字节日志以归档?

假设我们的程序是a.out

粗略的想法是将其通过管道传输到head ,然后通过管道传输到tee (如果附加到日志,则使用-a )。

例如,以下将写入前 2 行:

./a.out | head -n 2 | tee startup.out 

正如@Shawn 所指出的,这个简单解决方案的问题是,当head终止管道中的所有先前步骤(包括a.out )时,将收到一个SIGPIPE并过早终止。

因此,一种解决方法是强制运行head的 shell 保持活动状态,直到第一个进程终止。 让我们创建一个 FIFO completed.fifo并在我们终止时在那里写一个日期戳。 head外壳将等待(阻塞读取),直到写入日期戳。

mkfifo completed.fifo
(./a.out && date >completed.fifo) | \
    (head -n 2 && read <completed.fifo)| \
    tee startup.out

尝试使用将应用程序的输出重定向到head的管道的一个大问题是,当head在读取请求的数据量后退出时,下次您的应用程序尝试写入时,您将获得一个(通常是致命的)SIGPIPE输出 - 因为没有任何东西在听了。

我的想法是创建一个小程序,它可以代替管道中的head ,从其标准输入读取并写入真实的日志文件,直到传输了请求的数据量。 此时它继续读取,但只是处理输入而不是继续将其添加到真实日志中。 这样,您的应用程序就永远不会过早地获得 SIGPIPE。

以下特定于 Linux 的 C 程序将有效地做到这一点:

#define _GNU_SOURCE

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char **argv) {
  if (argc != 3) {
    fprintf(stderr, "Usage: %s SIZE-IN-MB LOGFILE\n", argv[0]);
    return EXIT_FAILURE;
  }

  // Make sure standard input is a pipe
  struct stat sb;
  if (fstat(STDIN_FILENO, &sb) < 0) {
    fprintf(stderr, "Unable to stat standard input: %s\n", strerror(errno));
    return EXIT_FAILURE;
  }
  if (!S_ISFIFO(sb.st_mode)) {
    fprintf(stderr, "Standard input must be a pipe!\n");
    return EXIT_FAILURE;
  }

  // Calculate how many bytes to log
  size_t bytes = strtoul(argv[1], NULL, 10) * 1024 * 1024;

  int null_fd = open("/dev/null", O_WRONLY);
  if (null_fd < 0) {
    fprintf(stderr, "Unable to open /dev/null: %s\n", strerror(errno));
    return EXIT_FAILURE;
  }

  // The real log file
  int log_fd = open(argv[2], O_WRONLY | O_CREAT, 0644);
  if (log_fd < 0) {
    fprintf(stderr, "Unable to open log file '%s': %s\b", argv[2], strerror(errno));
    return EXIT_FAILURE;
  }
  if (lseek(log_fd, 0, SEEK_END) < 0) {
    fprintf(stderr, "Unable to seek to end of log file: %s\n", strerror(errno));
    return EXIT_FAILURE;
  }

  // Use splice(2) to efficiently move data from the stdin pipe to the log
  while (bytes > 0) {
    ssize_t transferred = splice(STDIN_FILENO, NULL, log_fd, NULL, bytes, SPLICE_F_MORE);
    if (transferred < 0) {
      fprintf(stderr, "Unable to copy data to log: %s\n", strerror(errno));
      break;
    } else if (transferred == 0) { // End of file
      return 0;
    } else {
      bytes -= transferred;
    }
  }
  close(log_fd); // Don't need this any more

  // Now loop until the main app exits, moving data it writes to /dev/null
  // to get rid of it.
  while (1) {
    ssize_t transferred = splice(STDIN_FILENO, NULL, null_fd, NULL, 4096, SPLICE_F_MORE);
    if (transferred < 0) {
      fprintf(stderr, "Unable to dump excess log data: %s\n", strerror(errno));
      return EXIT_FAILURE;
    } else if (transferred == 0) {
      // The writer exited, so shall we.
      return 0;
    }
  }
}

示例用法:

$ gcc -o logcopy -O -Wall -Wextra logcopy.c 
# real.log will grow to around 1mb and stop no matter how long you let the
# below command run
$ yes | ./logcopy 1 real.log

您的用法可能类似于

exec /usr/java/latest/bin/java "$JAVA_OPTS" -jar "$JAR_FILE" 2>&1 | ./logcopy 3 "/logs/$APP_NAME/startup.out" &
exec stdbuf -oL /usr/java/latest/bin/java $JAVA_OPTS -jar $JAR_FILE 2>&1 |
    { head -c 3M >> /logs/$APP_NAME/startup.out; cat > /dev/null; }

有两个问题需要克服:

  1. stdout被重定向到一个文件时, glibc从行缓冲切换到全缓冲,其中输出被缓冲并以 4KB 块的形式写出。 这样做是为了提高效率,但这意味着如果输出生成缓慢,您将看不到打印的行。

    您可以使用stdbuf从外部更改此行为。 stdbuf -oL覆盖默认行为并强制行缓冲。

  2. head完成时,您的程序将收到一个SIGPIPE指示管道已关闭。 通常这很有帮助。 如果没有人在读取它们的输出,它会告诉程序停止。 但是,如果您希望程序在head退出后继续运行,您需要忽略或阻止SIGPIPE

    您可以通过在head之后调用cat来保持管道运行。 cat将吸收多余的输出,在head退出后保持管道运行。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM