Esvaziar um arquivo sem interromper a gravação do pipe nele

12

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

./my_app > log

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

cat "" > log

No entanto, sempre parece que o canal 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 cospe no stdout e eu quero salvá-lo em um log para que eu possa inspecioná-lo quando precisar e limpá-lo quando quiser. No entanto, não preciso reiniciar o aplicativo.

bangnab
fonte
É por isso que você costuma usar um daemon de registro para registrar as coisas ...
kiwy
@ Kiwy, você pode explicar como isso resolveria o problema?
bangnab
bem, você geralmente usa um daemon de log ou deixa seu aplicativo lidar com o log, porque gravar coisas na saída e redirecioná-lo não é confiável. você deve dar uma olhada syslogdoulogrotate
kiwy
2
As coisas funcionam se você o fizer ./my_app >> log(para forçar a adição) e cp /dev/null logtruncá-lo?
Mark Plotnick
1
Que mensagem de erro você recebe? Que comportamento você vê? "Não redireciona sua saída para o arquivo de log" não é muito específico. Além disso, cat "" > lognão é um catcomando válido, pois não há arquivo chamado "".
Mikel

Respostas:

13

Outra forma desse problema ocorre com aplicativos de execução longa 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 log real, se o processo estiver mantendo o arquivo aberto, ele acabará gravando 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 maneira) é implementar um manipulador de sinais no processo que fechará e reabrirá seus logs. Então, sempre que você desejar mover ou limpar (excluindo) o log, envie esse sinal para o processo imediatamente depois.

Aqui está uma demonstração simples do bash - perdoe minhas habilidades cruas de shell (mas se você quiser editar isso para obter boas práticas etc.), entenda a funcionalidade primeiro e teste sua revisão antes de editar:

#!/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 isso entrando em segundo plano:

> ./test.sh &
12356

Observe que ele reporta seu PID ao terminal e começa a fazer logon no log.txt. Agora você tem 2 minutos para brincar. Aguarde alguns segundos e tente:

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

Simplesmente kill -2 12356pode funcionar para você aqui também. O sinal 2 é SIGINT (também é o que Ctrl-C faz, para que você possa tentar isso em primeiro plano e mover ou remover o arquivo de log de outro terminal), que trapdeve ser interceptado. Checar;

> 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 ele ainda está gravando em um log.txtarquivo, mesmo que o 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 ele continuou indo exatamente de onde parou. Se você não deseja manter o registro, basta limpar o registro excluindo-o

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

Verifica:

> 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 indo.

Você não pode fazer isso em um script de shell para um subprocesso executado, infelizmente, porque, se estiver em primeiro plano, os manipuladores de sinal do bash trapserão suspensos e, se você o colocar em segundo plano, não poderá redesigná-lo. resultado. Ou seja, isso é algo que você precisa implementar em seu aplicativo.

Contudo...

Se você não pode modificar o aplicativo (por exemplo, porque você não o escreveu), eu tenho um utilitário CLI que você pode usar como intermediário. Você também pode implementar uma versão simples disso em um script que serve como 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 assim 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

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

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

Verifica:

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

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

Cachinhos Dourados
fonte
Obrigado pelas boas explicações. No entanto, no meu caso, não posso modificar o aplicativo para implementar sua solução.
bangnab
2
Se você não conseguir implementar um manipulador de sinal em seu aplicativo (porque não pode modificá-lo), poderá usar esta técnica para canalizar o log através de uma interceptação de sinal - veja as coisas depois de "No entanto ..."
goldilocks
Ok, vou tentar e informá-lo como foi.
bangnab
Eu finalmente tenho um aplicativo CLI escrito em C para este (desculpe demorou um pouco mais do que inicialmente previsto): cognitivedissonance.ca/cogware/pipelog
goldilocks
6

TL; DR

Abra seu arquivo de log no modo de acréscimo :

cmd >> log

Em seguida, você pode truncá-lo com segurança com:

: > log

Detalhes

Com um shell semelhante ao Bourne, existem três maneiras principais de abrir um arquivo para gravação. No modo somente gravação ( >), leia + escreva ( <>) ou acrescente (e somente gravação >>).

Nos dois primeiros, o kernel se lembra da posição atual em que você (por você, quero dizer, a descrição do arquivo aberto , compartilhada por todos os descritores de arquivo que o duplicaram ou herdaram ao bifurcar aquele em que você abriu o arquivo) está no diretório Arquivo.

Quando você faz:

cmd > log

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

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

Por exemplo, se gravar cmdinicialmente zzz, a posição será no deslocamento de bytes 4 no arquivo e, na próxima vez em que cmdseus filhos gravarem no arquivo, é onde os dados serão gravados, independentemente de o arquivo ter aumentado ou diminuído no intervalo .

Se o arquivo encolheu, por exemplo, se ele foi truncado com um

: > log

e cmdgravações xx, elas xxserão gravadas em offset 4e os 3 primeiros caracteres serão substituídos por 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  \0  \0  \0  \0  \0   x
0000006

Isso significa que você não pode truncar um arquivo que foi aberto no modo somente gravação (e é o mesmo para leitura + gravação ) como se o fizesse, processos que tinham descritores de arquivo abertos no arquivo, deixarão caracteres NUL no início do arquivo (aqueles, exceto no 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 escreve nos arquivos de log), você deve abrir o arquivo no modo de acréscimo :

cmd >> log

ou

: > log && cmd >> log

se você deseja iniciar em um arquivo vazio.

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

$ 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

Isso também é mais seguro, como se dois processos tivessem aberto (dessa maneira) o arquivo por engano (como por exemplo, se você iniciou duas instâncias do mesmo daemon), a saída deles não será substituída.

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

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

Ou com:

$ 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

Esses sinalizadores correspondem aos sinalizadores O ..._ passados ​​para a openchamada do sistema.

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

( O_APPENDé 0x400 ou octal 02000)

Portanto, o shell >>abre o arquivo com O_WRONLY|O_APPEND(e 0100000 aqui é O_LARGEFILE, o que não é relevante para esta questão) enquanto >é O_WRONLYapenas (e <>é O_RDWRapenas).

Se você fizer um:

sudo lsof -nP +f g | grep ,AP

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

Stéphane Chazelas
fonte
Por que você usa o :(dois pontos) em : > ?
mvorisek
1
@Mvorisek, isso é para redirecionar a saída do comando que não produz nenhuma saída: :. Sem um comando, o comportamento varia entre conchas.
Stéphane Chazelas
1

Se estou entendendo corretamente, teeparece 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
bispo
fonte
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 redirecione o log para ele ./my_app >> log$date.log

Charles nakhel
fonte
Eu gostaria de poder girar sob demanda. Na verdade, este é um log produzido durante um teste automatizado e eu gostaria de limpá-lo antes de executar o teste.
bangnab
0

Este é um problema que há muito tempo foi resolvido com o syslog (em todas as suas variantes), mas existem duas ferramentas que resolveriam seu problema específico com o mínimo de esforço.

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

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

hildred
fonte