Opróżnianie pliku bez zakłócania zapisywania do niego potoku

12

Mam program, którego dane wyjściowe przekierowuję do pliku dziennika:

./my_app > log

Chciałbym od czasu do czasu wyczyścić (tj. Opróżnić) dziennik (na żądanie) i wypróbować różne rzeczy

cat "" > log

Jednak zawsze wydaje się, że oryginalny potok jest wtedy zakłócany, a program nie przekierowuje już swojego wyjścia do pliku dziennika.

Czy jest na to jakiś sposób?

Aktualizacja

Pamiętaj, że nie mogę modyfikować aplikacji generującej dane wyjściowe. Po prostu wyrzuca go na standardowe wyjście i chcę zapisać go w dzienniku, aby móc go sprawdzić, kiedy jest to potrzebne, i wyczyścić, kiedy chcę. Nie powinienem jednak ponownie uruchamiać aplikacji.

Bangnab
źródło
dlatego zwykle używasz demona logowania do rejestrowania rzeczy ...
Kiwy,
@ Kiwi, czy możesz opracować sposób rozwiązania tego problemu?
Bangnab
zazwyczaj używasz demona dziennika lub pozwalasz aplikacji obsługiwać dziennik, ponieważ pisanie danych do wyjścia i przekierowywanie go nie jest niezawodne. powinieneś spojrzeć na syslogdlublogrotate
Kiwy
2
Czy coś działa, jeśli to robisz ./my_app >> log(aby wymusić dołączanie) i cp /dev/null logobciąć?
Mark Plotnick
1
Jaki komunikat o błędzie pojawia się? Jakie widzisz zachowanie? „Nie przekierowuje już swoich danych wyjściowych do pliku dziennika” nie jest zbyt szczegółowe. Ponadto cat "" > lognie jest prawidłowym catpoleceniem, ponieważ nie ma wywołanego pliku "".
Mikel

Odpowiedzi:

13

Inna postać tego problemu występuje w przypadku długo działających aplikacji, których dzienniki są okresowo obracane. Nawet jeśli przeniesiesz oryginalny dziennik (np. mv log.txt log.1) I zastąpisz go natychmiast plikiem o tej samej nazwie, zanim nastąpi rzeczywiste rejestrowanie, jeśli proces utrzymuje plik otwarty, albo skończy się na zapisie log.1(ponieważ nadal może to być otwarta i-węzeł) lub do zera.

Częstym sposobem radzenia sobie z tym (sam logger systemu działa w ten sposób) jest zaimplementowanie procedury obsługi sygnału w procesie, który zamyka i ponownie otwiera swoje dzienniki. Następnie, gdy tylko chcesz przenieść lub wyczyścić (usuwając) dziennik, natychmiast wyślij ten sygnał do procesu.

Oto prosta demonstracja bash - wybacz moje umiejętności z grubej skorupy (ale jeśli zamierzasz edytować to dla najlepszych praktyk itp., Upewnij się, że najpierw rozumiesz funkcjonalność i przetestujesz swoją wersję przed edycją):

#!/bin/bash

trap sighandler INT

function sighandler () {
    touch log.txt
    exec &> log.txt
}

echo $BASHPID
exec &> log.txt

count=0;
while [ $count -lt 60 ]; do
    echo "$BASHPID Count is now $count"
    sleep 2
    ((count++))
done          

Rozpocznij od przejścia do tła:

> ./test.sh &
12356

Zauważ, że zgłasza swój PID do terminala, a następnie zaczyna się logować log.txt. Masz teraz 2 minuty na zabawę. Poczekaj kilka sekund i spróbuj:

> mv log.txt log.1 && kill -s 2 12356

Po prostu kill -2 12356może tu również działać. Sygnał 2 to SIGINT (to też robi Ctrl-C, więc możesz wypróbować to na pierwszym planie i przenieść lub usunąć plik dziennika z innego terminala), który trappowinien zatrzymać. Sprawdzić;

> cat log.1
12356 Count is now 0
12356 Count is now 1
12356 Count is now 2
12356 Count is now 3
12356 Count is now 4
12356 Count is now 5
12356 Count is now 6
12356 Count is now 7
12356 Count is now 8
12356 Count is now 9
12356 Count is now 10
12356 Count is now 11
12356 Count is now 12
12356 Count is now 13
12356 Count is now 14

Zobaczmy teraz, czy nadal pisze do, log.txtmimo że go przenieśliśmy:

> cat log.txt
12356 Count is now 15
12356 Count is now 16
12356 Count is now 17
12356 Count is now 18
12356 Count is now 19
12356 Count is now 20
12356 Count is now 21

Zauważ, że szło dalej tak, jak zostało przerwane. Jeśli nie chcesz przechowywać rekordu, po prostu wyczyść dziennik, usuwając go

> rm -f log.txt && kill -s 2 12356

Czek:

> cat log.txt
12356 Count is now 29
12356 Count is now 30
12356 Count is now 31
12356 Count is now 32
12356 Count is now 33
12356 Count is now 34
12356 Count is now 35
12356 Count is now 36

Wciąż idzie.

Nie można tego zrobić w skrypcie powłoki dla wykonanego podprocesu, niestety, ponieważ jeśli jest on na pierwszym planie, własne procedury obsługi sygnałów bash trapsą zawieszone, a jeśli rozwidlisz go w tle, nie możesz ponownie przypisać jego wynik. Tj. Musisz to zaimplementować w swojej aplikacji.

Jednak...

Jeśli nie możesz zmodyfikować aplikacji (np. Ponieważ jej nie napisałeś), mam narzędzie CLI, którego możesz użyć jako pośrednika. Możesz również zaimplementować prostą wersję tego w skrypcie, który służy jako potok do dziennika:

#!/bin/bash

trap sighandler INT

function sighandler () {
    touch log.txt
    exec 1> log.txt
}

echo "$0 $BASHPID"
exec 1> log.txt

count=0;
while read; do
    echo $REPLY
done  

Nazwijmy to pipetrap.sh. Teraz potrzebujemy osobnego programu do testowania, naśladując aplikację, którą chcesz zalogować:

#!/bin/bash

count=0
while [ $count -lt 60 ]; do
    echo "$BASHPID Count is now $count"
    sleep 2
    ((count++))
done           

To będzie test.sh:

> (./test.sh | ./pipetrap.sh) &
./pipetrap.sh 15859

Są to dwa oddzielne procesy z osobnymi PID. Aby wyczyścić test.shdane wyjściowe, które są wprowadzane poprzez pipetrap.sh:

> rm -f log.txt && kill -s 2 15859

Czek:

>cat log.txt
15858 Count is now 6
15858 Count is now 7
15858 Count is now 8

15858,, test.shnadal działa, a jego dane wyjściowe są rejestrowane. W takim przypadku nie są wymagane żadne modyfikacje aplikacji.

Złotowłosa
źródło
Dzięki za miłe wyjaśnienia. Jednak w moim przypadku nie mogę zmodyfikować aplikacji w celu wdrożenia Twojego rozwiązania.
bangnab
2
Jeśli nie można zaimplementować obsługi sygnału w danej aplikacji (ponieważ nie można go zmodyfikować okres), można użyć tej techniki do rury dziennik przez pułapkę sygnału - patrz rzeczy po „Jednakże ...”
Złotowłosa
Ok spróbuję i dam znać, jak poszło.
Bangnab
W końcu mam w tym celu napisaną w C aplikację CLI (przepraszam, że zajęło to trochę więcej czasu niż pierwotnie zamierzano): cognitivedissonance.ca/cogware/pipelog
goldilocks
6

TL; DR

Otwórz plik dziennika w trybie dołączania :

cmd >> log

Następnie możesz bezpiecznie obciąć go za pomocą:

: > log

Detale

Dzięki powłoce podobnej do Bourne'a istnieją 3 główne sposoby otwierania pliku do zapisu. W trybie tylko do zapisu ( >), odczytu + zapisu ( <>) lub dołączania (i tylko zapisu >>).

W pierwszych dwóch jądrach zapamiętuje bieżącą pozycję, którą ty (mam na myśli, opis otwartego pliku , udostępniony przez wszystkie deskryptory plików, które zduplikowały lub odziedziczyły go, przechodząc od tego, w którym otworzyłeś plik) jesteś w plik.

Kiedy to zrobisz:

cmd > log

logjest otwarty w trybie tylko do zapisu przez powłokę dla standardowego wejścia cmd.

cmd(jego początkowy proces został zaszczepiony przez powłokę i wszystkie możliwe dzieci) podczas pisania na standardowe wyjście, pisz w bieżącej pozycji kursora utrzymywanej przez otwarty opis pliku, który udostępniają w tym pliku.

Na przykład, jeśli cmdpoczątkowo zapisuje zzz, pozycja będzie w przesunięciu bajtu 4 do pliku, a następnym razem cmdlub jego dzieci zapisują do pliku, to tam zapisywane będą dane bez względu na to, czy plik urósł, czy zmniejszył się w tym przedziale .

Jeśli plik się skurczył, na przykład jeśli został obcięty za pomocą

: > log

i cmdpisze xx, xxzostaną one zapisane z przesunięciem 4, a pierwsze 3 znaki zostaną zastąpione znakami NUL.

$ exec 3> log # open file on fd 3.
$ printf zzz >&3
$ od -c log
0000000   z   z   z
0000003
$ printf aaaa >> log # other open file description -> different cursor
$ od -c log
0000000   z   z   z   a   a   a   a
0000007
$ printf bb >&3 # still write at the original position
$ od -c log
0000000   z   z   z   b   b   a   a
0000007
$ : > log
$ wc log
0 0 0 log
$ printf x >&3
$ od -c log
0000000  \0  \0  \0  \0  \0   x
0000006

Oznacza to, że nie można obciąć pliku, który został otwarty w trybie tylko do zapisu (i to samo dotyczy odczytu i zapisu ), tak jak w przypadku procesów, w których deskryptory plików były otwarte na pliku, pozostawi znaki NUL na początku plik (te, z wyjątkiem OS / X, zwykle nie zajmują miejsca na dysku, stają się rzadkimi plikami).

Zamiast tego (a zauważysz, że większość aplikacji robi to podczas zapisywania plików dziennika), powinieneś otworzyć plik w trybie dołączania :

cmd >> log

lub

: > log && cmd >> log

jeśli chcesz zacząć od pustego pliku.

W trybie dołączania wszystkie zapisy są wykonywane na końcu pliku, niezależnie od tego, gdzie był ostatni zapis:

$ exec 4>> log
$ printf aa >&4
$ printf x >> log
$ printf bb >&4
$ od -c log
0000000   a   a   x   b   b
0000005
$ : > log
$ printf cc >&4
$ od -c log
0000000   c   c
0000002

Jest to również bezpieczniejsze, ponieważ jeśli dwa procesy otworzyły (w ten sposób) plik przez pomyłkę (na przykład, jeśli uruchomiłeś dwa wystąpienia tego samego demona), ich dane wyjściowe się nie zastąpią.

W najnowszych wersjach systemu Linux można sprawdzić bieżącą pozycję i sprawdzić, czy deskryptor pliku został otwarty w trybie dołączania , patrząc na /proc/<pid>/fdinfo/<fd>:

$ cat /proc/self/fdinfo/4
pos:        2
flags:      0102001

Lub z:

$ lsof +f G -p "$$" -ad 4
COMMAND  PID USER   FD   TYPE  FILE-FLAG DEVICE SIZE/OFF     NODE NAME
zsh     4870 root    4w   REG 0x8401;0x0 252,18        2 59431479 /home/chazelas/log
~# lsof +f g -p "$$" -ad 4
COMMAND  PID USER   FD   TYPE FILE-FLAG DEVICE SIZE/OFF     NODE NAME
zsh     4870 root    4w   REG   W,AP,LG 252,18        2 59431479 /home/chazelas/log

Te flagi odpowiadają flagom O ..._ przekazanym do openwywołania systemowego.

$ gcc -E - <<< $'#include <fcntl.h>\nO_APPEND O_WRONLY' | tail -n1
02000 01

( O_APPENDjest 0x400 lub ósemkowy 02000)

Więc powłoka >>otwiera plik za pomocą O_WRONLY|O_APPEND(a 0100000 to O_LARGEFILE, co nie ma związku z tym pytaniem), podczas gdy >jest O_WRONLYtylko (i <>jest O_RDWRtylko).

Jeśli wykonasz:

sudo lsof -nP +f g | grep ,AP

Aby wyszukać pliki otwarte za pomocą O_APPEND, znajdziesz większość plików dziennika aktualnie otwartych do zapisu w systemie.

Stéphane Chazelas
źródło
Dlaczego używasz :(dwukropka) w : > ?
mvorisek
1
@Mvorisek, to przekierować dane wyjściowe komendy, która nie tworzy żadnego wyjścia: :. Bez polecenia zachowanie różni się w zależności od powłoki.
Stéphane Chazelas
1

Jeśli dobrze rozumiem, teewydaje się to rozsądnym podejściem:

$ ./myapp-that-echoes-the-date-every-second | tee log > /dev/null &
[1] 20519
$ head log
Thu Apr  3 11:29:34 EDT 2014
Thu Apr  3 11:29:35 EDT 2014
Thu Apr  3 11:29:36 EDT 2014
$ > log
$ head log
Thu Apr  3 11:29:40 EDT 2014
Thu Apr  3 11:29:41 EDT 2014
Thu Apr  3 11:29:42 EDT 2014
biskup
źródło
1

Jako szybkie rozwiązanie można użyć dziennika z rotacją (na przykład rotacja dzienna):

date=`date +%Y%m%d`
LOGFILE=/home/log$date.log

i przekieruj do niego logowanie ./my_app >> log$date.log

Charles Nakhel
źródło
Chciałbym móc obracać na żądanie. To właściwie dziennik tworzony podczas automatycznego testu i chciałbym go wyczyścić przed uruchomieniem testu.
Bangnab
0

Jest to problem, który od dawna został rozwiązany za pomocą syslog (we wszystkich jego wariantach), ale istnieją dwa narzędzia, które rozwiązałyby konkretny problem przy minimalnym wysiłku.

Pierwszym, bardziej przenośnym, ale mniej wszechstronnym rozwiązaniem jest rejestrator (niezbędny dla każdego zestawu narzędzi dla administratorów). Jest to proste narzędzie, które kopiuje standardowe dane wejściowe do syslog. (przekazanie złotówki i uczynienie z obracania pliku problemu logrotate i syslog)

Drugim, bardziej eleganckim, ale mniej przenośnym rozwiązaniem jest syslog-ng, który oprócz przyjmowania komunikatów dziennika ze standardowych gniazd syslog może uruchamiać programy, których dane wyjściowe są filtrowane przez rejestrator. (Nie korzystałem jeszcze z tej funkcji, ale wygląda ona idealnie do tego, co chcesz zrobić).

Hildred
źródło