LINUX.ORG.RU

Форматирование даты-времени для лога: strftime не работает с mingw?

 , , , ,


0

2

Всех приветствую!

Необходимо выводить дату-время в лог-файл.

char* get_datetime_str_alt1(char *str_datetime)
{
    char buff[64]={'\0'};

    struct timespec ts;
    clock_gettime(CLOCK_REALTIME, &ts);
  
    strftime(buff, sizeof(buff), "%e-%b-%Y %Z %T", localtime(&ts.tv_sec));
    sprintf(str_datetime, "%s", buff);
       
    return str_datetime;
}


char* get_datetime_str_alt2(char *str_datetime)
{
    char buff[64]={'\0'};
       
    time_t t = time(NULL);
    struct tm *local = localtime(&t);
  
    strftime(buff, sizeof(buff), "%e-%b-%Y %Z %H:%M:%S", local);
    sprintf(str_datetime, "%s", buff);
       
    return str_datetime;
}

Эти два варианта работают после gcc

1-Aug-2024 MSK 12:32:45: Start!
1-Aug-2024 MSK 12:32:45: Bla-bla
1-Aug-2024 MSK 12:32:46: End!

, но ничего не выводят после mingw:

: Start!
: Bla-bla
: End!

Работает отлично только

char* get_datetime_str(char *str_datetime)
{
    time_t t = time(NULL);
        
    sprintf(str_datetime, "%s", ctime(&t) );
    char *p = strchr(str_datetime, '\n');
    *p = '\0';
 
    return str_datetime;
}

Но форматирование уже выбираю не я:

Thu Aug 01 12:13:08 2024: Start!
Thu Aug 01 12:13:08 2024: Bla-bla
Thu Aug 01 12:13:09 2024: End!

По идее и так работает. Но все таки какая-то неудовлетворенность остается. Сталкивался кто-нибудь с подобным эффектом?

Подозреваю нужно обновлять mingw, но так не хочется: столько библиотек опять устанавливать (к тому же я уже забыл как это делал).

PS gcc (Ubuntu 9.4.0-1ubuntu1~20.04.2) 9.4.0

x86_64-w64-mingw32-gcc (GCC) 9.3-win32 20200320 wincrt


Кстати еще вопрос вдогонку.

А как лучше выводить в лог: открыл файл, записал, закрыл?

Или держать в памяти дескриптор файла, а закрывать его только в самом конце работы программы или при сбое?

Gyros
() автор топика
Ответ на: комментарий от Gyros

А как лучше выводить в лог: открыл файл, записал, закрыл?

Так никто не делает. Запись в лог и так может стать узким местом, а если ты на каждую будешь ещё в ядро два раза ходить и делать там всю машинерию по открытию-закрытию файла, станет ещё тяжелее.

anonymous
()

Кстати, функция весьма неторопливая. Мы для ускорения логирования кешируем эту строчку и заново ее генерим при смене секунды.

vromanov ★★★
()
Ответ на: комментарий от anonymous

Вроде бы таже самая функция, что и у меня.

Но я понял в чем дело: из-за %Z возникает UB.

Может черти что вывести или ничего не вывести.

Без %Z все работает одинаково.

(получается %Z не реализовали в mingw)

C %Z выдавало бы еще пояс: MSK.

Gyros
() автор топика
Последнее исправление: Gyros (всего исправлений: 4)
Ответ на: комментарий от Gyros
$ man 3 strftime

DESCRIPTION

    %Z     The timezone name or abbreviation.

RETURN VALUE

    If the length of the result string (including the terminating null byte) would exceed max bytes, then strftime() returns 0, and the  contents  of  the array are undefined.

Там может быть не только абревиатура на полное название часового пояса, возможно даже на русском в UTF-8, например (выдумано): «Летнее время Москва, Ярославль, Сочи, Тегеран, Дели, Сидней». Влезет в 64 байт?

anonymous
()
Ответ на: комментарий от Gyros

Поведение определенное (задокументированное). Если строка не влезает в буфер, функция возвращает 0 и содержимое буфера неопределено. Ты результат strftime не проверяешь.

anonymous
()
Ответ на: комментарий от anonymous

Поставил %Z. Увеличил буфер до 512.

 size_t c = strftime(buff, 512, "%d/%m/%Y %Z %H:%M:%S", local);

gcc;
c = 23

[INFO]: [01/08/2024 MSK 15:33:06]: Start! 
[INFO]: [01/08/2024 MSK 15:33:06]: Bla-bla 
[INFO]: [01/08/2024 MSK 15:33:07]: Done.

mingw;
c = 38

[INFO]: [01/08/2024 Arab Standard Time 15[INFO]: []: Start! 
[INFO]: [01/08/2024 Arab Standard Time 15[INFO]: []: Bla-bla 
[INFO]: [01/08/2024 Arab Standard Time 15[INFO]: []: Done.
Gyros
() автор топика
Последнее исправление: Gyros (всего исправлений: 1)
Ответ на: комментарий от Gyros

В приведенном куске кода размер буфера равен магическому числу 512. Это магическое число где нибудь еще используется, например, при указании размера буфера? Или используется другое магическое число 64?

anonymous
()
Ответ на: комментарий от anonymous

Я понял, у меня в ф-ции, пишущей в лог, маленькие размеры буферов стояли. Теперь исправил

void log_print(LOG *log, enum msg_type type, const char* msg)
{
    if (log->f)
    {
        //char log_str[LITTLE_SIZE]={'\0'};
        //char datetime[TINY_SIZE] ={'\0'};
        char log_str[1024]={'\0'};
        char datetime[512] ={'\0'};
        sprintf(log_str, "[%s]: [%s]: %s \n", get_type_msg(type), get_datetime_str_alt2(datetime), msg);
        fputs(log_str, log->f);
    }
}

Стало корректно выдавать (после mingw)

[INFO]: [01/08/2024 Arab Standard Time 16:03:21]: Start! 
[INFO]: [01/08/2024 Arab Standard Time 16:03:21]: Bla-bla 
[INFO]: [01/08/2024 Arab Standard Time 16:03:23]: Done.

Arab Standard Time - это наверное из-за настроек часового пояса wine? Стоит первое по алфавиту.

Gyros
() автор топика
Последнее исправление: Gyros (всего исправлений: 1)
Ответ на: комментарий от Gyros

Лучше использовать «%z» (маленькая z), как разницу от UTC «%H:%M:%S %z», с предсказуемым результатом не зависящим от хитрого имении часового пояса

anonymous
()
Ответ на: комментарий от Gyros

sprintf лишний же, почему не просто fprintf? И через snprintf можно получить размер буфера который нужен для итоговой строки.

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

MOPKOBKA ★★★★
()
Последнее исправление: MOPKOBKA (всего исправлений: 2)
Ответ на: комментарий от MOPKOBKA
void log_print(LOG *log, enum msg_type type, const char* msg)
{
    if (log->f)
    {
        const char fmt[]="[%s]: [%s]: %s \n";
        char datetime[512] ={'\0'};
        int sz = snprintf(NULL, 0, fmt, get_type_msg(type), get_datetime_str_alt2(datetime), msg);
        char log_str[sz + 1];
	snprintf(log_str, sizeof(log_str), fmt,  get_type_msg(type), get_datetime_str_alt2(datetime), msg);
        fputs(log_str, log->f);
    }
}

Так?

Или

void log_print(LOG *log, enum msg_type type, const char* msg)
{
    if (log->f)
    {
       
        const char fmt[]="[%s]: [%s]: %s \n";
	char log_str[1024] ={'\0'};
        char datetime[512] ={'\0'};
       	fprintf(log->f, fmt, get_type_msg(type), get_datetime_str_alt2(datetime), msg);
    }

}

Оба рабочие.

Что-то мне второй вариант нравиться больше.

Он короче и нет повторов кода.

Gyros
() автор топика
Ответ на: комментарий от Gyros

Так зачем вам фиксированные буферы вообще? Вы как только отправили его в какую-то функцию - всё, считайте что у вас дыра. Потому что вы потеряли информацию о размере и не можете использовать sNprintf.

PPP328 ★★★★★
()
Ответ на: комментарий от PPP328

А не фиксированные буферы - это тогда мне malloc-ом надо пользоваться? Или snprintf-ами заранее узнавать небходимый размер?

Вот тут как я заранее узнаю размер buff?

    char buff[512]={'\0'};
       
    time_t t = time(NULL);
    struct tm *local = localtime(&t);
  
    size_t c = strftime(buff, 512, "%d/%m/%Y %H:%M:%S %Z", local);
  
    sprintf(str_datetime, "%s", buff);
Gyros
() автор топика
Последнее исправление: Gyros (всего исправлений: 1)
Ответ на: комментарий от Gyros

А не фиксированные буферы - это тогда мне malloc-ом надо пользоваться?

Нет, это когда вы пользуете напрямую fprintf с форматтером например «%02u:%02u:%02u» а потом передаете ему local.tm_hour, local.tm_min, local.tm_sec, вообще без аллокаций и буферов на вашей стороне.

PPP328 ★★★★★
()
Последнее исправление: PPP328 (всего исправлений: 1)

в журнале неплохо выводить время в стандартном виде, есть такое ISO8601, и с достаточной точностью. Миллисекунды типично просятся, секунд не всегда хватает.

в зависимости от целей журнала, бывает невредно сразу приводить в UTC

Читается чуть хуже чем «из-головы взятое», зато стандартно и не зависит от локалей с личными тараканами, и не надо никому ничего объяснять. 01-08-2024T17:08:50.200Z вполне даже ничего

MKuznetsov ★★★★★
()
Ответ на: комментарий от Gyros

В первом варианте ты сначала пишешь в буфер, потом в файл, во втором у тебя нету лишней записи в буфер, конечно он лучше. Следующий шаг, убрать ненужную инициализацию нулями, и удалить log_str, он же нигде не используется.

Выше тоже хорошее улучшение подсказали по strftime, вообще fprintf более универсален в linux, где можно любую память, буфер превратить в FILE*, но это решение не кроссплатформенно.

MOPKOBKA ★★★★
()
Последнее исправление: MOPKOBKA (всего исправлений: 2)
Ответ на: комментарий от PPP328

Получилось так:

void log_print(LOG *log, enum msg_type type, const char* msg)
{
    if (log->f)
    {
        time_t t = time(NULL);
        struct tm *lt = localtime(&t);
        const char fmt[]="[%s]: [%2d/%2d/%4d  %2d:%2d:%2d]: %s \n";
        fprintf(log->f, fmt, get_type_msg(type), lt->tm_mday, lt->tm_mon+1, lt->tm_year+1900, lt->tm_hour, lt->tm_min, lt->tm_sec, msg);        
    }
}

Вывод

[INFO]: [ 2/ 8/2024   0: 2:32]: Start! 
[INFO]: [ 2/ 8/2024   0: 2:32]: Bla-bla 
[INFO]: [ 2/ 8/2024   0: 2:33]: Done.

Вы так имели ввиду?

Gyros
() автор топика
Ответ на: комментарий от PPP328

Получилось отлично. Большое спасибо!

Но сразу так кратко (и без ф-ции strftime) было мне не очевидно. И да, мне миллисекунды не нужны, но этим способом миллисекунды не получишь.

PS Пробел в конце - опечатка.

Gyros
() автор топика

когда ты работаешь с виндой, выкинь нахер из головы все свои практики из юниксов, и практически забудь про стандартную библиотеку что сишечки что плюсов. Просто забудь.

Используй WinAPI, там всё есть, и гораздо более лучше и продуманно сделано.

lovesan ★★
()
Ответ на: комментарий от lovesan

Проблема в том что авторы MinGW, а до этого еще был такой Cygwin, они пытаются натянуть сову на глобус. Но сова на глобус натягивается херово. И вместо того чтобы вот страдать этим костным ригидным мышлением, всяким юникс-веем и прочим, изучи систему под которую ты пишешь, в данном случае винду, и используй ее нормально, а не через все эти прослойки черезжопные.

lovesan ★★
()
Ответ на: комментарий от lovesan

Так мне нужно и под Windows, и под Linux получать исполняемые файлы. Я хочу написать один и тот же код (+/- #ifdef WINDOWS …) и компилировать сразу для двух платформ, а не писать отдельно еще программу только для Windows.

Gyros
() автор топика
Последнее исправление: Gyros (всего исправлений: 1)
Ответ на: комментарий от Gyros

Не благодари.

Засунешь в #ifdef

Тут поправлены три важных момента которые у тебя сделаны как делать не надо.

Первое, время всегда надо брать в UTC, для логов - особенно.

Второе - про форматирование таймзон вообще забудь, кроме как где-то на UI, т.к. они не совсем стандартизированы, тем более на разных ОС. Таймзоны это вообще сложно, лучше туда не лезь особенно для таких задач как логи.

Третий - язык/локаль надо выставлять явно и полагаться на какие-то там глобальные состояния locale() нельзя. По дефолту язык должен всегда быть Neutral, особенно для логов. В винде это относительно нейтральный английский основанный на американском английском.

линуксовую реализацию поправь соответственно.

Отдельный вопрос про файлы, в любом случае нельзя просто так брать и срать в логи синхронными операциями. Операции должны быть асинхронные, в винде это отдельная тема, отличающаяся от других систем. Там и APC, и всякие IOCP и так далее. Обертки над этим всем нормально сделаны в дотнете, скажем, тасками. Впрочем там и с локализацией и датами и всем остальным тоже все нормально переносимо сделано. На какой хер писать на си в 2024 - непонятно.

#include <windows.h>

// Appends "dd-MMM-yyyy HH:mm:ss UTC"-formatted current UTC time to the "buffer" but DOES NOT append a null terminator.
//
// Returns either the "buffer" pointer advanced to a number of characters appended, or NULL on error.
//
// "chars_written" pointer is optional, and can hold a number of appended characters on return.
//
// In case of "buffer_size" is insufficient to hold the formatted system time, the function fails
//   and the value of "chars_written" holds the required buffer size.
char* append_standard_date_time_format(char* buffer, size_t buffer_size, /* out, optional */ int* chars_written)
{
    SYSTEMTIME sysTime;
    const wchar_t dateFormat[] = L"dd-MMM-yyyy "; // use 'dd' instead of 'd' because you want consistency in case of string length
    const wchar_t timeFormat[] = L"HH:mm:ss UTC";
    int dateCch, timeCch, lenCch = ARRAYSIZE(dateFormat) + ARRAYSIZE(timeFormat) - 2;
    wchar_t tmpBuff[ARRAYSIZE(dateFormat) + ARRAYSIZE(timeFormat) - 1];

    if (chars_written)
        *chars_written = 0;

    if (buffer_size < (size_t)lenCch) // do not include null terminators
    {
        if (chars_written)
            *chars_written = lenCch;
        return NULL; // insufficient buffer
    }

    // UTC. Because you only want UTC for logs.
    GetSystemTime(&sysTime);

    // write date to temporary buffer
    dateCch = GetDateFormatEx(
        LOCALE_NAME_INVARIANT, // because you only want neutral US-based english in logs
        0,
        &sysTime,
        dateFormat,
        tmpBuff,
        ARRAYSIZE(dateFormat),
        NULL);

    if (dateCch != ARRAYSIZE(dateFormat))
        return NULL; // some unknown error

    // write time to temporary buffer
    timeCch = GetTimeFormatEx(
        LOCALE_NAME_INVARIANT,
        0,
        &sysTime,
        timeFormat,
        tmpBuff + ARRAYSIZE(dateFormat) - 1,
        ARRAYSIZE(timeFormat));

    if (timeCch != ARRAYSIZE(timeFormat))
        return NULL; // unknown error

    // only use UTF-8 or ASCII for logs
    lenCch = WideCharToMultiByte(
        20127, // US ASCII
        0,
        tmpBuff,
        lenCch,
        buffer,
        lenCch,
        NULL,
        NULL);
    if (lenCch != (ARRAYSIZE(dateFormat) + ARRAYSIZE(timeFormat) - 2))
        return NULL; // unknown error

    if (chars_written)
        *chars_written = lenCch;

    return buffer + lenCch;
}
lovesan ★★
()
Ответ на: комментарий от PPP328

Вот вариант с миллисекундами: (Диапазон изменения миллисекунд от 0 до 999)

void log_print_ms(LOG *log, enum msg_type type, const char* msg)
{
    if (log->f)
    {
       	char str_datetime[64]={'\0'};
        const char fmt[]="[%s]: [%s:%d]: %s\n";
        
        struct timespec ts;
        clock_gettime(CLOCK_REALTIME, &ts);
        
        strftime(str_datetime, sizeof(str_datetime), "%d/%m/%Y %H:%M:%S", localtime(&ts.tv_sec));
        
        fprintf(log->f, fmt, get_type_msg(type), str_datetime, (int)(1e-6*ts.tv_nsec),  msg); // nsec-> msec       
    }
}

Вывод:

[INFO]: [02/08/2024 14:42:23:654]: Start!
[INFO]: [02/08/2024 14:42:23:654]: Bla-bla
[INFO]: [02/08/2024 14:42:24:297]: Done.

Еще для извлечения миллисекунд, наверное, можно использовать (попробовал повозиться - там с типами несовпадения какие-то)

    struct timeval tv;
    gettimeofday(&tv, NULL);

Вариант с

timespec_get(&tp, TIME_UTC);

не компилируется в mingw wincrt

Gyros
() автор топика
Ответ на: комментарий от lovesan

Не благодари.

Засунешь в #ifdef

алаверды !

лучше сразу засунуть в мусорку...

так делать нельзя, отчасти потому и «нет кроссплатформы на Си» :-)

нельзя использовать wchar_t при записи журналов (чувствительных данных, баз,сетевого обмена и прочего) ни при каким условиях. Вообще лучше его не использовать, он сцуко везде разный. Просто в windows от него сложно избавится. За литералы L"сторка" надо держать строгий ответ перед всеми причастными. Только для гуя и старого dbf в офтопике.

MKuznetsov ★★★★★
()
Ответ на: комментарий от MKuznetsov

Ого, подъехали эксперты по винде, которые видели ее последний раз лет 20 назад.

Это не у винды проблема в «undefined/unspecified behaviour», а у юникс-систем, где разброд и шатание, и ничего конкретно заданного нет. В том числе представления строк. И даже кодировки путей файлов.

wchar_t в винде четко и конкретно специфицирован. Это 16-битное беззнаковое целое число. Все строки, которые используют wchar_t, или WCHAR или подобные typedef-ы, винда считает кодирующимися в UTF-16/LE. Эта кодировка является компромиссом между занимаемым местом и способностью отображать буквы без дополнительного дрочева(как в UTF-8), так как весь Unicode BPM(Basic Multilingual Plane) в нем влезает в 16 бит(то что дальше кодируется суррогатными парами, но это редко используемые буквы). (Отсюда же идет внутреннее представление строк в JVM или .NET, которое там точно такое же.).

Литералы L - точно так же, на винде, во всех сколько-нибудь вменяемых компиляторах Си и крестов, точно так же вот это вот и означают. Даже если файл у тебя в UTF-8, после компиляции литерал превращается в UTF-16/LE.

Далее, это не какая-то непонятная(красноглазым разве что) хреновина от которой «сцуко сложно избавиться». У Windows - четко специфицированная работа со строковыми данными. Вся система - внутри использует юникод, вышеуказанную кодировку. Даже если ты используешь кривые обертки типа MinGW, или *A функции(CreateFileA итп), внутри, эти функции ВСЕ-РАВНО переводят строки в UTF-16/LE, и вызывают соответствующие функции WinAPI, которые используют юникод.

Поэтому, на винде всегда имеет смысл работать только с UTF-16/LE когда мы говорим о сишечке и о внутреннем представлении строк в программе(впрочем как я уже сказал, те же JVM и .NET тоже используют UTF-16/LE). Чтобы не конвертировать 100500 раз туда-сюда. И чтобы было конкретно понятно что за кодировка. Поэтому я в том числе и говорю, что забудьте к херам о какой-то там мифической кроссплатформенности. Писать код, особенно на таком низкоуровневом говне как Си, надо отдельно и конкретно под каждую систему. Но красноглазым не понять, видимо.

Хранение текста вне памяти это другой вопрос. Там имеет смысл UTF-8.

lovesan ★★
()
Ответ на: комментарий от Gyros

Кстати, в догонку к вышеуказанному моему посту.

То что ты имеешь ввиду и чего пытаешься добиться, это не «кроссплатформенность». Ты пытаешься использовать «Linux API» на винде. И естественно, у тебя выходит не то что ты задумал. Потому что все такие «Linux API»(ну кроме Interix, который давно помер) на винде реализованы через задницу. Потому что сова на глобус не натягивается. Винда это другая система, не Unix-подобная. И если ты собрался под нее писать, сядь и изучай её.

И то что мы тут затронули, это еще вообще мелочи. Мне даже лень рассказывать, с какой задницей ты столкнешься, если попытаешься делать «кроссплатформенное» GUI, которое на винде выглядит вообще абсолютно чужеродно, часто до сих пор не умеет в DPI, не умеет нормально интегрироваться с оболочкой, со встроенными медиа-платформами(кодеками там), со всякой COM/ActiveX-фиговиной, с DirectX, и так далее. И также, не умеет нормально инсталлироваться и использовать всякие SxS, и прочее и прочее, и так далее и тому подобное.

Кроссплатформенное ПО - это миф. Это просто попытка написать какое-нибудь говно сидя на линуксе, вообще не прилагая усилий к тому чтобы хоть сколько-нибудь изучить другую систему. В результате, естественно, получается вырвиглазное, отвратительное и глючное говно. С «кроссплатформенными» программами и библиотеками на MacOS, кстати(исключая всякое консольное говно, м.б., да и то…), дела обстоят еще хуже, они там выглядят вообще вырвиглазно и оторванно от всего что там находится, а пользователи Apple такого крайне не любят.

Да, есть исключения, такие как какие-нибудь браузеры, но там весь код это сплошной ifdef, а во-вторых, они там переизобретают и стандартную библиотеку(начиная со сраных строк) и все остальное, с нуля, ориентируясь на современные реалии.

lovesan ★★
()
Последнее исправление: lovesan (всего исправлений: 1)
Ответ на: комментарий от lovesan

Отстал от жизни, товарищч!

Ну, что это такое в самом деле!?:

wchar_t в винде четко и конкретно специфицирован. Это 16-битное беззнаковое целое число. Все строки, которые используют wchar_t, или WCHAR или подобные typedef-ы, винда считает кодирующимися в UTF-16/LE. Эта кодировка является компромиссом между занимаемым местом и способностью отображать

Вот как сейчас объясняют:

Данный язык вурдалак/вампир. Он мёртв, но забирает в себя энергетику других. Поэтому он жив!
(типичные рускоговорящие рубинисты)
anonymous
()
Ответ на: комментарий от PPP328

Все время меня тянет использовать strftime.

Вот ф-ция вывода сообщения (с миллисекундами) в лог-файл без strftime:

void log_print_ms(LOG *log, enum msg_type type, const char* msg)
{
    if (log->f)
    {
       	struct timespec ts;
        clock_gettime(CLOCK_REALTIME, &ts);
	struct tm *lt = localtime(&ts.tv_sec);
        const char fmt[]="[%s]: [%02d/%02d/%4d  %02d:%02d:%02d:%03d] %s\n";       
        fprintf(log->f, fmt, get_type_msg(type), lt->tm_mday, lt->tm_mon+1, lt->tm_year+1900, lt->tm_hour, lt->tm_min, lt->tm_sec, (int)(1e-6*ts.tv_nsec), msg); 
    }
}

Макрос для вывода сразу и на экран, и в лог-файл:

#define PRINT_MSG(type_msg, log, msg) { const char fmt[]="%s";                         \
                                     int sz = snprintf(NULL, 0, fmt, msg);             \
				     char message[sz];                                 \
				     snprintf(message, sz, msg);                       \
				     if (ERROR_MSG==type_msg || FATAL_MSG==type_msg)   \
					fprintf(stderr, "%s\n", message);              \
				     else                                              \
                                        printf("%s\n", message);                       \
                                     if (log !=NULL) if (log.f) log_print_ms(log, type_msg, message); }

Или лучше не макрос, а функцию сделать?

Самв структура лог-файла:

enum msg_type
     {
       DEBUG_MSG,    // debug message
       INFO_MSG,     // info message
       WARNING_MSG,  // warning message
       ERROR_MSG,    // error message
       FATAL_MSG     // critical message
     };

typedef struct LOG
{
    FILE  *f;
    char file_name[TINY_SIZE];
} LOG;

void log_print   (LOG *log, enum msg_type type, const char* msg);
void log_print_ms(LOG *log, enum msg_type type, const char* msg);
void log_init(LOG *log, const char* fileName);
void log_close(LOG *log);
Gyros
() автор топика
Последнее исправление: Gyros (всего исправлений: 2)
Ответ на: комментарий от Gyros
  1. Выглядит так, как будто вы в chatgpt промпт засунули и оно вам вот это высрало. Не делайте так.
  2. Вам нужно осознать, что такое макрос в принципе. Это не какая-то транспилятивная единица, это текстовая подстановка. Вы использовали макрос для лога. Это не просто плохо, это плохо втройне:
    1. Потому что сам по себе вызов макроса в исходнике является одной строкой. Поэтому его отладка невозможна с помощью стандартных средств.
    2. Вы выделяете память на стеке блоком в макросе. В макросе, который может быть вызван неограниченное количество раз в функции. Стек ограничен 1-8 МБ. Сами осознаете, что будет, когда вы исчерпаете стек?
    3. Макрос - это подстановка. Вы в каждом месте вызова лога подставляете вот всю эту абракадабру. Без оптимизации. Т.е. Если у вас в функции написано
log(1);
do(1);
log(2);
do(2);
log(3);

то после компиляции длина этого блока будет 29 строк. Из которых 3 нельзя отладить.

  1. Вам не нужны условия в проверке в какой поток писать - stderr или stdout, если вы хотите переиспользовать код для записи в файл. Потому что stdout и stderr - это FILE.
  2. Вы собираетесь делать открытие и закрытие файла вручную в начале и конце программы. Это плохо. Потому что flush вызывается только в close, что означает, что пока вы не закроете файл, все что туда написали - пропадет в случае неожиданного завершения (SIGSEGV, _Exit, abort)
  3. Вместо TINY_SIZE вам нужен PATH_MAX, раз уж вы решили делать статический массив.
  4. char message[sz] это VLA/flexible array. Забудьте про такие лайфхаки, половина компилей такое не умеет.
  5. Что за привычка из C++ тайпдефить всё, что под руку попадется? Сиди потом при удаленной отладке (или без всплывашек IDE) гадай - это птица? Это самолет? Нет, это enum.
  6. И прекратите вообще использовать статические буферы! Ну сколько можно?

Короч, вот вам пример, дальше как-нибудь сами (я его не компилил, написал прям тут, тут ничего сложного нет. Если есть ошибки - исправите сами). Комменты к функциям не писал, так как не увидел их у вас. Нормальные люди их пишут, мы, например, используем doxygen:

enum log_level {
    LOG_CRT,
    LOG_ERR,
    LOG_WRN,
    LOG_INF,
    LOG_DBG
}

static const char *_log_str_level(enum log_level level) {
    // Я без понятия как вы там у себя накодили, 
    // я бы сделал свищ или статический массив (но в 
    // случае последнего надо сначала проверить что 
    // level имеет адекватные значения, что по нормальному 
    // без хардкода сделать трудно

    switch(level) {
        case LOG_CRT: return "CRT";
        ..
        case LOG_DBG: return "DBG";
    }

    abort();
}

static void _log_std_file(enum log_level level) {
    if (level < LOG_INF)
         return stderr;
    else return stdout;
}

// Общая рутина. Если вам нужно разное форматирование в std/FILE,
// то напишите к каждому свою.
static void _log_file_write(FILE *file, const char *msg) {
    // Если вас ну прям совсем волнует быстродействие -
    // то чтобы сэкономить 5-20 мкс, вынесите запрос времени в `log`,
    // А сюда передавайте ts & lt
    struct timespec ts;
    // Алсо почитайте, чем отличается MONOTONIC от REALTIME
    // P.S. В mingw MONOTONIC не работает/работает через ass.
    clock_gettime(CLOCK_REALTIME, &ts); 
    struct tm *lt = localtime(&ts.tv_sec);

    fprintf(
        file,
        "[%s]: [%02d/%02d/%4d  %02d:%02d:%02d.%03d] %s\n",
        _log_str_level(level),
        lt->tm_mday, 
        lt->tm_mon+1, 
        lt->tm_year+1900, 
        lt->tm_hour, 
        lt->tm_min, 
        lt->tm_sec, 
        (int)(1e-6*ts.tv_nsec), 
        msg
    );
}

/* Атрибут, указывающий компилятору проверять 
 * наши аргументы как если бы это был printf.
 * Используйте атрибуты по максимуму, их много */
__attribute__((format(printf, 2, 3)) 
void log(enum log_level level, const char *fmt, ...) {
    /* Тут у вас несколько вариантов.
     * 1. Просто прописать тут список проверок и вызовов
     * 2. Если вы собираетесь расширяться (std, file, syslog, graylog),
     *    то лучше разбить по модулям и у каждого сделать регистратор в
     *    конструкторе, который передаст процедуру проверки необходимости
     *    вызова такого логгера в данный момент и саму функцию, которую
     *    необходимо вызвать
     *
     * Для простоты тут покажу как сделать по п.1 */

    /* Тут мы будем использовать vasprintf. Она не входит в стандарт, но
     * присутствует в gcc и mingw. Если вам не нравится - можете
     * - написать свою через `vsnprintf(NULL, 0` + `malloc` + `vsnprintf(str, fmt` */
     * - или каждый раз писать через fprintf сначала ваш дополнительный блок с
     *   временем, именем уровня, потом отдельным вызовом сообщение, потом '\n' */

    // 0. Подготовка к записи:
    va_list args;
    va_start(args, fmt);
    char *str;
    if (vasprintf(&str, fmt, args) < 0)
        abort(); // Можете тут упасть, например. Такое бывает когда нет памяти
    va_end(args);
    

    // 1. Запись в std. (Тут) не выключается ничем, можете добавить проверку по ENV
    _log_file_write(_log_std_file(level), str);

    // 2. Запись в FILE. 
    // Мы тут будем проверять необходимость записи в файл по ENV
    // Это наиболее гибкий способ, потому что позволяет оператору
    // программы настроить место записи лога самостоятельно.
    // Если что, под Windows это тоже работает.
    static const char *log_env_file = "LOG_FILENAME";

    /* Для Linux это работает хорошо, для Windows 
     * лучше проставить ifdef и брать через GetEnvironmentVariableW,
     * потому что версия getenv в mingw забагована и не обновляет
     * значение если изменить его в процессе работы программы */
    const char *filename = getenv(log_env_file);
    
    if (filename) {
        FILE *file = fopen(filename, "a");
        if (!file)
            // Ну вы тут сами обработку ошибок напишите, мне лень

        // Еще тут можно сдобрить проверкой размера этого файла и ротацией
        // (у нас так), лично вы можете забить. Просто у нас уже были логи по 500 МБ
        _log_file_write(file, str);
        fclose(file); // Форсируем закрытие транзакции, гарантируем, что файл записан
    }

    // Очистки
    free(str);
}

Что тут можно улучшить для семантики:

  • Вынести // 0, 1, 2 В отдельные функции. Делается на изях
  • Всё-таки сделать через раздельные модули с регистрацией. Потом скажете спасибо
  • Если не нравится делать _GNU_SOURCE то выделить три разных вызова:
    • _log_head(file), где выводить loglvl и time
    • _log_body(file, msg), где выводить просто msg
    • _log_foot(file), где добавлять в конец ‘\n’ Последнее нужно для того, чтобы stdout, например, форснул вывод, т.к. он буферизованный
  • Добавить mutex_lock на log, потому что в разных потоках вы запорете небуферизованный вывод.
PPP328 ★★★★★
()
Ответ на: комментарий от PPP328

Огромное спасибо за подробный ответ!

Я осознал свои ошибки. Делаю свой вариант, в направлении заданном Вами, с использованием

vsnprintf(NULL, 0` + `malloc` + `vsnprintf(str, fmt` 

Так у меня на этапе malloc возникает

Segmentation fault (core dumped)

Делаю через VLA:

int sz = vsnprintf(NULL, 0, fmt, args); // вызов чтобы узнать размер
...
char str[1+sz]; // VLA
    
va_start(args, fmt);
vsnprintf(str, sz+1, fmt, args);
va_end(args);

все работает как надо.

Попробую побороть ситуацию с Segmentation fault и позже выложу полностью свой вариант.

Gyros
() автор топика