Ведение журнала совместимо с logrotate - PullRequest
0 голосов
/ 07 ноября 2018

Я пишу демон Linux, который пишет журнал. Я бы хотел, чтобы журнал вращался с помощью logrotate. Программа написана на языке C.

Обычно моя программа открывает файл журнала при запуске, затем записывает записи по мере необходимости и, наконец, закрывает файл журнала при выходе.

Что мне нужно сделать по-другому, чтобы поддерживать ротацию журналов с помощью logrotate? Насколько я понял, моя программа должна иметь возможность открывать файл журнала каждый раз, когда logrotate завершает свою работу. Источники, которые я гуглил, однако, не указывали, что именно означает повторное открытие файла журнала. Нужно ли что-то делать со старым файлом, и могу ли я просто создать другой файл с тем же именем? Я бы предпочел довольно конкретные инструкции, например, простой пример кода.

Я также понял, что должен быть способ сообщить моей программе, когда пришло время повторного открытия. Моя программа уже имеет интерфейс D-Bus, и я подумал об использовании этого для этих уведомлений.

Примечание: мне не нужны инструкции по настройке logrotate. Этот вопрос только о том, как сделать мое собственное программное обеспечение совместимым с ним.

Ответы [ 3 ]

0 голосов
/ 07 ноября 2018

Есть несколько распространенных способов:

  1. вы используете logrotate, и ваша программа должна быть в состоянии перехватить сигнал (обычно SIGHUP) как запрос на закрытие и повторное открытие своего файла журнала. Затем logrotate отправляет сигнал в постротационном скрипте
  2. вы используете logrotate, и ваша программа не знает об этом, но может быть перезапущена. Затем logrotate перезапускает вашу программу в постротационном скрипте. Минусы: если запуск программы стоит дорого, это может быть неоптимальным
  3. вы используете logrotate, и ваша программа не знает об этом, но вы передаете опцию copytruncate в logrotate. Затем logrotate копирует файл и затем обрезает его. Минусы: в условиях гонки вы можете потерять сообщения. С rotatelog.conf manpage

    ... Обратите внимание, что между копированием файла и его усечением существует очень маленький промежуток времени, поэтому некоторые данные журналов могут быть потеряны ...

  4. вы используете rotatelogs, утилиту для httpd Apache. Вместо того, чтобы записывать напрямую в файл, вы программируете каналы его журналов в rotatelogs. Затем rotatelogs управляет различными файлами журнала. Минусы: ваша программа должна иметь возможность войти в канал, или вам нужно будет установить имя fifo.

Но будьте осторожны, для критических журналов может быть интересно закрыть файлы после каждого сообщения, потому что это гарантирует, что все достигло диска в случае сбоя приложения.

0 голосов
/ 08 ноября 2018

Хотя в примерах man logrotate используется сигнал HUP, я рекомендую использовать USR1 или USR2, так как обычно используется HUP для «перезагрузки конфигурации». Таким образом, в файле конфигурации logrotate у вас будет, например,

/var/log/yourapp/log {
    rotate 7
    weekly
    postrotate
        /usr/bin/killall -USR1 yourapp
    endscript
}

Хитрый бит для обработки случая, когда сигнал приходит в середине регистрации. Тот факт, что ни один из блокирующих примитивов (кроме sem_post(), который здесь не помогает) не является безопасным для асинхронного сигнала , делает этот вопрос интересным.

Самый простой способ сделать это - использовать выделенный поток, ожидающий в sigwaitinfo(), с сигналом, заблокированным во всех потоках. Во время выхода процесс отправляет сам сигнал и присоединяется к выделенному потоку. Например,

#define  ROTATE_SIGNAL  SIGUSR1

static pthread_t        log_thread;
static pthread_mutex_t  log_lock = PTHREAD_MUTEX_INITIALIZER;
static char            *log_path = NULL;
static FILE *volatile   log_file = NULL;

int log(const char *format, ...)
{
    va_list  args;
    int      retval;

    if (!format)
        return -1;
    if (!*format)
        return 0;

    va_start(args, format);
    pthread_mutex_lock(&log_lock);
    if (!log_file)
        return -1;
    retval = vfprintf(log_file, format, args);
    pthread_mutex_unlock(&log_lock);
    va_end(args);

    return retval;
}

void *log_sighandler(void *unused)
{
    siginfo_t info;
    sigset_t  sigs;
    int       signum;

    sigemptyset(&sigs);
    sigaddset(&sigs, ROTATE_SIGNAL);

    while (1) {

        signum = sigwaitinfo(&sigs, &info);
        if (signum != ROTATE_SIGNAL)
            continue;

        /* Sent by this process itself, for exiting? */
        if (info.si_pid == getpid())
            break;

        pthread_mutex_lock(&log_lock);
        if (log_file) {
            fflush(log_file);
            fclose(log_file);
            log_file = NULL;
        }
        if (log_path) {
            log_file = fopen(log_path, "a");
        }
        pthread_mutex_unlock(&log_lock);
    }

    /* Close time. */
    pthread_mutex_lock(&log_lock);
    if (log_file) {
        fflush(log_file);
        fclose(log_file);
        log_file = NULL;
    }
    pthread_mutex_unlock(&log_lock);

    return NULL;
}

/* Initialize logging to the specified path.
   Returns 0 if successful, errno otherwise. */
int log_init(const char *path)
{
    sigset_t          sigs;
    pthread_attr_t    attrs;
    int               retval;

    /* Block the rotate signal in all threads. */
    sigemptyset(&sigs);
    sigaddset(&sigs, ROTATE_SIGNAL);
    pthread_sigmask(SIG_BLOCK, &sigs, NULL);

    /* Open the log file. Since this is in the main thread,
       before the rotate signal thread, no need to use log_lock. */
    if (log_file) {
        /* You're using this wrong. */
        fflush(log_file);
        fclose(log_file);
    }
    log_file = fopen(path, "a");
    if (!log_file)
        return errno;

    log_path = strdup(path);

    /* Create a thread to handle the rotate signal, with a tiny stack. */
    pthread_attr_init(&attrs);
    pthread_attr_setstacksize(65536);
    retval = pthread_create(&log_thread, &attrs, log_sighandler, NULL);
    pthread_attr_destroy(&attrs);
    if (retval)
        return errno = retval;

    return 0;       
}

void log_done(void)
{
    pthread_kill(log_thread, ROTATE_SIGNAL);
    pthread_join(log_thread, NULL);
    free(log_path);
    log_path = NULL;
}

Идея состоит в том, что в main() перед входом в систему или созданием любых других потоков вы вызываете log_init(path-to-log-file), отмечая, что копия пути к файлу журнала сохраняется. Он устанавливает маску сигнала (наследуется любыми потоками, которые вы можете создать) и создает вспомогательный поток. Перед выходом звоните log_done(). Чтобы записать что-то в файл журнала, используйте log(), как если бы вы использовали printf().

Я бы лично добавил метку времени перед строкой vfprintf(), автоматически:

    struct timespec  ts;
    struct tm        tm;

    if (clock_gettime(CLOCK_REALTIME, &ts) == 0 &&
        localtime_r(&(ts.tv_sec), &tm) == &tm)
        fprintf(log_file, "%04d-%02d-%02d %02d:%02d:%02d.%03ld: ",
                          tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
                          tm.tm_hour, tm.tm_min, tm.tm_sec,
                          ts.tv_nsec / 1000000L);

Этот формат YYYY-MM-DD HH:MM:SS.sss имеет то преимущество, что он близок к мировому стандарту ( ISO 8601 ) и сортирует в правильном порядке.

0 голосов
/ 07 ноября 2018

Обычно моя программа открывает файл журнала при запуске, затем при необходимости запишите записи, а затем закройте файл журнала при выходе.

Что мне нужно сделать по-другому, чтобы поддерживать ротацию логов используя logrotate?

Нет, ваша программа должна работать так, как будто она ничего не знает о logrotate.

Нужно ли что-то делать со старым файлом, и могу ли я просто создать другой файл с тем же именем?

Нет. Должен быть только один файл журнала, который нужно открыть и записать. Logrotate проверит этот файл и, если он станет слишком большим, скопирует / сохранит старую часть и урежет текущий файл журнала. Следовательно, ваша программа должна работать полностью прозрачно - ей не нужно ничего знать о logrotate.

...