blob: ed55722fa224f42869efc620dce6aeedf5feef6e [file] [log] [blame]
/*
* rollog
*
* Buffer arbitrary output in a circular buffer for some number of lines
* into potentially time disjoint logging buffer(s).
*
* The buffer(s) will currently be dumped under the following conditions:
*
* - The input stream hits EOF
* - A SIGHUP is received
* - Periodically, based on command line parameters
* - A SIGTERM is received
*
* TODO: - Zero out all sample lines before reusing a sample container.
* - Non-reentrant, so not useful to convert to a library
* - Memory is not free on completion (also a library blocker)
*
* DOCUMENTATION
*
* 50,00 foot view of the data structure relationships:
*
* current >-. ,----------------- . . . --------------------------.
* | | |
* v v |
* sample 0: sample N |
* ,---------------. ,---------------. |
* | next >------------------ . . . -----> | next >-----------'
* | fd | | fd |
* | linebuf: | | linebuf: |
* | ,-----------. | | ,-----------. |
* | | ,-< index | | | | ,-< index | |
* | | | data | | | | | data | |
* | | | ,---. | | ,-------------. | | | ,---. | |
* | | | | 0 | >-----> | data line\0 |
* | | | +---+ | | `-------------' ...
* | | | | | | | .
* | | | . | | .
* | | `-> . | | .
* | | . | |
* | | ,---. | |
* | | | N | | |
* | | `---' | |
* | `-----------' |
* `---------------'
*
* We allocate a group of N sample containers, and link them into a circular
* list. In each one, we allocate a pointer vector to contain pointers to
* sample lines. The number of both containers and lines per container is
* controlled by command line arguments.
*
* We start in the first sample container and populate its linebuf with
* lines of data up to the max; the index is the next line to be populated.
* When we hit the max, we roll back over to the first index.
*
* As a result of an event (currently, only timers or SIGUSR1), we trigger a
* switch to the next sample container, and then repeat the process for that
* container. If we trigger more times than there are sample containers, we
* roll back to the first one and start over.
*
* When we receive a termination event (currently, EOF on input, SIGTERM,
* SIGHUP, SIGINT), we take advantage of the pre-traversal of the index and
* current context, and dump out our logs in the order they were collected.
*
* By default, logs are written to stdout; they may also be sent to a specific
* file via command line option. If that file has a suffix of "XXXXXX", then
* it is used as a template for mkstemp(3), and one file is written for each
* sample container instead. When using this mode, file names will end with
* (effectively) a random string of characters; collection order can be
* inferred by file time stamp.
*/
#include <stdio.h>
#include <stdlib.h> /* atoi */
#include <unistd.h> /* exit */
#include <getopt.h> /* getopt */
#include <string.h> /* strcmp */
#include <ctype.h> /* isdigit */
#include <sys/types.h> /* open */
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h> /* sigaction */
#include <libgen.h> /* basename */
#include <setjmp.h> /* setjmp/longjmp */
/* program name with base name removed */
char *progname;
/*
* Configuration options; these can usually be left as-is
*/
#define MAX_DATA 4096 /* largest line we allow, in bytes */
#define VAL_BUFFER_DEF 100 /* number of lines in a container ring */
#define VAL_BUFFER_MIN 1
#define VAL_BUFFER_MAX 10000
#define VAL_PERIOD_DEF 0 /* seconds per container (0 disables) */
#define VAL_PERIOD_MIN 0
#define VAL_PERIOD_MAX 600
#define VAL_SAMPLES_DEF 1 /* number of containers */
#define VAL_SAMPLES_MIN 1
#define VAL_SAMPLES_MAX 10
/*
* Options we understand (the long form); there are corresponding short
* versions for each of these.
*/
static struct option long_options[] = {
{ "buffer", 1, 0, 'b' },
{ "help", 0, 0, '?' },
{ "output", 1, 0, 'o' },
{ "period", 1, 0, 'p' },
{ "samples", 1, 0, 's' },
{ 0, 0, 0, 0 }
};
/*
* usage output of help message for the program
*/
void
usage(void)
{
fprintf(stderr, "%s:\n", progname);
fprintf(stderr, "\t[-b|--buffer <lines>] lines in a circular\n");
fprintf(stderr, "\t sample buffer\n");
fprintf(stderr, "\t : default %d\n",
VAL_BUFFER_DEF);
fprintf(stderr, "\t[-h|--help] this usage message\n");
fprintf(stderr, "\t[-o|--output <name>] output file name or\n");
fprintf(stderr, "\t mkstemp(3) template\n");
fprintf(stderr, "\t (must end in XXXXXX)\n");
fprintf(stderr, "\t : default stdout\n");
fprintf(stderr, "\t[-p|--period <seconds>] sample interval\n");
fprintf(stderr, "\t : default %d%s\n",
VAL_PERIOD_DEF,
VAL_PERIOD_DEF ? "" : "(disabled)");
fprintf(stderr, "\t[-s|--samples <count>] number of samples; this\n");
fprintf(stderr, "\t indicates the number of\n");
fprintf(stderr, "\t output files when using\n");
fprintf(stderr, "\t a mkstemp(3) template\n");
fprintf(stderr, "\t : default %d\n",
VAL_SAMPLES_DEF);
exit(99);
}
/*
* Command line options and defaults.
*/
int opt_lines = VAL_BUFFER_DEF;
int opt_period = VAL_PERIOD_DEF;
int opt_samples = VAL_SAMPLES_DEF;
char *opt_template = NULL;
/*
* Line buffers; a line buffer is a ring containing up to a number of lines
* equal to opt_lines. Line data elements are reused as needed to limit the
* number of output lines we carry around.
*/
struct linebuf;
typedef struct linebuf linebuf_t;
struct linebuf {
int line_index;
char **line_data;
};
/*
* Sample containers; containers are arranged in a ring and reused as needed
* for subsequent sample runs, once the limit on the number of containers is
* reached.
*/
struct sample;
typedef struct sample sample_t;
struct sample {
sample_t *sample_next;
int sample_fd;
linebuf_t sample_linebuf;
};
sample_t *samples;
/*
* Validate that options are within allowable ranges; error out and provide
* a usage message if something is out of range.
*/
void
range(char *option, int min, int max, int requested)
{
if (requested >= min && requested <= max)
return;
fprintf(stderr, "error: %s invalid: %d; must be in the range %d..%d\n",
option, requested, min, max);
usage();
}
/*
* Save a single sample container contents. Depending on the value of
* sample_fd, this will be going to stdout, a file previously opened, or
* a mkstemp(3) file based on the input pattern.
*/
void
save_sample(sample_t *sample)
{
int index;
FILE *output = stdout;
/*
* shortcut: nothing was collected for this container, or at least
* the first sample line would have been allocated.
*/
if (sample->sample_linebuf.line_data[0] == NULL)
return;
/* Retarget output to the appropriate file */
if (sample->sample_fd != fileno(stdout)) {
/* temporary file */
if (sample->sample_fd == -1) {
/*
* Why use mkstemp? Because there's no other way to
* get a persistent temporary file attached to an fd
* so that we can use it e.g. as a log file in
* /var/log or otherwise keep it around to look at.
*/
char *template = strdup(opt_template);
if (template == NULL) {
perror("strdup");
exit(97);
}
sample->sample_fd = mkstemp(template);
if (sample->sample_fd == -1) {
perror("mkstemp");
exit(96);
}
}
/* Hook the output stream to the fd */
output = fdopen(sample->sample_fd, "w");
if (output == NULL) {
perror("fdopen");
exit(95);
}
}
/*
* Traverse the line ring buffer and dump each line out to the
* output file for the sample. We take advantage of the index
* in the linebuf_t having been pre-incremented; this means it's
* either pointing to the oldest line collected in that ring, or,
* if only a partial ring was collected, then to an uncollected
* entry, which means a NULL pointer is at that index. We run
* the index until we hit the same value again by preincrementing
* over the wrap boundary during iteration.
*/
index = sample->sample_linebuf.line_index;
for(;;) {
/*
* Save data, if it exists, until we wrap; this gets us the
* same order out as we had in. Since this is a ring buffer,
* data earlier than this will have been lost; this is either
* OK, or the program should have been invoked with a larger
* ring.
*/
if (sample->sample_linebuf.line_data[index] != NULL) {
/*
* use fprintf to avoid an extra NL; use a format
* string in case the data contains a '%' character.
*/
fprintf(output, "%s",
sample->sample_linebuf.line_data[index]);
#if NOT_NEEDED
/*
* Not strictly needed, as we are about to exit;
* note that this should use a temp variable if
* this code were intended to be reentrant, or the
* freed pointer could be traversed after it was
* freed but before it was NULL'ed.
*/
free(sample->sample_linebuf.line_data[index]);
sample->sample_linebuf.line_data[index] = NULL;
#endif /* NOT_NEEDED */
}
/* wrap boundary */
if (++index == opt_lines)
index = 0;
/* Got all lines? */
if (index == sample->sample_linebuf.line_index)
break;
}
}
/*
* Save out all the samples we have collected; in some cases, we will have
* hit a termination event prior to having collected into more than one
* sample container, despite them having been specified. In the case
* where there is no sample to collect, we avoid creating any output. See
* previous function for this skip.
*/
void
save_samples(sample_t *current)
{
sample_t *dump = current->sample_next;
/*
* Traverse the circularly linked container list to dump all the
* containers; we cheat by pre-incrementing over the end so that
* we output the samples in the order they were collected when
* they actually go to the output stream, in case there was more
* than one container involved.
*/
for(;;) {
save_sample(dump);
dump = dump->sample_next;
if (dump == current->sample_next)
break;
}
}
/*
* This is a naieve algorithm which assumes that the length of an input
* line, on average, will be much less than the maximum, and therefore any
* allocation will be smaller. This is a poor efficiency trade-off for
* buffer reuse, but we can live with that.
*
* We collect an input line into a ring biffer, and then use the index
* value on the read out to sync to the head of the buffer list by
* iterating until we hit ourselves again, using the same index fold point.
*/
void
collect_sample(sample_t *sample)
{
char linebuf[MAX_DATA];
char *old;
/*
* Read input lines until we get interrupted by something; this
* may be an elapsed interval, or it may be an EOF on input or
* a SIGHUP/SIGTERM.
*/
while(fgets(linebuf, sizeof(linebuf), stdin) != NULL) {
old = sample->sample_linebuf.line_data[
sample->sample_linebuf.line_index];
sample->sample_linebuf.line_data[
sample->sample_linebuf.line_index] =
strdup(linebuf);
if (old != NULL)
free(old);
if (++sample->sample_linebuf.line_index == opt_lines)
sample->sample_linebuf.line_index = 0;
}
}
/*
* Signal handler context for timer expiration triggering moving to another
* sample container, or SIGHUP/SIGTERM/EOF triggering program termination.
*/
sigjmp_buf sigjmp_env;
#define JR_INIT 0 /* setjmp() initial return */
#define JR_TERM 1 /* done taking samples */
#define JR_NEXT 2 /* switch to next sample container */
/*
* We're exiting; we need to dump out out collected samples for all the
* containers for which we've collected samples.
*
* Note: Context is carried around for debugging purposes
*/
void
sa_term(int signo, siginfo_t *info, void *context)
{
info = info; /* portable __unused */
signo = signo; /* portable __unused */
context = context; /* portable __unused */
siglongjmp(sigjmp_env, JR_TERM);
}
/*
* We're into the next interval; we need to move onto collecting the next
* sample container worth of data.
*
* Note: Context is carried around for debugging purposes
*/
void
sa_next(int signo, siginfo_t *info, void *context)
{
info = info; /* portable __unused */
signo = signo; /* portable __unused */
context = context; /* portable __unused */
siglongjmp(sigjmp_env, JR_NEXT);
}
struct handler {
int handler_signal;
void (*handler_func)(int, siginfo_t *, void *);
} handlers[] = {
{ SIGHUP, sa_term },
{ SIGINT, sa_term },
{ SIGTERM, sa_term },
{ SIGALRM, sa_next }, /* timer triggered */
{ SIGUSR1, sa_next } /* user triggered */
};
int numhandlers = sizeof(handlers)/sizeof(struct handler);
/*
* Set up intervals and other conditions for sample collection and trigger
* sampling. We will jump out of the signal handler into either the next
* iteration or our termination condition, if we received an EOF.
*/
void
collect_all(void)
{
struct sigaction sa;
sample_t *current = samples; /* start at the top */
int i;
sa.sa_flags = SA_SIGINFO;
for(i = 0; i < numhandlers; i++) {
sa.sa_sigaction = handlers[i].handler_func;
if (sigaction(handlers[i].handler_signal, &sa, NULL) == -1) {
perror("sigaction");
exit(98);
}
}
fprintf(stderr, "Sampling %d sample sets of %d lines at period %d\n",
opt_samples, opt_lines, opt_period);
reset_timer:
/* if we periodically switch containers, arm the alarm here */
if (opt_period)
alarm(opt_period);
/*
*
*/
switch (sigsetjmp(sigjmp_env, 1)) {
case JR_INIT: /* initial run through */
collect_sample(current);
/* fallsthrough: EOF */
case JR_TERM: /* termination by signal */
collect_sample(current);
save_samples(current);
break;
case JR_NEXT: /* interval timer fired; move to next container */
current = current->sample_next;
goto reset_timer;
}
}
/*
* rollog main program
*/
int
main(int ac, char *av[])
{
int option_index = 0;
int ofd = fileno(stdout);
int i;
sample_t *next_sample;
progname = basename(av[0]);
/*
* Process input options. All options have single character aliases
* and do not use the default value or variable pointer functionality.
*/
for(;;) {
int c = getopt_long(ac, av, "b:ho:p:s:",
long_options, &option_index);
if (c == -1)
break;
switch (c) {
case 'b': /* buffer size, in lines */
if (!isdigit(optarg[0])) {
c = '?';
break;
}
opt_lines = atoi(optarg);
range("buffer", VAL_BUFFER_MIN, VAL_BUFFER_MAX,
opt_lines);
break;
case 'o': /* output file, if not stdout */
/* '-' is an alias for stdout */
if (!strcmp("-", optarg))
break;
/*
* If it looks like a template for a temp file name,
* it is.
*/
if (strstr(optarg, "XXXXXX") != NULL) {
opt_template = optarg;
ofd = -1; /* distinguish open error -1 */
break;
}
/* otherwise, it's just a file name */
ofd = open(optarg, O_RDWR|O_CREAT|O_TRUNC|O_EXCL, 0600);
if (ofd == -1) {
perror("open");
fprintf(stderr, "can not open '%s'\n", optarg);
exit(1);
}
break;
case 'p': /* sample period, in seconds */
/*
* If specified, we will dump a sample every this
* many seconds. If there is a template file, then
* the sample will be dumped consecutively to the
* temp file. If there is a sample count, that many
* sample files will be involved.
*/
if (!isdigit(optarg[0])) {
c = '?';
break;
}
opt_period = atoi(optarg);
range("period", VAL_PERIOD_MIN, VAL_PERIOD_MAX,
opt_period);
break;
case 's': /* number of samples */
/*
* If specified, this will be the number of sample
* files we will create, if a template was given for
* the name using the output file name option.
*/
if (!isdigit(optarg[0])) {
c = '?';
break;
}
opt_samples = atoi(optarg);
range("samples", VAL_SAMPLES_MIN, VAL_SAMPLES_MAX,
opt_samples);
break;
case '?':
default:
c = '?'; /* Trigger usage/help message */
break;
}
/*
* Explicit request for help, or silent cry for help due to
* improper option usage.
*/
if (c == '?')
usage(); /* no return */
}
/*
* Meat of the code... collect sample liness from our input a line
* at a time, and buffer them up. At the sample period
*/
samples = calloc(opt_samples, sizeof(sample_t));
if (samples == NULL) {
perror("calloc");
fprintf(stderr,
"can not allocate %d sample containers", opt_samples);
exit(1);
}
/*
* Circularly link the sample containers; for each container,
* initialize the sample fd and the linebuf; initializing the
* linebuf means allocating a line vector and an initial index.
*/
next_sample = samples;
for(i = opt_samples - 1; i >= 0; i--) {
samples[i].sample_next = next_sample;
next_sample = &samples[i];
samples[i].sample_fd = ofd; /* stdout or alloc */
samples[i].sample_linebuf.line_data =
calloc(opt_lines, sizeof(linebuf_t));
if (samples[i].sample_linebuf.line_data == NULL) {
/* hope they take the hint and use a smaller #... */
perror("calloc");
fprintf(stderr,
"can not allocate lines for sample %d",
opt_samples - i);
exit(2);
}
}
collect_all();
exit(0);
}