Esvaziando um arquivo sem interromper o pipe escrevendo para ele

9

Eu tenho um programa cuja saída eu redireciono para um arquivo de log:

./my_app > log

Eu gostaria de limpar (ou seja, esvaziar) o registro de tempos em tempos (sob demanda) e tentei várias coisas como

cat "" > log

No entanto, parece que o pipe original é interrompido e o programa não redireciona sua saída para o arquivo de log.

Existe alguma maneira de fazer isso?

Atualizar

Observe que não posso modificar o aplicativo que produz a saída. Ele apenas mostra o stdout e eu quero salvá-lo em um log para que eu possa inspecioná-lo quando precisar, e desmarcá-lo quando quiser. No entanto, não preciso reiniciar o aplicativo.

    
por inovaovao 03.04.2014 / 12:52

5 respostas

11

Outra forma deste problema ocorre com aplicativos de longa execução cujos logs são rotacionados periodicamente. Mesmo se você mover o log original (por exemplo, mv log.txt log.1 ) e substituí-lo imediatamente por um arquivo com o mesmo nome antes que ocorra qualquer registro real, se o processo estiver mantendo o arquivo aberto, ele acabará gravando em log.1 (porque isso ainda pode ser o inode aberto) ou para nada.

Uma maneira comum de lidar com isso (o próprio criador de logs do sistema funciona dessa forma) é implementar um manipulador de sinal no processo que fechará e reabrirá seus logs. Então, sempre que você quiser mover ou limpar (excluindo) o log, envie esse sinal para o processo imediatamente depois.

Aqui está uma demonstração simples para o bash - perdoe minhas habilidades de casca rudimentares (mas se você for editar isto para melhores práticas, etc., por favor, certifique-se de entender a funcionalidade primeiro e testar sua revisão antes você edita):

#!/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          

Comece por colocar em segundo plano:

> ./test.sh &
12356

Observe que ele informa seu PID para o terminal e começa a registrar em log.txt . Agora você tem 2 minutos para brincar. Aguarde alguns segundos e tente:

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

Apenas kill -2 12356 pode funcionar para você também. O sinal 2 é SIGINT (também é o que o Ctrl-C faz, então você pode tentar isso em primeiro plano e mover ou remover o arquivo de log de outro terminal), que o trap deve capturar. Para verificar;

> 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

Agora vamos ver se ainda está escrevendo para um log.txt , embora tenhamos movido:

> 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

Observe que continuou indo exatamente de onde parou. Se você não quiser manter o registro, limpe o registro excluindo-o

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

Verifique:

> 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

Ainda vai.

Você não pode fazer isto em um shell script para um subprocesso executado, infelizmente, porque se ele estiver em primeiro plano, os manipuladores de sinal do bash ( trap s) serão suspensos, e se você forçar o plano em segundo plano, você não pode reatribuir sua saída. Ou seja, isso é algo que você precisa implementar em seu aplicativo.

No entanto ...

Se você não pode modificar o aplicativo (por exemplo, porque você não o escreveu), eu tenho um utilitário CLI você pode usar como intermediário. Você também pode implementar uma versão simples disso em um script que serve como um canal para o log:

#!/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  

Vamos chamar isso de pipetrap.sh . Agora precisamos de um programa separado para testar, imitando o aplicativo que você deseja registrar:

#!/bin/bash

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

Isso será test.sh :

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

Estes são dois processos separados com PIDs separados. Para limpar a saída de test.sh , que está sendo canalizada através de pipetrap.sh :

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

Verifique:

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

15858, test.sh , ainda está em execução e sua saída está sendo registrada. Nesse caso, nenhuma modificação no aplicativo é necessária.

    
por 03.04.2014 / 14:30
5

TL; DR

Abra seu arquivo de log no modo append :

cmd >> log

Em seguida, você pode truncar com segurança:

: > log

Detalhes

Com um shell parecido com o Bourne, existem três maneiras principais de abrir um arquivo para gravação. Em somente gravação ( > ), ler + gravar ( <> ) ou anexar (e somente gravação, >> ).

Nos dois primeiros, o kernel lembra a posição atual de você (por você, quero dizer, a descrição do arquivo aberto , compartilhada por todos os descritores de arquivo que duplicaram ou herdaram o arquivo por bifurcação do em que você abriu o arquivo) estão no arquivo.

Quando você faz:

cmd > log

log está aberto no modo somente gravação pelo shell para o stdout de cmd .

cmd (seu processo inicial gerado pelo shell e todos os possíveis filhos) ao gravar em seu stdout, escreve na posição atual do cursor mantida pela descrição do arquivo aberto que ele compartilha nesse arquivo .

Por exemplo, se cmd inicialmente gravar zzz , a posição será em byte offset 4 no arquivo, e na próxima vez que cmd ou seus filhos gravarem no arquivo, é aí que os dados serão gravados independentemente de o arquivo ter crescido ou encolhido no intervalo.

Se o arquivo foi reduzido, por exemplo, se ele foi truncado com

: > log

e cmd escrevem xx , esses xx serão gravados no deslocamento 4 e os 3 primeiros caracteres serão substituídos pelos caracteres 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  
cmd >> log
: > log && cmd >> log
$ 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
$ cat /proc/self/fdinfo/4
pos:        2
flags:      0102001
$ 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
x 0000006

Isso significa que você não pode truncar um arquivo que tenha sido aberto no modo somente gravação (e isso é o mesmo para leitura + gravação ) como se você o fizesse, processos que tivessem descritores de arquivos abertos no arquivo , vai deixar os caracteres NUL no início do arquivo (aqueles, exceto em OS / X, normalmente não ocupam espaço no disco, eles se tornam arquivos esparsos).

Em vez disso (e você notará que a maioria dos aplicativos faz isso quando eles gravam em arquivos de log), você deve abrir o arquivo no modo append :

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

ou

sudo lsof -nP +f g | grep ,AP

se você quiser começar um arquivo vazio.

No modo append, todas as gravações são feitas no final do arquivo, independentemente de onde a última gravação foi:

cmd >> log

Isso também é mais seguro como se dois processos tivessem aberto (dessa forma) o arquivo por engano (como por exemplo, se você iniciou duas instâncias do mesmo daemon), sua saída não irá sobrescrever uma à outra.

Em versões recentes do Linux, você pode verificar a posição atual e se um descritor de arquivo foi aberto no modo append olhando /proc/<pid>/fdinfo/<fd> :

: > log

Ou com:

cmd > log

Esses sinalizadores correspondem aos O ... _ sinalizadores passados para a chamada de sistema open .

: > log

( O_APPEND é 0x400 ou octal 02000)

Portanto, o >> do shell abre o arquivo com O_WRONLY|O_APPEND (e 0100000 aqui é O_LARGEFILE, que não é relevante para essa pergunta), enquanto > é O_WRONLY apenas (e <> é O_RDWR ) .

Se você fizer um:

$ 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  
cmd >> log
: > log && cmd >> log
$ 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
$ cat /proc/self/fdinfo/4
pos:        2
flags:      0102001
$ 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
x 0000006

para procurar por arquivos abertos com O_APPEND , você encontrará a maioria dos arquivos de registro atualmente abertos para gravação em seu sistema.

    
por 14.08.2014 / 08:49
1

Se eu estiver entendendo corretamente, tee parece uma abordagem razoável:

$ ./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
    
por 03.04.2014 / 17:30
1

Como solução rápida, você pode usar um log com rotação (rotação diária, por exemplo):

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

e redirecionar o registro de log para ele ./my_app >> log$date.log

    
por 03.04.2014 / 14:23
0

Este é um problema que tem sido resolvido com o syslog (em todas as suas variantes), mas existem duas ferramentas que solucionam seu problema com um mínimo de esforço.

A primeira solução, mais portátil mas menos versátil, é o logger (indispensável para qualquer caixa de ferramentas de administradores). É um utilitário simples que copia a entrada padrão para o syslog. (passando o dinheiro e fazendo a rotação do arquivo o problema de logrotate e syslog)

A segunda solução, mais elegante mas menos portátil, é o syslog-ng, que além de aceitar mensagens de log dos syslog padrão, pode executar programas cuja saída é filtrada pelo logger. (Eu não usei esse recurso ainda, mas parece perfeito para o que você está querendo fazer.)

    
por 03.04.2014 / 17:35