Como capturar STDOUT / STDERR ordenado e adicionar timestamp / prefixos?

18

Eu explorei quase todos available semelhante perguntas , sem sucesso.

Deixe-me descrever o problema em detalhes:

Eu executo alguns scripts autônomos e eles podem produzir saídas padrão e linhas de erro padrão, eu quero capturá-los em sua ordem precisa conforme exibido por um emulador de terminal e então adicionar um prefixo como "STDERR: "e" STDOUT: "para eles.

Eu tentei usar canos e até mesmo abordagem baseada em epoll neles, sem sucesso. Eu acho que a solução está em uso pty, embora eu não sou mestre nisso. Eu também espiei no código-fonte do VTE do Gnome , mas isso não tem sido muito produtivo.

O ideal seria usar Ir em vez do Bash para fazer isso, mas não fui capaz de. Parece que os pipes proíbem automaticamente manter uma ordem de linhas correta por causa do buffer.

Alguém conseguiu fazer algo semelhante? Ou é simplesmente impossível? Eu acho que se um emulador de terminal pode fazer isso, então não é - talvez criando um pequeno programa em C gerenciando o PTY (s) diferentemente?

O ideal seria usar uma entrada assíncrona para ler esses dois fluxos (STDOUT e STDERR) e depois reimprimi-los segundo minhas necessidades, mas a ordem de entrada é crucial!

NOTA: estou ciente de stderred mas não funciona para mim com scripts Bash e não pode ser facilmente editado para adicionar um prefixo (já que basicamente envolve vários syscalls).

Atualização: adicionada abaixo de duas ideias

(atrasos aleatórios abaixo de segundos podem ser adicionados no script de amostra que forneci para provar um resultado consistente)

Atualização: a solução para essa questão também resolveria essa outra pergunta , como apontou @Gilles. No entanto, cheguei à conclusão de que não é possível fazer o que pedimos aqui e ali. Ao usar 2>&1 , ambos os fluxos são mesclados corretamente no nível pty / pipe, mas para usar os fluxos separadamente e na ordem correta, deve-se usar a abordagem de stderred que invoca o syscall e pode ser visto como sujo de várias maneiras.

Eu estarei ansioso para atualizar esta pergunta se alguém puder refutar o que foi dito acima.

    
por Deim0s 26.09.2014 / 12:26

2 respostas

9

Você pode usar coprocessos. Invólucro simples que alimenta as duas saídas de um determinado comando para duas sed instances (uma para stderr a outra para stdout ), que faz a marcação.

#!/bin/bash
exec 3>&1
coproc SEDo ( sed "s/^/STDOUT: /" >&3 )
exec 4>&2-
coproc SEDe ( sed "s/^/STDERR: /" >&4 )
eval $@ 2>&${SEDe[1]} 1>&${SEDo[1]}
eval exec "${SEDo[1]}>&-"
eval exec "${SEDe[1]}>&-"

Observe várias coisas:

  1. É um encantamento mágico para muitas pessoas (incluindo eu) - por uma razão (veja a resposta abaixo).

  2. Não há garantia de que não irá ocasionalmente trocar algumas linhas - tudo depende do agendamento dos coprocessos. Na verdade, é quase garantido que em algum momento isso acontecerá. Dito isso, se a ordem for estritamente a mesma, você terá que processar os dados de stderr e stdin no mesmo processo, caso contrário, o agendador do kernel pode (e vai) fazer uma bagunça.

    Se eu entendi o problema corretamente, isso significa que você precisaria instruir o shell a redirecionar ambos os fluxos para um processo (o que pode ser feito pelo AFAIK). O problema começa quando esse processo começa a decidir sobre o que atuar primeiro - ele teria que pesquisar as duas fontes de dados e, em algum momento, chegar ao estado em que estaria processando um fluxo e os dados chegariam aos dois fluxos antes de terminar. E é exatamente aí que ele se desfaz. Isso também significa que agrupar os syscalls de saída como stderred é provavelmente a única maneira de alcançar o resultado desejado (e, mesmo assim, você pode ter um problema quando algo se torna multissegmentado em um sistema multiprocessador).

No que diz respeito aos coprocessos, leia a excelente resposta de Stéphane em Como você usa o comando coproc no Bash? para uma visão mais aprofundada.

    
por 26.09.2014 / 12:56
4

Método 1. Usando descritores de arquivos e awk

E algo assim usando as soluções deste SO Q & A intitulado: Existe um utilitário Unix para preceder timestamps a linhas de texto? e este SO Q & A intitulado: canaliza STDOUT e STDERR para dois processos diferentes no shell script? .

A abordagem

Passo 1, criamos 2 funções no Bash que irão executar a mensagem de timestamp quando chamado.

$ msgOut () {  awk '{ print strftime("STDOUT: %Y-%m-%d %H:%M:%S"), $0; fflush(); }'; }
$ msgErr () {  awk '{ print strftime("STDERR: %Y-%m-%d %H:%M:%S"), $0; fflush(); }'; }

Passo 2 você usaria as funções acima assim para obter a mensagem desejada:

$ { { ...command/script... } 2>&3; } 2>&3 | msgErr; } 3>&1 1>&2 | msgOut

Exemplo

Aqui eu criei um exemplo que escreve a para STDOUT, dorme por 10 segundos e grava a saída em STDERR. Quando colocamos essa sequência de comando em nossa construção acima, recebemos as mensagens conforme você especificou.

$ { { echo a; sleep 10; echo >&2 b; } 2>&3 | \
    msgErr; } 3>&1 1>&2 | msgOut
STDERR: 2014-09-26 09:22:12 a
STDOUT: 2014-09-26 09:22:22 b

Método # 2. Usando o annotate-output

Existe uma ferramenta chamada annotate-output que faz parte do pacote devscripts que fará o que você deseja. A única restrição é que ele deve executar os scripts para você.

Exemplo

Se colocarmos nossa sequência de comandos de exemplo acima em um script chamado mycmds.bash da seguinte forma:

$ cat mycmds.bash 
#!/bin/bash

echo a
sleep 10
echo >&2 b

Podemos, então, executá-lo assim:

$ annotate-output ./mycmds.bash 
09:48:00 I: Started ./mycmds.bash
09:48:00 O: a
09:48:10 E: b
09:48:10 I: Finished with exitcode 0

O formato da saída pode ser controlado pela parte do timestamp, mas não além disso. Mas é uma saída parecida com o que você está procurando, então pode se encaixar na conta.

    
por 26.09.2014 / 15:29