8

Is there a way to do something like this:

tail -f logs/

and make the stdout to be updated on every line added to each file already present in logs/ and to every file that will be created in logs/ after the command is issued?

MadHatter
  • 78,442
  • 20
  • 178
  • 229
Jack
  • 485
  • 1
  • 4
  • 12

6 Answers6

8

Thanks all for support, but since nor mutitail nor tail -F nor watch tail seems to help for what I need, I developed a small solution in C. I post the code here, since maybe someone can find it useful. (There are missing checks and some weakness I know, but so far it's enough)

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/inotify.h>
#include <sys/stat.h>
#include <signal.h>
#include <dirent.h>
#include <linux/limits.h>
#define    CHAR_BACK   500

// * File handler structure
struct file_followed { long last_position; char filename[NAME_MAX]; struct file_followed * next; };
struct file_followed * file_list = NULL;

// * To quit peacefully
int cycle = 1;
void stopCycle(int u) { cycle = 0; }

// * Last tailed filename
char last_tailed[NAME_MAX];

void fileAdd(char * file) {
    struct file_followed ** list = &file_list;
    struct stat statdesc;

    if(stat(file, &statdesc) || !S_ISREG(statdesc.st_mode)) { return; }
    while(*list) { list = &((*list)->next); }
    *list = malloc(sizeof(struct file_followed));
    (*list)->last_position = -1;
    strcpy((*list)->filename, file);
    (*list)->next = NULL;
}

int fileTail(struct file_followed * item) {
    int ret = 0;
    FILE * fp = fopen(item->filename, "r");
    fseek(fp, 0, SEEK_END);
    long end_position = ftell(fp);

    if( end_position != item->last_position ) {
        if(strcmp(item->filename, last_tailed)) { strcpy(last_tailed, item->filename); printf("\n** %s **:\n", item->filename); }

        int start_position = item->last_position == -1 || item->last_position > end_position ? (end_position-CHAR_BACK > 0 ? end_position-CHAR_BACK : 0) : item->last_position;
        fseek(fp, start_position, SEEK_SET);

        int len = end_position - start_position;
        char * buf = malloc(len+1);
        fread(buf, len, 1, fp);
        buf[len] = '\0';
        printf("%s%s", len == CHAR_BACK ? "[...]" : "", buf);
        free(buf);

        item->last_position = end_position;
        ret = 1;
    }

    fclose(fp);
    return ret;
}

void fileRem(char * file) {
    struct file_followed ** list = &file_list;
    while(*list && strcmp((*list)->filename, file)) { list = &((*list)->next); }
    if(*list) { struct file_followed * todel = *list; *list = (*list)->next; free(todel); }
}

int main(int argc, char ** argv) {

    struct dirent **namelist;
    struct stat statdesc;
    struct timeval tv;
    fd_set set;
    int fd;
    int wd;
    int r;

    // * Help
    if(stat(argv[1], &statdesc) || !S_ISDIR(statdesc.st_mode)) { printf("[usage] %s dir-to-monitor\n", argv[0]); exit(EXIT_FAILURE); }

    // * Init
    chdir(argv[1]);
    memset(last_tailed, 0, sizeof(last_tailed));
    signal(SIGINT, stopCycle);
    signal(SIGTERM, stopCycle);

    // * Inotify
    if( (fd = inotify_init()) < 0) { perror("inotify_init"); }
    if( (wd = inotify_add_watch( fd, ".", IN_CREATE | IN_DELETE ) < 0)) { perror("inotify_add_watch"); }

    // * File add recursively on dirscan
    if( (r = scandir(".", &namelist, 0, alphasort)) < 0) { perror("scandir"); }
    while (r--) { fileAdd(namelist[r]->d_name); free(namelist[r]); }
    free(namelist);

    // * Neverending cycle
    while(cycle) {
        // * Select on inotify
        FD_ZERO(&set);
        FD_SET(fd, &set);
        tv.tv_sec = 0;
        tv.tv_usec = 1000;
        if( (r = select(fd+1, &set, NULL, NULL, &tv)) == -1) { perror("select"); }

        // * New add or del on inotify
        if(r) {
            struct inotify_event * event;
            char buf[1024];
            if(read(fd, buf, 1024) <= 0) { perror("read"); }
            event = (struct inotify_event *) buf;
            if(event->mask & IN_CREATE) { fileAdd(event->name); } 
            else if(event->mask & IN_DELETE) { fileRem(event->name); }
        }

        // * Check for new tails
        struct file_followed * list = file_list;
        int tailers = 0;
        while(list) { tailers += fileTail(list); list = list->next; }
        if(!tailers) { usleep(500000); }
    }

    // * Stop inotify
    inotify_rm_watch( fd, wd );
    close(fd);

    return EXIT_SUCCESS;
}
Jack
  • 485
  • 1
  • 4
  • 12
  • You Sir, are a genius. Been looking for something like this for a while. I have the output of an app that is date logged and the current tail doesn't pick up new days. This works perfectly. Care to move this to `github` or similar and let it be improved upon (only based on what you said re checks). Did you base this on the code for `tail` originally, or is it built from the ground up? – Madivad May 04 '16 at 00:22
  • 1
    Glad to find it helped someone. I wrote this from scratch, logic is quite simple thanks to inotify api.. so simple that I never thought to githubbing it. Feel free to do it if you think it's worth it – Jack May 04 '16 at 09:28
  • 2
    G'day Sir Jack, I have taken the liberty to add this to [github](https://github.com/madivad/retail) since it is one of the best little utilities I've come across in ages and I now have it running literally 24/7. Please feel free to add/change/TAKE CONTROL OF the repo and if you so desire, I will gladly hand it over to you (if you tell me how) since I don't know if I have the skills required to do it justice. Thanks again for answering a plaguing question of mine and possibly others :) Cheers! – Madivad May 15 '16 at 11:20
  • You're great Madivad. I saw the page and loved it. You did very good, thanks! – Jack May 15 '16 at 15:42
  • I made a version of Madivad's retail script that works on macos: https://github.com/aiba/retail – Aaron Iba Sep 23 '20 at 05:01
3

I made changes to https://serverfault.com/a/542580/203373 to fix a couple of compile errors on my system (using Ubuntu linux). I added casts to (struct file_followed*) and (char*), and included IN_MODIFY in the add watch list to watch for modifications to current files. Added this line:

if(event->mask & IN_MODIFY) { fileMod(event->name, file_list); }

and the fileMod function

void fileMod(char* fileName, struct file_followed* file_list)

to check if a modified file was truncated, and print out if it had been as well as update item->last_position = -1so that it reprints the file.

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/inotify.h>
#include <sys/stat.h>
#include <signal.h>
#include <dirent.h>
#include <linux/limits.h>
#define    CHAR_BACK   500

// * File handler structure
struct file_followed { long last_position; char filename[NAME_MAX]; struct file_followed * next; };
struct file_followed * file_list = NULL;

// * To quit peacefully
int cycle = 1;
void stopCycle(int u) { cycle = 0; }

// * Last tailed filename
char last_tailed[NAME_MAX];

void fileAdd(char * file) {
    struct file_followed ** list = &file_list;
    struct stat statdesc;

    if(stat(file, &statdesc) || !S_ISREG(statdesc.st_mode)) { return; }
    while(*list) { list = &((*list)->next); }
    *list = (struct file_followed*)malloc(sizeof(struct file_followed));
    (*list)->last_position = -1;
    strcpy((*list)->filename, file);
    (*list)->next = NULL;
}

void fileMod(char* fileName, struct file_followed* file_list) {
    struct file_followed* item = file_list;
    while(item) { 
        if(strcmp(item->filename, fileName) == 0) {
            FILE* fp = fopen(item->filename, "r");
            fseek(fp, 0, SEEK_END);
            long end_position = ftell(fp);
            fclose(fp);
            if (end_position <= item->last_position) {
                printf("\n** %s truncated **\n", fileName);
                item->last_position = -1;
            }
            usleep(100);
            return;
        }
        item = item->next;
    }
}

int fileTail(struct file_followed * item) {
    int ret = 0;
    FILE * fp = fopen(item->filename, "r");
    fseek(fp, 0, SEEK_END);
    long end_position = ftell(fp);

    if( end_position != item->last_position ) {
        if(strcmp(item->filename, last_tailed)) { strcpy(last_tailed, item->filename); printf("\n** %s **:\n", item->filename); }

        int start_position = item->last_position == -1 || item->last_position > end_position ? (end_position-CHAR_BACK > 0 ? end_position-CHAR_BACK : 0) : item->last_position;
                    fseek(fp, start_position, SEEK_SET);

        int len = end_position - start_position;
        char * buf = (char*)malloc(len+1);
        fread(buf, len, 1, fp);
        buf[len] = '\0';
        printf("%s%s", len == CHAR_BACK ? "[...]" : "", buf);
        free(buf);

        item->last_position = end_position;
        ret = 1;
    }

    fclose(fp);
    return ret;
}

void fileRem(char * file) {
    struct file_followed ** list = &file_list;
    while(*list && strcmp((*list)->filename, file)) { list = &((*list)->next); }
    if(*list) { struct file_followed * todel = *list; *list = (*list)->next; free(todel); }
}

int main(int argc, char ** argv) {

    struct dirent **namelist;
    struct stat statdesc;
    struct timeval tv;
    fd_set set;
    int fd;
    int wd;
    int r;

    // * Help
    if(stat(argv[1], &statdesc) || !S_ISDIR(statdesc.st_mode)) { printf("[usage] %s dir-to-monitor\n", argv[0]); exit(EXIT_FAILURE); }

    // * Init
    chdir(argv[1]);
    memset(last_tailed, 0, sizeof(last_tailed));
    signal(SIGINT, stopCycle);
    signal(SIGTERM, stopCycle);

    // * Inotify
    if( (fd = inotify_init()) < 0) { perror("inotify_init"); }
    if( (wd = inotify_add_watch( fd, ".", IN_CREATE | IN_MODIFY |IN_DELETE ) < 0)) { perror("inotify_add_watch"); }

    // * File add recursively on dirscan
    if( (r = scandir(".", &namelist, 0, alphasort)) < 0) { perror("scandir"); }
    while (r--) { fileAdd(namelist[r]->d_name); free(namelist[r]); }
    free(namelist);

    // * Neverending cycle
    while(cycle) {
        // * Select on inotify
        FD_ZERO(&set);
        FD_SET(fd, &set);
        tv.tv_sec = 0;
        tv.tv_usec = 1000;
        if( (r = select(fd+1, &set, NULL, NULL, &tv)) == -1) { perror("select"); }

        // * New add or del on inotify
        if(r) {
            struct inotify_event * event;
            char buf[1024];
            if(read(fd, buf, 1024) <= 0) { perror("read"); }
            event = (struct inotify_event *) buf;
            if(event->mask & IN_MODIFY) { fileMod(event->name, file_list);} 
            else if(event->mask & IN_CREATE) { fileAdd(event->name); } 
            else if(event->mask & IN_DELETE) { fileRem(event->name); }
        }

        // * Check for new tails
        struct file_followed * list = file_list;
        int tailers = 0;
        while(list) { tailers += fileTail(list); list = list->next; }
        if(!tailers) { usleep(500000); }
    }

    // * Stop inotify
    inotify_rm_watch( fd, wd );
    close(fd);

    return EXIT_SUCCESS;
}
ekangas
  • 131
  • 4
  • I've been doing a lot of programming problems on https://uva.onlinejudge.org/ and was looking for a tool which could tail all my output files in an output folder including new ones, and update when they truncate. This seems to be the best solution for that. – ekangas Apr 03 '17 at 09:58
1

I don't think there is a way with using only tail, but you should be able to achieve the same effect using watch together with tail. The only danger then becomes making sure you don't get a directory created instead of just a new file, which can be mitigated by making sure you use an appropriate shell glob passed to tail. Example: watch -n 2 tail *.log

John
  • 8,920
  • 1
  • 28
  • 34
  • 1
    Yes, I don't think there is a way with tail. But watch is far distant from what I'm looking for – Jack Sep 26 '13 at 14:00
  • I have used `watch` for several similar tasks and unfortunately it falls short. I think the biggest deficiency is my knowledge because I always seem to get errors where the command line seems to look good to me. – Madivad May 15 '16 at 11:22
0

This thread inspired me to write my own version of the retail script. My version of retail works cross-platform, including on macos.

Aaron Iba
  • 101
  • 1
0

You could use: tail -F logs/*

Bonus tip: Check out multitail, it's a great little command.

For example: Merge ALL apache logfiles (*access_log/*error_log) into one window:

multitail -cS apache --mergeall /var/log/apache2/*access_log --no-mergeall \  
  -cS apache_error --mergeall /var/log/apache2/*error_log --no-mergeall

Show 5 logfiles while merging 2 and put them in 2 columns with only one in the left column:

multitail -s 2 -sn 1,3  /var/log/apache/access.log -I /var/log/apache/error.log \ 
  /var/log/messages /var/log/mail.log /var/log/syslog
rafi
  • 119
  • 3
  • Thanks for multitail, it's a great tool. Anyway none has still helped with original question. When you do "tail -F logs/*" shell expands the star with all files present currently in logs/; hence the file that will be created after the command is issued will not be read by tail – Jack Sep 27 '13 at 14:39
  • I have to add, I LOVE `multitail`, and is one of the first tools I install on any new system. Especially for all the `/var/log` files that you can want to monitor. But as with every tool that currently exists, there is no way to monitor NEW files that get created AFTER invocation. Except for @Jack 's post above as he accepted answer – Madivad May 15 '16 at 11:25
0

You might be able to use something like multitail to tail multiple files at the same time. If you combine that with the -F option (follow with retry, in case the file doesn't exist) it could get you what you're looking for.

Bob
  • 726
  • 4
  • 4