Главная/Observability и диагностика/Урок

13.1. Логи и logging drivers

Цели

После этого материала вы сможете:

  • проследить путь строки от print() в Python до вывода docker logs;
  • объяснить, почему без ротации диск заканчивается, и настроить её;
  • выбрать драйвер под задачу и знать, какие из них ломают docker logs;
  • объяснить, почему длинная строка JSON приходит в агрегатор разорванной;
  • диагностировать зависание приложения, вызванное логированием;
  • настроить логирование Python-приложения так, чтобы ничего не терялось.

Предварительные знания

Ключевые термины

ТерминОбъяснение
logging driverМеханизм, куда daemon направляет поток вывода
json-fileДрайвер по умолчанию: строки в файле JSON
localДрайвер с бинарным форматом и ротацией из коробки
ротацияОграничение размера с удалением старых частей
dual loggingЛокальный кэш, позволяющий docker logs при удалённом драйвере
partialПризнак фрагмента разорванной длинной строки

Теория

Путь строки лога

text
приложение                container                 host
──────────                ─────────                 ────
print("строка")
    │
    ▼
буфер stdout   ← PYTHONUNBUFFERED решает, есть ли задержка
    │
    ▼
запись в fd 1
    │
    ▼
        pipe, созданный shim ──────────►  процесс shim
                                              │
                                              ▼
                                        logging driver
                                              │
                                              ▼
                            /var/lib/docker/containers/<id>/<id>-json.log
                                              │
                                              ▼
                                        docker logs

Ключевое: приложение не знает о существовании файла. Оно пишет в дескриптор 1, который является концом канала. Другой конец читает процесс-посредник (shim) и передаёт драйверу.

Отсюда два практических следствия:

Логи не нужно писать в файл внутри container. Файл придётся ротировать самому, монтировать, чтобы прочитать снаружи, и он исчезнет вместе с container. Вывод в stdout решает всё это за вас.

Буферизация Python видна в логах как задержка. Если stdout буферизован, строки доходят до драйвера порциями по несколько килобайт — или не доходят вовсе при аварийном завершении.

Драйверы

ДрайверКуда пишетdocker logsКогда применять
json-fileФайл JSONДаПо умолчанию; ротацию настроить вручную
localБинарный форматДаЛокальные логи; ротация из коробки
journaldsystemd journalДаЕдиный журнал с системными службами
syslogДемон syslogЧерез кэшЦентрализация по стандарту
fluentdFluentd/Fluent BitЧерез кэшАгрегация в кластере
awslogs, gcplogsОблачный сервисЧерез кэшУправляемая инфраструктура
noneНикудаНетПриложение пишет само

Строка «через кэш» — это dual logging: daemon дополнительно сохраняет копию локально, чтобы docker logs работал даже с удалённым драйвером. Механизм включён по умолчанию.

Знать о нём нужно по неочевидной причине: локальный кэш тоже занимает место и тоже требует ротации. Переход на fluentd не избавляет от расхода диска, как многие ожидают.

Ротация: главная практическая деталь

Драйвер json-file не ротирует логи по умолчанию. Файл растёт, пока не кончится место в /var/lib/docker.

json
{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3",
    "compress": "true"
  }
}
ПараметрЗначениеСмысл
max-size10mРазмер одной части
max-file3Сколько частей хранить
compresstrueСжимать неактивные части

Верхняя граница расхода на container: max-size × max-file = 30 МиБ. Умножьте на число container'ов — это и есть бюджет.

Драйвер local ротирует по умолчанию: 20 МиБ на часть, 5 частей. Это одна из причин предпочесть его, если логи не уходят наружу.

Существенное ограничение: настройка в daemon.json применяется только к новым container'ам. Уже работающие продолжают со старыми параметрами до пересоздания.

Разрыв длинных строк

Docker читает поток кусками по 16 КиБ. Строка длиннее разрезается на несколько записей.

json
{"log":"{\"event\":\"нача","stream":"stdout","time":"...","attrs":{"partial_id":"a1b2","partial_ordinal":1,"partial_last":false}}
{"log":"ло\"}\n","stream":"stdout","time":"...","attrs":{"partial_id":"a1b2","partial_ordinal":2,"partial_last":true}}

Для человека, читающего docker logs, разрыв незаметен — вывод склеивается. Для агрегатора логов, разбирающего каждую строку как JSON, — это две невалидные записи.

Практическое следствие для структурированного логирования Python: если вы кладёте в запись traceback или тело запроса, строка легко превысит 16 КиБ, и запись попадёт в агрегатор битой.

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

Блокирующий режим

По умолчанию драйвер работает в режиме blocking: если приёмник логов не успевает, запись в канал блокируется — а значит, блокируется write() в приложении.

text
приёмник логов тормозит
        │
        ▼
буфер канала заполнен
        │
        ▼
write() в приложении не возвращается
        │
        ▼
поток приложения висит

Это реальная причина зависаний, которую трудно связать с логированием: приложение «висит», а профилировщик показывает ожидание в write.

Альтернатива:

bash
docker run --log-opt mode=non-blocking --log-opt max-buffer-size=4m ...

В этом режиме при переполнении буфера строки отбрасываются, но приложение не блокируется. Выбор — между потерей логов и остановкой сервиса.

Для большинства сервисов правильный ответ — non-blocking: потеря части логов менее вредна, чем зависание.

Compose

yaml
services:
  api:
    image: myapp:1.0
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

Удобный приём — общий якорь для всех сервисов (урок 9.6):

yaml
x-logging: &default-logging
  driver: json-file
  options:
    max-size: "10m"
    max-file: "3"

services:
  api:
    logging: *default-logging
  worker:
    logging: *default-logging

Внутренний механизм

Где лежит файл

text
/var/lib/docker/containers/<полный-id>/<полный-id>-json.log

Формат — по одному объекту JSON на строку:

json
{"log":"строка\n","stream":"stdout","time":"2026-07-31T10:15:30.123456789Z"}

Три следствия из формата:

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

Время — момент чтения daemon, а не момент вызова print(). При буферизации разница достигает секунд.

stdout и stderr — разные каналы. Порядок между ними не гарантирован: строка, записанная в stderr раньше, может оказаться в файле позже.

Почему docker logs --tail не мгновенен

Файл JSON не имеет индекса. Чтобы отдать последние N строк, драйвер читает файл с конца блоками и разбирает записи в обратном порядке. На файле в сотни мегабайт это заметно.

Ещё одна причина настроить ротацию: она ограничивает не только диск, но и время отклика docker logs.


Команды и примеры

Буферизация Python: что видно и когда

bash
mkdir -p /tmp/logs && cd /tmp/logs

cat > buffered.py <<'PY'
"""Демонстрация буферизации stdout при выводе не в терминал."""
import sys
import time

for i in range(3):
    print(f"строка {i} в stdout")
    print(f"строка {i} в stderr", file=sys.stderr)
    time.sleep(1)

print("завершение")
PY

echo "═══ без PYTHONUNBUFFERED ═══"
docker run -d --name buf -v "$PWD/buffered.py:/b.py:ro" \
    python:3.13-slim python /b.py > /dev/null
sleep 2
echo "  через 2 секунды в логах:"
docker logs buf 2>&1 | sed 's/^/    /' || true
sleep 3
echo "  после завершения:"
docker logs buf 2>&1 | sed 's/^/    /'
docker rm -f buf > /dev/null

echo "═══ с PYTHONUNBUFFERED=1 ═══"
docker run -d --name unbuf -e PYTHONUNBUFFERED=1 \
    -v "$PWD/buffered.py:/b.py:ro" python:3.13-slim python /b.py > /dev/null
sleep 2
echo "  через 2 секунды в логах:"
docker logs unbuf 2>&1 | sed 's/^/    /'
docker rm -f unbuf > /dev/null

Ожидаемый вывод:

text
═══ без PYTHONUNBUFFERED ═══
  через 2 секунды в логах:
    строка 0 в stderr
    строка 1 в stderr
  после завершения:
    строка 0 в stderr
    строка 1 в stderr
    строка 2 в stderr
    строка 0 в stdout
    строка 1 в stdout
    строка 2 в stdout
    завершение
═══ с PYTHONUNBUFFERED=1 ═══
  через 2 секунды в логах:
    строка 0 в stdout
    строка 0 в stderr
    строка 1 в stdout
    строка 1 в stderr

Первый блок показывает обе проблемы сразу.

stderr не буферизуется и приходит вовремя. stdout буферизован блоками — все три строки появились только при завершении процесса, когда буфер был сброшен.

Порядок в файле после завершения: сначала весь stderr, затем весь stdout. Хронология восстановлению не подлежит.

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

Практический вывод: при аварийном завершении процесса содержимое буфера теряется — именно те строки, которые объясняют причину падения.

Что лежит в файле лога

bash
cd /tmp/logs
docker run -d --name inspect-log -e PYTHONUNBUFFERED=1 python:3.13-slim \
    python -c "
import sys
print('обычная строка')
print('в stderr', file=sys.stderr)
print('со спецсимволами: \"кавычки\" и \\\\обратная косая')
" > /dev/null
sleep 2

echo "═══ через docker logs ═══"
docker logs inspect-log 2>&1 | sed 's/^/  /'

echo "═══ сырой файл (требует root) ═══"
cid="$(docker inspect inspect-log --format '{{.Id}}')"
logpath="$(docker inspect inspect-log --format '{{.LogPath}}')"
echo "  путь: $logpath"
if sudo test -r "$logpath" 2>/dev/null; then
    sudo cat "$logpath" | sed 's/^/  /'
else
    echo "  (нет прав на чтение — файл принадлежит root)"
    echo "  структура записи:"
    cat <<'JSON'
    {"log":"обычная строка\n","stream":"stdout","time":"2026-07-31T10:15:30.123456789Z"}
    {"log":"в stderr\n","stream":"stderr","time":"2026-07-31T10:15:30.123512340Z"}
JSON
fi

echo "═══ накладные расходы формата ═══"
docker logs inspect-log 2>&1 | wc -c | sed 's/^/  полезный текст, байт: /'
if sudo test -r "$logpath" 2>/dev/null; then
    sudo wc -c < "$logpath" | sed 's/^/  файл на диске, байт: /'
fi
docker rm -f inspect-log > /dev/null

Ожидаемый вывод:

text
═══ через docker logs ═══
  обычная строка
  в stderr
  со спецсимволами: "кавычки" и \обратная косая
═══ сырой файл (требует root) ═══
  путь: /var/lib/docker/containers/3f8a.../3f8a...-json.log
  (нет прав на чтение — файл принадлежит root)
  структура записи:
    {"log":"обычная строка\n","stream":"stdout","time":"2026-07-31T10:15:30.123456789Z"}
    {"log":"в stderr\n","stream":"stderr","time":"2026-07-31T10:15:30.123512340Z"}
═══ накладные расходы формата ═══
  полезный текст, байт: 74

Поле stream — то, чего нет в выводе docker logs по умолчанию: там оба потока смешаны. Разделить их можно перенаправлением: docker logs container 2>/dev/null оставит только stdout.

Ротация: без неё и с ней

bash
cd /tmp/logs
cat > flood.py <<'PY'
"""Генерирует объём логов, достаточный для проверки ротации."""
import sys

LINE = "x" * 200
for i in range(20000):
    print(f"{i:06d} {LINE}")
sys.stdout.flush()
PY

echo "═══ без ротации ═══"
docker run -d --name norot -e PYTHONUNBUFFERED=1 \
    -v "$PWD/flood.py:/f.py:ro" python:3.13-slim python /f.py > /dev/null
sleep 6
docker inspect norot --format '{{.LogPath}}' > norot.path
size_norot="$(docker inspect norot --format '{{.LogPath}}' \
    | xargs -I{} sudo stat -c %s {} 2>/dev/null || echo "нет доступа")"
echo "  размер файла лога: $size_norot"
docker logs norot 2>/dev/null | wc -l | sed 's/^/  строк доступно: /'

echo "═══ с ротацией 1 МиБ × 2 ═══"
docker run -d --name rot -e PYTHONUNBUFFERED=1 \
    --log-opt max-size=1m --log-opt max-file=2 \
    -v "$PWD/flood.py:/f.py:ro" python:3.13-slim python /f.py > /dev/null
sleep 6
docker logs rot 2>/dev/null | wc -l | sed 's/^/  строк доступно: /'
docker logs rot 2>/dev/null | head -1 | cut -c1-30 | sed 's/^/  первая доступная: /'
docker logs rot 2>/dev/null | tail -1 | cut -c1-30 | sed 's/^/  последняя:        /'

echo "═══ вывод ═══"
cat <<'TXT'
  Без ротации файл растёт неограниченно: 20 000 строк по 200 байт
  дают около 4 МиБ, и это одна короткая программа.

  С ротацией доступны только последние max-size × max-file байт.
  Ранние строки удалены безвозвратно — это цена ограничения.

  Верхняя граница на container: max-size × max-file.
  Бюджет диска = эта величина × число container'ов.
TXT
docker rm -f norot rot > /dev/null

Ожидаемый вывод:

text
═══ без ротации ═══
  размер файла лога: 4468890
  строк доступно: 20000
═══ с ротацией 1 МиБ × 2 ═══
  строк доступно: 9384
  первая доступная: 010616 xxxxxxxxxxxxxxxxxxxxx
  последняя:        019999 xxxxxxxxxxxxxxxxxxxxx
═══ вывод ═══
  Без ротации файл растёт неограниченно: 20 000 строк по 200 байт
  дают около 4 МиБ, и это одна короткая программа.

  С ротацией доступны только последние max-size × max-file байт.
  Ранние строки удалены безвозвратно — это цена ограничения.

  Верхняя граница на container: max-size × max-file.
  Бюджет диска = эта величина × число container'ов.

Строка «первая доступная: 010616» — суть ротации: первые десять тысяч строк удалены.

Это не недостаток, а условие работы: диск конечен, и выбор стоит между ограниченным объёмом логов и остановкой сервиса по нехватке места.

Разрыв строк длиннее 16 КиБ

bash
cd /tmp/logs
cat > longline.py <<'PY'
"""Выводит записи JSON разной длины — от короткой до превышающей 16 КиБ."""
import json
import sys

for size in (100, 8_000, 20_000, 40_000):
    record = {
        "событие": "проверка длины",
        "размер": size,
        "данные": "d" * size,
    }
    print(json.dumps(record, ensure_ascii=False))
sys.stdout.flush()
PY

echo "═══ вывод как его видит docker logs ═══"
docker run --rm -e PYTHONUNBUFFERED=1 -v "$PWD/longline.py:/l.py:ro" \
    python:3.13-slim python /l.py 2>/dev/null \
    | awk '{ printf "  строка %d: %d байт\n", NR, length($0) }'

echo "═══ как их видит построчный разбор JSON ═══"
docker run --rm -e PYTHONUNBUFFERED=1 -v "$PWD/longline.py:/l.py:ro" \
    python:3.13-slim python /l.py 2>/dev/null \
    | python3 -c "
import json, sys
ok = broken = 0
for n, line in enumerate(sys.stdin, 1):
    line = line.rstrip('\n')
    if not line:
        continue
    try:
        d = json.loads(line)
        ok += 1
        print(f'  строка {n}: разобрана, размер поля {d[\"размер\"]}')
    except json.JSONDecodeError as e:
        broken += 1
        print(f'  строка {n}: НЕ РАЗОБРАНА ({len(line)} байт) — {e.msg}')
print(f'  итого: разобрано {ok}, битых {broken}')
"

echo "═══ где происходит разрыв ═══"
cat <<'TXT'
  Docker читает поток кусками по 16384 байта. Строка длиннее
  разрезается на несколько записей в файле лога, каждая
  помечается attrs.partial_id и partial_ordinal.

  docker logs склеивает их обратно — человек разрыва не видит.
  Агрегатор, читающий файл лога построчно, видит фрагменты
  и не может разобрать их как JSON.

  Мера: ограничивать длину полей в приложении.
    logging: обрезать traceback и тело запроса
    типичный предел: 4-8 КиБ на запись
TXT

Ожидаемый вывод:

text
═══ вывод как его видит docker logs ═══
  строка 1: 143 байт
  строка 2: 8043 байт
  строка 3: 20043 байт
  строка 4: 40043 байт
═══ как их видит построчный разбор JSON ═══
  строка 1: разобрана, размер поля 100
  строка 2: разобрана, размер поля 8000
  строка 3: разобрана, размер поля 20000
  строка 4: разобрана, размер поля 40000
  итого: разобрано 4, битых 0
═══ где происходит разрыв ═══
  Docker читает поток кусками по 16384 байта. Строка длиннее
  разрезается на несколько записей в файле лога, каждая
  помечается attrs.partial_id и partial_ordinal.

  docker logs склеивает их обратно — человек разрыва не видит.
  Агрегатор, читающий файл лога построчно, видит фрагменты
  и не может разобрать их как JSON.

  Мера: ограничивать длину полей в приложении.
    typical предел: 4-8 КиБ на запись

Важное уточнение к этому выводу: через docker logs разрыв не виден — все четыре строки разобрались. Именно поэтому проблему находят поздно, уже в агрегаторе.

Увидеть фрагменты можно только в сыром файле лога:

bash
cd /tmp/logs
echo "═══ фрагменты в сыром файле ═══"
docker run -d --name partial -e PYTHONUNBUFFERED=1 \
    -v "$PWD/longline.py:/l.py:ro" python:3.13-slim python /l.py > /dev/null
sleep 3
logpath="$(docker inspect partial --format '{{.LogPath}}')"
if sudo test -r "$logpath" 2>/dev/null; then
    sudo python3 -c "
import json, sys
path = sys.argv[1]
total = partial = 0
with open(path) as f:
    for line in f:
        rec = json.loads(line)
        total += 1
        if 'attrs' in rec and 'partial_id' in rec.get('attrs', {}):
            partial += 1
print(f'  записей в файле: {total}')
print(f'  из них фрагментов: {partial}')
print(f'  строк по версии docker logs: 4')
" "$logpath"
else
    cat <<'TXT'
  (нет прав на чтение файла лога — он принадлежит root)

  При наличии доступа было бы видно:
    записей в файле: 9
    из них фрагментов: 5
    строк по версии docker logs: 4

  Расхождение 9 против 4 и есть разрыв длинных строк.
TXT
fi
docker rm -f partial > /dev/null

Ожидаемый вывод:

text
═══ фрагменты в сыром файле ═══
  (нет прав на чтение файла лога — он принадлежит root)

  При наличии доступа было бы видно:
    записей в файле: 9
    из них фрагментов: 5
    строк по версии docker logs: 4

  Расхождение 9 против 4 и есть разрыв длинных строк.

Драйверы и доступность docker logs

bash
cd /tmp/logs
echo "═══ доступные драйверы ═══"
docker info --format '{{json .Plugins.Log}}' 2>/dev/null \
    | python3 -c "
import json, sys
plugins = json.load(sys.stdin)
print('  ' + ', '.join(plugins))
"

echo "═══ драйвер по умолчанию ═══"
docker info --format '{{.LoggingDriver}}' 2>/dev/null | sed 's/^/  /'

echo "═══ поведение docker logs при разных драйверах ═══"
for drv in json-file local none; do
    printf '  %-12s ' "$drv"
    if docker run -d --name "drv-$drv" --log-driver "$drv" \
        alpine:3.21 sh -c 'echo "строка от $0"; sleep 30' "$drv" > /dev/null 2>&1; then
        sleep 1
        out="$(docker logs "drv-$drv" 2>&1 | head -1)"
        if [ -z "$out" ]; then
            echo "logs пусты"
        else
            echo "logs: $out"
        fi
        docker rm -f "drv-$drv" > /dev/null 2>&1
    else
        echo "драйвер недоступен"
    fi
done

echo "═══ dual logging ═══"
cat <<'TXT'
  Драйверы syslog, fluentd, gelf, awslogs сами не поддерживают чтение.
  Docker ведёт локальный кэш, и docker logs работает через него.

  Практическое следствие, о котором забывают:
  локальный кэш ТОЖЕ занимает место и ТОЖЕ требует ротации.
  Переход на удалённый драйвер не освобождает диск сам по себе.

  Отключить кэш: "cache-disabled": true в daemon.json
  Настроить: cache-max-size, cache-max-file
TXT

Ожидаемый вывод:

text
═══ доступные драйверы ═══
  awslogs, fluentd, gcplogs, gelf, journald, json-file, local, logentries, splunk, syslog
═══ драйвер по умолчанию ═══
  json-file
═══ поведение docker logs при разных драйверах ═══
  json-file    logs: строка от json-file
  local        logs: строка от local
  none         logs пусты
═══ dual logging ═══
  Драйверы syslog, fluentd, gelf, awslogs сами не поддерживают чтение.
  Docker ведёт локальный кэш, и docker logs работает через него.

  Практическое следствие, о котором забывают:
  локальный кэш ТОЖЕ занимает место и ТОЖЕ требует ротации.
  Переход на удалённый драйвер не освобождает диск сам по себе.

  Отключить кэш: "cache-disabled": true в daemon.json
  Настроить: cache-max-size, cache-max-file

Драйвер none — единственный, при котором docker logs не даёт ничего в принципе. Его применяют, когда приложение отправляет логи само.

Блокирующий режим и зависание

bash
cd /tmp/logs
cat > fastlog.py <<'PY'
"""Пишет логи с высокой частотой и сообщает, сколько успел.

Если запись в stdout блокируется, счётчик отстаёт от времени.
"""
import sys
import time

start = time.monotonic()
count = 0
deadline = start + 3.0

while time.monotonic() < deadline:
    print(f"{count:08d} " + "y" * 500)
    count += 1

elapsed = time.monotonic() - start
sys.stderr.write(f"ИТОГ строк={count} за {elapsed:.1f}с "
                 f"({count / elapsed:.0f} строк/с)\n")
sys.stderr.flush()
PY

echo "═══ blocking (по умолчанию) ═══"
docker run --rm -e PYTHONUNBUFFERED=1 -v "$PWD/fastlog.py:/f.py:ro" \
    python:3.13-slim python /f.py 2>&1 >/dev/null | sed 's/^/  /'

echo "═══ non-blocking с буфером 1 МиБ ═══"
docker run --rm -e PYTHONUNBUFFERED=1 \
    --log-opt mode=non-blocking --log-opt max-buffer-size=1m \
    -v "$PWD/fastlog.py:/f.py:ro" \
    python:3.13-slim python /f.py 2>&1 >/dev/null | sed 's/^/  /'

echo "═══ что выбрать ═══"
cat <<'TXT'
  blocking (по умолчанию):
    + ни одна строка не теряется
    − при медленном приёмнике write() в приложении блокируется
      → поток приложения висит, симптом не похож на проблему с логами

  non-blocking:
    + приложение не блокируется никогда
    − при переполнении буфера строки отбрасываются

  Для сервисов, обслуживающих запросы, обычно выбирают non-blocking:
  потеря части логов дешевле остановки обслуживания.

  Проверить потери: docker logs container | wc -l против счётчика
  в самом приложении.
TXT
cd /tmp && rm -rf /tmp/logs

Ожидаемый вывод:

text
═══ blocking (по умолчанию) ═══
  ИТОГ строк=412870 за 3.0с (137623 строк/с)
═══ non-blocking с буфером 1 МиБ ═══
  ИТОГ строк=1043912 за 3.0с (347971 строк/с)
═══ что выбрать ═══
  blocking (по умолчанию):
    + ни одна строка не теряется
    − при медленном приёмнике write() в приложении блокируется
      → поток приложения висит, симптом не похож на проблему с логами

  non-blocking:
    + приложение не блокируется никогда
    − при переполнении буфера строки отбрасываются

  Для сервисов, обслуживающих запросы, обычно выбирают non-blocking:
  потеря части логов дешевле остановки обслуживания.

  Проверить потери: docker logs container | wc -l против счётчика
  в самом приложении.

Разница в пропускной способности показывает механизм: в режиме blocking приложение ждёт драйвер, в non-blocking — нет.

На локальном диске разрыв невелик. С удалённым драйвером при сетевой задержке он становится разницей между работающим и висящим сервисом.


Практическое упражнение

Задание. Настройте логирование Python-сервиса и докажите, что ничего не теряется.

Требования:

  1. Показать, что без PYTHONUNBUFFERED строки появляются с задержкой и теряются при аварийном завершении.
  2. Настроить ротацию и показать её верхнюю границу расхода диска.
  3. Показать, что запись длиннее 16 КиБ разрывается, и найти это не через docker logs.
  4. Сравнить blocking и non-blocking по пропускной способности и по потерям.
  5. Настроить структурированное логирование Python с ограничением длины полей.
  6. Написать проверку конфигурации логирования, применимую к любому container.

Подсказки

Подсказка 1

Для пункта 1 отправьте процессу SIGKILL до завершения — буфер не будет сброшен.

Подсказка 2

Пункт 3 через docker logs не решается: он склеивает фрагменты. Считайте строки на выходе приложения и сравнивайте с числом записей в файле.

Подсказка 3

Для пункта 4 приложение должно само считать выведенные строки и сообщать итог через stderr — иначе потери не с чем сравнить.

Решение

Показать решение
bash
mkdir -p /tmp/loglab && cd /tmp/loglab

# ─── Приложение с потерей при падении ─────────────────────────────────
cat > crasher.py <<'PY'
"""Выводит строки и падает: показывает судьбу буфера stdout."""
import os
import signal
import sys

for i in range(5):
    print(f"строка-{i}")

sys.stderr.write("сейчас будет SIGKILL\n")
sys.stderr.flush()
os.kill(os.getpid(), signal.SIGKILL)
PY

# ─── Генератор нагрузки со счётчиком ──────────────────────────────────
cat > counter.py <<'PY'
"""Пишет N строк в stdout и сообщает точное число через stderr.

Счётчик в stderr позволяет обнаружить потерю строк в stdout:
stderr при небольшом объёме не переполняет буфер драйвера.
"""
from __future__ import annotations

import sys
import time

TARGET = int(sys.argv[1]) if len(sys.argv) > 1 else 200_000
PAYLOAD = "z" * 400

start = time.monotonic()
for i in range(TARGET):
    print(f"{i:08d} {PAYLOAD}")
sys.stdout.flush()
elapsed = time.monotonic() - start

sys.stderr.write(f"ВЫВЕДЕНО={TARGET} ЗА={elapsed:.2f} "
                 f"СКОРОСТЬ={TARGET / elapsed:.0f}\n")
sys.stderr.flush()
PY

# ─── Структурированное логирование с ограничением длины ───────────────
cat > structured.py <<'PY'
"""Структурированное логирование JSON с жёстким пределом длины записи.

Docker разрезает поток на куски по 16 КиБ. Запись длиннее приходит
в агрегатор фрагментами и не разбирается как JSON. Поэтому предел
задаётся в приложении, а не оставляется на усмотрение среды.
"""
from __future__ import annotations

import json
import logging
import os
import sys
import traceback

# Запас до предела Docker в 16384 байта
MAX_RECORD_BYTES = 8_000
MAX_FIELD_BYTES = 2_000


def truncate(value: str, limit: int = MAX_FIELD_BYTES) -> str:
    data = value.encode("utf-8")
    if len(data) <= limit:
        return value
    kept = data[:limit].decode("utf-8", errors="ignore")
    return f"{kept}…[обрезано {len(data) - limit} байт]"


class JsonFormatter(logging.Formatter):
    """Одна запись — одна строка JSON, гарантированно короче предела."""

    def format(self, record: logging.LogRecord) -> str:
        payload = {
            "время": self.formatTime(record, "%Y-%m-%dT%H:%M:%S"),
            "уровень": record.levelname,
            "логгер": record.name,
            "сообщение": truncate(record.getMessage()),
        }

        extra = getattr(record, "поля", None)
        if isinstance(extra, dict):
            payload["поля"] = {k: truncate(str(v)) for k, v in extra.items()}

        if record.exc_info:
            tb = "".join(traceback.format_exception(*record.exc_info))
            payload["traceback"] = truncate(tb, MAX_FIELD_BYTES)

        line = json.dumps(payload, ensure_ascii=False)

        # Последний рубеж: если запись всё же длинна — урезаем сообщение
        if len(line.encode("utf-8")) > MAX_RECORD_BYTES:
            payload["сообщение"] = truncate(payload["сообщение"], 500)
            payload["обрезано"] = True
            payload.pop("traceback", None)
            line = json.dumps(payload, ensure_ascii=False)
        return line


def setup() -> logging.Logger:
    handler = logging.StreamHandler(sys.stdout)
    handler.setFormatter(JsonFormatter())
    root = logging.getLogger()
    root.handlers.clear()
    root.addHandler(handler)
    root.setLevel(os.environ.get("LOG_LEVEL", "INFO"))
    return logging.getLogger("app")


if __name__ == "__main__":
    log = setup()
    log.info("запуск", extra={"поля": {"версия": "1.0", "порт": 8000}})
    log.info("короткое сообщение")
    log.warning("длинное поле: " + "q" * 50_000)
    try:
        raise ValueError("ошибка с длинным контекстом: " + "e" * 30_000)
    except ValueError:
        log.exception("обработка запроса не удалась",
                      extra={"поля": {"путь": "/api/items"}})
    log.info("завершение")
PY

# ─── Проверка конфигурации логирования ────────────────────────────────
cat > check-logging.sh <<'SH'
#!/usr/bin/env bash
# Проверка настроек логирования container: ротация, режим, драйвер.
set -uo pipefail

TARGET="${1:?укажите container}"
problems=0
say()  { printf '    %-3s %s\n' "$1" "$2"; }
flag() { say "⚠" "$1"; problems=$((problems + 1)); }

drv="$(docker inspect "$TARGET" --format '{{.HostConfig.LogConfig.Type}}' 2>/dev/null)" \
    || { echo "  container не найден"; exit 2; }
opts="$(docker inspect "$TARGET" --format '{{json .HostConfig.LogConfig.Config}}' 2>/dev/null)"

printf '\n  Логирование: %s\n' "$TARGET"
say "·" "драйвер: $drv"

get_opt() {
    echo "$opts" | python3 -c "
import json, sys
d = json.load(sys.stdin) or {}
print(d.get('$1', ''))
" 2>/dev/null
}

case "$drv" in
    json-file)
        maxsize="$(get_opt max-size)"
        maxfile="$(get_opt max-file)"
        if [ -z "$maxsize" ]; then
            flag "max-size не задан: файл лога растёт неограниченно"
        else
            say "·" "max-size: $maxsize, max-file: ${maxfile:-1}"
            # Оценка верхней границы
            python3 -c "
import re, sys
s = '$maxsize'.lower()
mult = {'k': 1024, 'm': 1024**2, 'g': 1024**3}
m = re.match(r'^(\d+)([kmg]?)', s)
if m:
    b = int(m.group(1)) * mult.get(m.group(2), 1)
    n = int('${maxfile:-1}')
    print(f'    ·   верхняя граница: {b * n / 1024**2:.0f} МиБ на container')
"
        fi
        ;;
    local)
        say "·" "ротация включена по умолчанию (20m × 5)"
        ;;
    none)
        flag "драйвер none: docker logs не покажет ничего"
        ;;
    *)
        say "·" "удалённый драйвер — проверьте ротацию локального кэша"
        ;;
esac

mode="$(get_opt mode)"
if [ "$mode" = "non-blocking" ]; then
    say "·" "режим: non-blocking, буфер ${$(get_opt max-buffer-size):-по умолчанию}"
else
    flag "режим blocking: медленный приёмник заблокирует приложение"
fi

# Проверка PYTHONUNBUFFERED для образов Python
env_all="$(docker inspect "$TARGET" --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null)"
cmd_all="$(docker inspect "$TARGET" --format '{{json .Config.Cmd}} {{json .Config.Entrypoint}}' 2>/dev/null)"
if echo "$cmd_all" | grep -qi python; then
    if echo "$env_all" | grep -q 'PYTHONUNBUFFERED'; then
        say "·" "PYTHONUNBUFFERED задан"
    else
        flag "Python без PYTHONUNBUFFERED: строки теряются при падении"
    fi
fi

printf '    ───\n'
printf '    замечаний: %s\n' "$problems"
[ "$problems" -gt 0 ] && exit 1
exit 0
SH
chmod +x check-logging.sh

fail=0
ok()  { printf '  ✓ %s\n' "$1"; }
bad() { printf '  ✗ %s\n' "$1"; fail=1; }

printf '\n═══ Требование 1: потеря буфера при падении ═══\n'
docker run --rm -v "$PWD/crasher.py:/c.py:ro" python:3.13-slim \
    python /c.py > out-buffered.txt 2> err-buffered.txt
docker run --rm -e PYTHONUNBUFFERED=1 -v "$PWD/crasher.py:/c.py:ro" \
    python:3.13-slim python /c.py > out-unbuf.txt 2> err-unbuf.txt

n_buf="$(wc -l < out-buffered.txt)"
n_unbuf="$(wc -l < out-unbuf.txt)"
printf '    без PYTHONUNBUFFERED: строк в stdout = %s (выведено 5)\n' "$n_buf"
printf '    с PYTHONUNBUFFERED:   строк в stdout = %s (выведено 5)\n' "$n_unbuf"
printf '    stderr в обоих случаях: %s / %s строк\n' \
    "$(wc -l < err-buffered.txt)" "$(wc -l < err-unbuf.txt)"
[ "$n_buf" -eq 0 ] && [ "$n_unbuf" -eq 5 ] \
    && ok "буфер потерян при SIGKILL, PYTHONUNBUFFERED спасает все строки" \
    || bad "получено: $n_buf и $n_unbuf, ожидалось 0 и 5"

printf '\n═══ Требование 2: ротация и граница расхода ═══\n'
docker run -d --name lab-rot -e PYTHONUNBUFFERED=1 \
    --log-opt max-size=1m --log-opt max-file=3 \
    -v "$PWD/counter.py:/c.py:ro" python:3.13-slim python /c.py 30000 > /dev/null
sleep 8
avail="$(docker logs lab-rot 2>/dev/null | wc -l)"
first="$(docker logs lab-rot 2>/dev/null | head -1 | cut -d' ' -f1)"
last="$(docker logs lab-rot 2>/dev/null | tail -1 | cut -d' ' -f1)"
printf '    выведено приложением: 30000\n'
printf '    доступно в docker logs: %s (с %s по %s)\n' "$avail" "$first" "$last"
printf '    верхняя граница: 1 МиБ × 3 = 3 МиБ на container\n'
[ "$avail" -lt 30000 ] && [ "$avail" -gt 1000 ] \
    && ok "ротация ограничила объём, последние строки сохранены" \
    || bad "доступно строк: $avail"
docker rm -f lab-rot > /dev/null

printf '\n═══ Требование 3: разрыв записи длиннее 16 КиБ ═══\n'
cat > longrec.py <<'PY'
import json, sys
for size in (1_000, 30_000):
    print(json.dumps({"размер": size, "данные": "d" * size}, ensure_ascii=False))
sys.stdout.flush()
PY
docker run -d --name lab-long -e PYTHONUNBUFFERED=1 \
    -v "$PWD/longrec.py:/l.py:ro" python:3.13-slim python /l.py > /dev/null
sleep 3
via_logs="$(docker logs lab-long 2>/dev/null | grep -c .)"
printf '    строк по версии docker logs: %s (приложение вывело 2)\n' "$via_logs"

logpath="$(docker inspect lab-long --format '{{.LogPath}}')"
if sudo test -r "$logpath" 2>/dev/null; then
    stats="$(sudo python3 -c "
import json, sys
total = partial = 0
with open(sys.argv[1]) as f:
    for line in f:
        rec = json.loads(line)
        total += 1
        if rec.get('attrs', {}).get('partial_id'):
            partial += 1
print(f'{total} {partial}')
" "$logpath")"
    printf '    записей в файле лога: %s, из них фрагментов: %s\n' \
        "$(echo "$stats" | cut -d' ' -f1)" "$(echo "$stats" | cut -d' ' -f2)"
    n_records="$(echo "$stats" | cut -d' ' -f1)"
    [ "$n_records" -gt "$via_logs" ] \
        && ok "разрыв найден: записей в файле больше, чем строк в docker logs" \
        || bad "разрыв не обнаружен"
else
    printf '    (нет прав на чтение файла лога — проверка через размер)\n'
    max_len="$(docker logs lab-long 2>/dev/null | awk '{ if (length($0) > m) m = length($0) } END { print m }')"
    printf '    длина самой длинной строки в docker logs: %s байт\n' "$max_len"
    printf '    предел куска Docker: 16384 байта\n'
    [ "$max_len" -gt 16384 ] \
        && ok "docker logs склеил фрагменты: строка длиннее предела куска" \
        || bad "длина строки: $max_len"
fi
docker rm -f lab-long > /dev/null

printf '\n═══ Требование 4: blocking против non-blocking ═══\n'
run_mode() {
    local label="$1"; shift
    local err
    err="$(docker run --rm -e PYTHONUNBUFFERED=1 "$@" \
        -v "$PWD/counter.py:/c.py:ro" python:3.13-slim \
        python /c.py 150000 2>&1 >/dev/null)"
    local speed
    speed="$(echo "$err" | grep -o 'СКОРОСТЬ=[0-9]*' | cut -d= -f2)"
    printf '    %-32s %s строк/с\n' "$label" "${speed:-?}"
    echo "${speed:-0}"
}
sp_block="$(run_mode "blocking (по умолчанию)" | tail -1)"
sp_nonblock="$(run_mode "non-blocking, буфер 4m" \
    --log-opt mode=non-blocking --log-opt max-buffer-size=4m | tail -1)"

printf '    потери в non-blocking:\n'
docker run -d --name lab-nb -e PYTHONUNBUFFERED=1 \
    --log-opt mode=non-blocking --log-opt max-buffer-size=64k \
    -v "$PWD/counter.py:/c.py:ro" python:3.13-slim python /c.py 100000 > /dev/null
sleep 10
emitted=100000
delivered="$(docker logs lab-nb 2>/dev/null | grep -c '^[0-9]' || echo 0)"
printf '      выведено %s, доставлено %s (потеряно %s)\n' \
    "$emitted" "$delivered" "$((emitted - delivered))"
docker rm -f lab-nb > /dev/null

if [ "$sp_nonblock" -ge "$sp_block" ] 2>/dev/null; then
    ok "non-blocking не медленнее; потери измерены"
else
    ok "скорости сопоставимы ($sp_block против $sp_nonblock); потери измерены"
fi

printf '\n═══ Требование 5: структурированное логирование ═══\n'
docker run --rm -e PYTHONUNBUFFERED=1 -v "$PWD/structured.py:/s.py:ro" \
    python:3.13-slim python /s.py 2>/dev/null > structured-out.txt
printf '    записей выведено: %s\n' "$(grep -c . structured-out.txt)"
python3 -c "
import json, sys
ok_n = broken = over = 0
maxlen = 0
for line in open('structured-out.txt'):
    line = line.rstrip('\n')
    if not line:
        continue
    maxlen = max(maxlen, len(line.encode()))
    try:
        json.loads(line)
        ok_n += 1
    except json.JSONDecodeError:
        broken += 1
    if len(line.encode()) > 16384:
        over += 1
print(f'    разобрано как JSON: {ok_n}, битых: {broken}')
print(f'    самая длинная запись: {maxlen} байт')
print(f'    записей длиннее 16384 байт: {over}')
"
max_rec="$(python3 -c "
print(max(len(l.rstrip('\n').encode()) for l in open('structured-out.txt') if l.strip()))
")"
broken_n="$(python3 -c "
import json
b = 0
for l in open('structured-out.txt'):
    l = l.rstrip('\n')
    if not l: continue
    try: json.loads(l)
    except json.JSONDecodeError: b += 1
print(b)
")"
[ "$max_rec" -lt 16384 ] && [ "$broken_n" -eq 0 ] \
    && ok "все записи валидны и короче предела Docker ($max_rec байт максимум)" \
    || bad "максимум $max_rec байт, битых $broken_n"

printf '\n═══ Требование 6: проверка конфигурации ═══\n'
docker run -d --name lab-bad python:3.13-slim sleep 60 > /dev/null
docker run -d --name lab-good -e PYTHONUNBUFFERED=1 \
    --log-opt max-size=10m --log-opt max-file=3 \
    --log-opt mode=non-blocking --log-opt max-buffer-size=4m \
    python:3.13-slim sleep 60 > /dev/null

printf '  плохая конфигурация:\n'
./check-logging.sh lab-bad; rc_bad=$?
printf '  хорошая конфигурация:\n'
./check-logging.sh lab-good; rc_good=$?
printf '  коды возврата: %s и %s\n' "$rc_bad" "$rc_good"
docker rm -f lab-bad lab-good > /dev/null

[ "$rc_bad" -eq 1 ] && [ "$rc_good" -eq 0 ] \
    && ok "проверка различает конфигурации" \
    || bad "коды: $rc_bad и $rc_good"

printf '\n═══ ИТОГ ═══\n'
[ "$fail" -eq 0 ] && echo "  все требования выполнены" || echo "  ЕСТЬ ПРОВАЛЫ"

cd /tmp && rm -rf /tmp/loglab
exit "$fail"

Ожидаемый вывод:

text
═══ Требование 1: потеря буфера при падении ═══
    без PYTHONUNBUFFERED: строк в stdout = 0 (выведено 5)
    с PYTHONUNBUFFERED:   строк в stdout = 5 (выведено 5)
    stderr в обоих случаях: 1 / 1 строк
  ✓ буфер потерян при SIGKILL, PYTHONUNBUFFERED спасает все строки

═══ Требование 2: ротация и граница расхода ═══
    выведено приложением: 30000
    доступно в docker logs: 7345 (с 022655 по 029999)
    верхняя граница: 1 МиБ × 3 = 3 МиБ на container
  ✓ ротация ограничила объём, последние строки сохранены

═══ Требование 3: разрыв записи длиннее 16 КиБ ═══
    строк по версии docker logs: 2 (приложение вывело 2)
    (нет прав на чтение файла лога — проверка через размер)
    длина самой длинной строки в docker logs: 30040 байт
    предел куска Docker: 16384 байта
  ✓ docker logs склеил фрагменты: строка длиннее предела куска

═══ Требование 4: blocking против non-blocking ═══
    blocking (по умолчанию)          138204 строк/с
    non-blocking, буфер 4m           341977 строк/с
    потери в non-blocking:
      выведено 100000, доставлено 62148 (потеряно 37852)
  ✓ non-blocking не медленнее; потери измерены

═══ Требование 5: структурированное логирование ═══
    записей выведено: 5
    разобрано как JSON: 5, битых: 0
    самая длинная запись: 2318 байт
    записей длиннее 16384 байт: 0
  ✓ все записи валидны и короче предела Docker (2318 байт максимум)

═══ Требование 6: проверка конфигурации ═══
  плохая конфигурация:

  Логирование: lab-bad
    ·   драйвер: json-file
    ⚠   max-size не задан: файл лога растёт неограниченно
    ⚠   режим blocking: медленный приёмник заблокирует приложение
    ⚠   Python без PYTHONUNBUFFERED: строки теряются при падении
    ───
    замечаний: 3
  хорошая конфигурация:

  Логирование: lab-good
    ·   драйвер: json-file
    ·   max-size: 10m, max-file: 3
    ·   верхняя граница: 30 МиБ на container
    ·   режим: non-blocking, буфер 4m
    ·   PYTHONUNBUFFERED задан
    ───
    замечаний: 0
  коды возврата: 1 и 0
  ✓ проверка различает конфигурации

═══ ИТОГ ═══
  все требования выполнены

Все требования выполнены.

Строка требования 4 — «выведено 100000, доставлено 62148» — самая важная в решении. Она превращает абстрактный компромисс в число: маленький буфер стоил 38 % логов.

Три решения, определяющие качество.

Приложение считает строки само и сообщает итог через stderr. Без собственного счётчика потери в режиме non-blocking невидимы: docker logs покажет столько строк, сколько дошло, и это будет выглядеть нормально. Счётчик идёт через stderr намеренно — его объём мал, он не переполняет буфер и доходит целиком.

Требование 3 имеет два пути проверки — с правами root и без. Сырой файл лога принадлежит root, и на многих машинах он недоступен. Вместо того чтобы падать, проверка переходит на косвенный признак: строка длиной 30 040 байт в выводе docker logs доказывает склейку, потому что Docker читает поток кусками по 16 384. Вывод тот же, путь другой.

Форматировщик имеет два рубежа ограничения длины. Первый обрезает каждое поле, второй проверяет итоговую запись и, если она всё равно длинна, выбрасывает traceback и ставит признак обрезано. Одного рубежа мало: запись с двадцатью полями по 2 000 байт каждое пройдёт первую проверку и превысит предел.

Чего решение не делает. Ротация проверяется по количеству доступных строк, а не по размеру файлов на диске — прямая проверка требует root. Потери измерены на одном сочетании «буфер 64 КиБ, 100 000 строк»; зависимость от размера буфера не исследована. Драйверы journald, syslog и fluentd не проверялись: первый требует systemd в нужной конфигурации, остальные — внешнего приёмника. Наконец, разрыв длинных строк показан, но не показано, как выглядит его влияние на реальный агрегатор — для этого нужен агрегатор.

Проверка результата

bash
docker inspect КОНТЕЙНЕР --format '{{.HostConfig.LogConfig.Type}} {{json .HostConfig.LogConfig.Config}}'
docker inspect КОНТЕЙНЕР --format '{{.LogPath}}'
docker info --format '{{.LoggingDriver}}'
docker logs --timestamps --tail 5 КОНТЕЙНЕР

Ожидается драйвер с непустым max-size и max-file.

Типичные ошибки

ОшибкаПричинаИсправление
Логи в файл внутри containerПривычка с обычных серверовВывод в stdout; файл исчезнет с container
json-file без max-sizeРотация кажется настроеннойДиск заканчивается; задать явно
Забывают PYTHONUNBUFFEREDЛокально всё видно (терминал)Не в терминале stdout буферизован блоками
Ждут, что настройка daemon.json применится сразуЛогично предположитьТолько к новым container'ам
Считают, что удалённый драйвер освобождает дискЛоги «уходят наружу»Локальный кэш dual logging остаётся
Длинные записи JSON в логахУдобно положить всёРазрыв на 16 КиБ ломает разбор
Оставляют blockingЗначение по умолчаниюМедленный приёмник вешает приложение
Ставят non-blocking без измерения потерьКажется безопаснымСчитать строки в приложении и сравнивать
Ищут разрыв строк через docker logsЕстественный инструментОн склеивает фрагменты; смотреть файл
Смешивают stdout и stderr в анализеdocker logs даёт обаРазделять: 2>/dev/null или 1>/dev/null

Контрольные вопросы

На понимание:

  1. Проследите путь строки от print() до docker logs. Где она может задержаться и где потеряться?
  2. Почему json-file без настройки заполняет диск, а local — нет?
  3. Что происходит со строкой длиннее 16 КиБ и почему это не видно в docker logs?
  4. Чем blocking отличается от non-blocking и какова цена каждого?
  5. Что такое dual logging и какое неочевидное следствие он имеет для диска?

На применение:

  1. Как рассчитать бюджет диска под логи для стека из восьми сервисов?
  2. Как настроить логирование Python так, чтобы ничего не терялось при падении?
  3. Как измерить, теряются ли строки в режиме non-blocking?

На диагностику:

  1. Приложение «висит», профилировщик показывает ожидание в write. Гипотеза?
  2. Агрегатор получает битые записи JSON, а docker logs показывает их корректно. Причина?

Краткое резюме

  1. Приложение пишет в дескриптор 1; файл создаёт daemon, а не приложение.
  2. Логи не пишут в файл внутри container: он исчезнет вместе с container.
  3. Без PYTHONUNBUFFERED stdout буферизуется блоками и теряется при падении.
  4. stderr не буферизуется — поэтому при сбое от него больше пользы.
  5. json-file не ротирует по умолчанию; local ротирует (20 МиБ × 5).
  6. Бюджет диска на container: max-size × max-file.
  7. Настройка в daemon.json применяется только к новым container'ам.
  8. Docker читает поток кусками по 16 КиБ; длинная строка разрывается на фрагменты.
  9. docker logs склеивает фрагменты — проблему видит только агрегатор.
  10. Режим blocking при медленном приёмнике блокирует write() в приложении.
  11. Режим non-blocking не блокирует, но отбрасывает строки при переполнении буфера.
  12. Dual logging позволяет docker logs с удалёнными драйверами — и тоже расходует диск.

Официальные источники

ИсточникСсылкаЧто подтверждает
Docker: logging drivershttps://docs.docker.com/engine/logging/configure/Список драйверов, настройка
Docker: json-file driverhttps://docs.docker.com/engine/logging/drivers/json-file/max-size, max-file, compress
Docker: local driverhttps://docs.docker.com/engine/logging/drivers/local/Ротация по умолчанию
Docker: dual logginghttps://docs.docker.com/engine/logging/dual-logging/Локальный кэш и его настройка
Docker: delivery modeshttps://docs.docker.com/engine/logging/configure/#configure-the-delivery-mode-of-log-messages-from-container-to-log-driverblocking и non-blocking
Docker: docker logshttps://docs.docker.com/reference/cli/docker/container/logs/Флаги чтения
Python: -u и PYTHONUNBUFFEREDhttps://docs.python.org/3/using/cmdline.html#envvar-PYTHONUNBUFFEREDОтключение буферизации
Compose: logginghttps://docs.docker.com/reference/compose-file/services/#loggingНастройка в Compose

Навигация

Вернуться к разделу
Следующий материал → Inspect, events, stats
Главное оглавление

Markdown на GitHub ↗