Analisando dados no terminal

Olá,
recentemente fazendo uma inspeção na aplicação precisei ver seu logs para entender com mais detalhes o que estava acontecendo em termos de comportamento e com isso para ser produtivo na atividade fiz uso de recursos presentes no meu terminal para realizar a atividade. Neste artigo vou compartilhar as ferramentas que usei e como elas podem ser úteis para você também. O interessante que depois de falar a respeito disso no Twitter surgiram comentários interessantes. O primeiro foi esse artigo e o segundo foi a ferramenta datamash.
AVISO: não assuma que sei isso tudo de memória e não se pressione a saber, muita coisa aqui tiver que procurar no Google que não lembrava ;D
As ferramentas
Os principais comandos que vou utilizar aqui são: cat, jq, awk, sort, tail, grep, uniq e pipelines.
cat
O cat
é um comando bem simples que é usado no geral para mostrar no terminal as informações de uma arquivo ou mais passados via argumentos, por exemplo:
$ cat hello.txt
world
Existem alguns outros recursos interessantes que valem nota para o uso que são as opções -n
e -b
:
$ cat -n hello.txt
1 world
$ cat linhas.txt
primeira linha
segunda linha
terceira linha
$ cat -b linhas.txt
1 primeira linha
2 segunda linha
3 terceira linha
Ambos mostram o número da linha com a diferença que na segunda opção as linhas em branco não são consideradas.
Bônus
Descobrir o que acontece se fizermos isso:
$ cat >a.txt<<EOF
> abcEOF
> 123
> EOF
tail
O tail
parece com o cat
mas o que ele faz é retornar os dados na ordem inversa do cat
e com limitação na quantidade de linhas. Vamos testar com o /etc/resolv.conf
:
$ cat -n /etc/resolv.conf
1 # This file is managed by man:systemd-resolved(8). Do not edit.
2 #
3 # This is a dynamic resolv.conf file for connecting local clients to the
4 # internal DNS stub resolver of systemd-resolved. This file lists all
5 # configured search domains.
6 #
7 # Run "systemd-resolve --status" to see details about the uplink DNS servers
8 # currently in use.
9 #
10 # Third party programs must not access this file directly, but only through the
11 # symlink at /etc/resolv.conf. To manage man:resolv.conf(5) in a different way,
12 # replace this symlink by a static file or a different symlink.
13 #
14 # See man:systemd-resolved.service(8) for details about the supported modes of
15 # operation for /etc/resolv.conf.
16
17 nameserver 127.0.0.53
18 options edns0
Usei o cat
aqui para termos uma visibilidade do quem no arquivo como um todo. Agora, vamos ao tail
.
$ tail /etc/resolv.conf
#
# Third party programs must not access this file directly, but only through the
# symlink at /etc/resolv.conf. To manage man:resolv.conf(5) in a different way,
# replace this symlink by a static file or a different symlink.
#
# See man:systemd-resolved.service(8) for details about the supported modes of
# operation for /etc/resolv.conf.
nameserver 127.0.0.53
options edns0
Já deu para notar que a quantidade de dados que veio foi reduzida, sabendo que são 18 linhas, para ver todas com o tail
, podemos fazer:
$ tail -n 18 /etc/resolv.conf
# This file is managed by man:systemd-resolved(8). Do not edit.
#
# This is a dynamic resolv.conf file for connecting local clients to the
# internal DNS stub resolver of systemd-resolved. This file lists all
# configured search domains.
#
# Run "systemd-resolve --status" to see details about the uplink DNS servers
# currently in use.
#
# Third party programs must not access this file directly, but only through the
# symlink at /etc/resolv.conf. To manage man:resolv.conf(5) in a different way,
# replace this symlink by a static file or a different symlink.
#
# See man:systemd-resolved.service(8) for details about the supported modes of
# operation for /etc/resolv.conf.
nameserver 127.0.0.53
options edns0
Assim como podemos fazer para ver menos linhas:
$ tail -n 2 /etc/resolv.conf
nameserver 127.0.0.53
options edns0
Bônus
Procure algum arquivo de log com escrita constante no seu computador ou servidor e execute, por exemplo: $ tail -f /var/log/syslog
.
pipelines
Acho que esse usamos e acabamos nem percebendo, quem nunca viu algum projeto livre por ai que o primeiro comando não é um curl
ou wget
para fazer download de uma aquivo, um |
e bash
. Então, esse |
é o pipelines
. O que esse comando fazer é conectar uma cadeia de comandos:
comando1 | comando2 | comando3 | ... | comandoN
O que estamos fazendo ai é que o comando1
vai gerar uma saída que vai ser entrada do comando2
que por consequência vai gerar uma saída nova que será a entrada do comando3
e assim até o comandoN
. No próximo item, jq
, vamos explorar melhor isso na prática.
Bônus
Existe uma variação do comando, |&
, que é para redirecionar as saídas de erro.
jq
É uma ferramenta bem legal feita em Node para lidarmos com JSON/string no terminal. Aqui não vamos explorar muito ela pois acho que isso pode ser um artigo só para isso de tantas coisas interessantes que ele tem. Vamos supor o seguinte JSON de uma compra em e-commerce:
{
"id": 12345,
"cliente": {
"id": 3243,
"nome": "joão da silva"
},
"estado": "aguardando pagamento",
"valor_total": 300.0,
"carrinho": {
"valor": 300.0,
"produtos": [
{
"id": 123,
"nome": "livro do dragão",
"quantidade": 1,
"valor_unitario": 150.0
},
{
"id": 312,
"nome": "copos",
"quantidade": 10,
"valor_unitario": 10.0
},
{
"id": 213,
"nome": "camisa preta",
"quantidade": 1,
"valor_unitario": 50.0
}
]
},
"entrega": {
"endereco": {
"rua": "rua x",
"numero": 123,
"complemento": "apt 101",
"cidade": "vitoria",
"estado": "es",
"cep": "29000000"
},
"valor": 0.0
},
"pagamento": {
"tipo": "boleto",
"codigo_barra": "23790504004200050320330008109206182470000019900"
}
}
Nome do cliente
$ cat compra.json | jq .cliente.nome
"joão da silva"
Listar produtos
$ cat compra.json | jq .carrinho.produtos
[
{
"id": 123,
"nome": "livro do dragão",
"quantidade": 1,
"valor_unitario": 150
},
{
"id": 312,
"nome": "copos",
"quantidade": 10,
"valor_unitario": 10
},
{
"id": 213,
"nome": "camisa preta",
"quantidade": 1,
"valor_unitario": 50
}
]
Nome dos produtos
$ cat compra.json | jq .carrinho.produtos[].nome
"livro do dragão"
"copos"
"camisa preta"
Valor da entrega
$ cat compra.json | jq .entrega.valor
0
Quantidade de itens (Bônus)
$ cat compra.json | jq -n '[inputs | .carrinho.produtos[].quantidade] | reduce .[] as $num (0; .+$num)'
12
grep
O grep
é uma comando para procurar padrões em arquivos e imprimir suas ocorrências por linha. Vamos explorar o uso grep
no arquivo: /etc/sysctl.conf
.
Todas configurações presentes relativas a IPv4
$ grep ipv4 /etc/sysctl.conf
#net.ipv4.conf.default.rp_filter=1
#net.ipv4.conf.all.rp_filter=1
#net.ipv4.tcp_syncookies=1
#net.ipv4.ip_forward=1
#net.ipv4.conf.all.accept_redirects = 0
# net.ipv4.conf.all.secure_redirects = 1
#net.ipv4.conf.all.send_redirects = 0
#net.ipv4.conf.all.accept_source_route = 0
#net.ipv4.conf.all.log_martians = 1
Todas as configurações ativas
Sabendo que #
no começo da linha significa que a linha está comentada.
$ grep -v ^# /etc/sysctl.conf
fs.inotify.max_user_watches=524288 # nao comentada
Várias linhas em branco e nota-se que a última não foi pega no grep
pois o ^
indica começo da linha, como o #
está no meio da linha o casamento de padrão é falso.
Bônus
$ grep -i --color -e 'ipv[46]' /etc/sysctl.conf
#net.ipv4.conf.default.rp_filter=1
#net.ipv4.conf.all.rp_filter=1
# Note: This may impact IPv6 TCP sessions too
#net.ipv4.tcp_syncookies=1
# Uncomment the next line to enable packet forwarding for IPv4
#net.ipv4.ip_forward=1
# Uncomment the next line to enable packet forwarding for IPv6
#net.ipv6.conf.all.forwarding=1
#net.ipv4.conf.all.accept_redirects = 0
#net.ipv6.conf.all.accept_redirects = 0
# net.ipv4.conf.all.secure_redirects = 1
#net.ipv4.conf.all.send_redirects = 0
#net.ipv4.conf.all.accept_source_route = 0
#net.ipv6.conf.all.accept_source_route = 0
#net.ipv4.conf.all.log_martians = 1
Recomendo executar para ver a opção --color
funcionando e entender melhor a -i
.
sort
Como próprio nome sugere, o sort
vai ordenar uma entrada informada por linhas. Vamos tomar o seguinte arquivo para exemplo:
$ cat ordenar.txt
zambia
nigeria
áfrica do sul
moçambique
mandagascar
93213
123123
13
3215
123
!@#!@#
5$%$%
~`''`
Você acha que conseguiria ordenar apenas no olhar de forma igual?
$ sort ordenar.txt
~`''`
!@#!@#
123
123123
13
3215
5$%$%
93213
áfrica do sul
mandagascar
moçambique
nigeria
zambia
Bônus
Considerando o seguinte arquivo:
$ cat colunas.txt
9 banana
8 maçã
7 abacate
6 uva
5 laranja
4 pêra
3 pokan
2 acerola
1 cajá
Entenda o comportamento de sort -k1 colunas.txt
e sort -k2 colunas.txt
.
uniq
O uniq
pelo nome já da sinais do que faça, o que ele faz é informar ou omitir linhas repetidas, vamos considerar o seguinte arquivo:
$ cat uniq.txt
zzzz
bbbb
zzzz
ffff
jjjj
kkkk
aaaa
zzzz
oooo
eeee
bbbb
mmmm
cccc
Fazendo o teste mais óbvio com o uniq
:
$ uniq uniq.txt
zzzz
bbbb
zzzz
ffff
jjjj
kkkk
aaaa
zzzz
oooo
eeee
bbbb
mmmm
cccc
O mesmo retorno! Vamos ler a documentação na man pages
:
Filter adjacent matching lines from INPUT (or standard input), writing to OUTPUT (or standard output).
Então, para ser efetivo no uso do uniq
precisamos de usar o sort antes, vejamos:
$ sort uniq.txt | uniq
aaaa
bbbb
cccc
eeee
ffff
jjjj
kkkk
mmmm
oooo
zzzz
Agora sim conseguimos ver o uso efetivo do uniq
.
Vendo apenas os dados repetidos
$ sort uniq.txt | uniq -d
bbbb
zzzz
Vendo apenas os únicos
$ sort uniq.txt | uniq -u
aaaa
cccc
eeee
ffff
jjjj
kkkk
mmmm
oooo
Bônus
Contanto as ocorrências:
$ sort uniq.txt | uniq -c
1 aaaa
2 bbbb
1 cccc
1 eeee
1 ffff
1 jjjj
1 kkkk
1 mmmm
1 oooo
3 zzzz
awk
Acho que melhor do que criar uma explicação do que é o awk
, preferi pegar a descrição do Wikipedia que diz tudo:
A linguagem de programação AWK foi criada em 1977 pelos cientistas Alfred Aho, Peter J. Weinberger e Brian Kernighan no laboratório Bell Labs. A palavra AWK é uma abreviatura das iniciais dos sobrenomes dos criadores da linguagem (Aho, Weinberger e Kernighan).
A linguagem é interpretada linha por linha e tem como principal objetivo deixar os scripts de Shell em sistemas POSIX mais poderosos e com muito mais recursos sem utilizar muitas linhas de comando, podendo resolver infinidades de problemas do dia-a-dia do desenvolvedor nesses sistemas operacionais.
Baseada na linguagem C, é utilizada frequentemente por desenvolvedores para processar textos e manipular arquivos. Tem como os paradigmas linguagem de script, procedural e orientada a eventos.
Esta linguagem é considerada por muitos um importante marco para história da programação, tendo tido bastante influência na criação de outras linguagens de programação, como Perl e Lua.
Então, sabendo do tamanho do universo que é o awk
é claro que esse artigo não vai cobrir ele e vamos ser pontuais no que precisamos para resolver o nosso desafio de analisar os logs.
Vamos considerar o seguinte arquivo:
$ cat awk1.txt
colunaA colunaB colunaC
1 a !
2 b @
3 c #
4 d $
5 e %
6 f &
Retornando a entrada
$ awk '{print($0)}' awk1.txt
colunaA colunaB colunaC
1 a !
2 b @
3 c #
4 d $
5 e %
6 f &
Retornando os dados isolados por coluna
$ awk '{print($1)}' awk1.txt
colunaA
1
2
3
4
5
6
$ awk '{print($2)}' awk1.txt
colunaB
a
b
c
d
e
f
$ awk '{print($3)}' awk1.txt
colunaC
!
@
#
$
%
&
Usando o split
$ echo "0,1,2,3,4,5,6,7,8,9" | awk '{ split($0,l,","); print(l[0]) }'
$ echo "0,1,2,3,4,5,6,7,8,9" | awk '{ split($0,l,","); print(l[1]) }'
0
$ echo "0,1,2,3,4,5,6,7,8,9" | awk '{ split($0,l,","); print(l[1]) }'
0
$ echo "0,1,2,3,4,5,6,7,8,9" | awk '{ split($0,l,","); print(l[10]) }'
9
$ echo "0,1,2,3,4,5,6,7,8,9" | awk '{ split($0,l,","); print(l[11]) }'
Fique atento, o awk
não da erro ao acessar uma posição do array
que não possui dados.
Bônus
$ echo "0,1,2,3,4,5,6,7,8,9" | awk '{ split($0,l,","); c = l[3]; print c; if (c < 2) { print("a") } else if (c == 2) { print("b") } else print("c") }'
Qual é a resposta?
Gerador aleatório de logs
Para demonstrar o uso combinado das ferramentas era preciso gerar um data set de logs. Hoje, é bem comum boa parte das aplicações escrevem seus logs no formato de JSON e esse era o meu caso. Então, no nosso arquivo de log vai conter apenas um JSON, por exemplo:
{"remote_ip_addr": "82.58.153.167", "access_token": "secret_a7f49337ecff4c50a97c3d23e7386e23", "x_tid": "d8d3d5b0-bd8d-4386-b381-c760a70b9f49", "user_agent": "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)", "method": "GET", "path": "/users/1", "timestamp": "2020-04-30 03:58:21", "status_code": 201}
{"remote_ip_addr": "188.235.234.165", "access_token": "secret_df60122168e048ba846b81fed1b09a1d", "x_tid": "d60291e2-e5a2-4222-a038-67f4adc88012", "user_agent": "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)", "method": "DELETE", "path": "/robots.txt", "timestamp": "2020-04-30 03:58:45", "status_code": 200}
{"remote_ip_addr": "51.203.201.123", "access_token": "secret_b36d6af8a0d542f1ae5be86ac38688a0", "x_tid": "74016c10-be59-45ff-b13c-ffd8732919d8", "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36", "method": "DELETE", "path": "/users/12", "timestamp": "2020-04-30 03:58:48", "status_code": 400}
{"remote_ip_addr": "59.1.220.255", "access_token": "secret_2eb5ec132dac4fecaeb32ef12cfd7cd8", "x_tid": "64659fa4-f9d6-4e10-98d5-fb5178a2cd1c", "user_agent": "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)", "method": "PUT", "path": "/security.txt", "timestamp": "2020-04-30 03:58:57", "status_code": 500}
{"remote_ip_addr": "199.18.137.169", "access_token": "secret_289c094aa4784d60987873f914b326a6", "x_tid": "af1d4979-de7e-4f9d-95f5-9de310d570b7", "user_agent": "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)", "method": "PUT", "path": "/health", "timestamp": "2020-04-30 03:59:33", "status_code": 301}
{"remote_ip_addr": "24.109.83.239", "access_token": "secret_e34f7166be1f40c2be3754aafffe334b", "x_tid": "47cfaca0-0345-441a-bb03-670285f29854", "user_agent": "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)", "method": "POST", "path": "/robots.txt", "timestamp": "2020-04-30 04:00:01", "status_code": 500}
{"remote_ip_addr": "62.112.131.39", "access_token": "secret_b3351fdeb0cb40279c6d1f48febf5838", "x_tid": "60b8714d-4cdc-4948-932c-19f594b8e8eb", "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36", "method": "GET", "path": "/robots.txt", "timestamp": "2020-04-30 04:00:21", "status_code": 301}
{"remote_ip_addr": "50.243.130.83", "access_token": "secret_f9c8f3495f324707b2a49878ce253844", "x_tid": "7ab5d9d4-2629-423c-b9b7-309fcab1958b", "user_agent": "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)", "method": "POST", "path": "/", "timestamp": "2020-04-30 04:00:39", "status_code": 500}
{"remote_ip_addr": "17.180.155.70", "access_token": "secret_bae6bfd17dbc4ec9b7c9d6b2974f9ef6", "x_tid": "4a3beff7-bc2c-4535-bba0-9b9906f846bc", "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:53.0) Gecko/20100101 Firefox/53.0", "method": "GET", "path": "/users/35", "timestamp": "2020-04-30 04:00:54", "status_code": 400}
Entrando nos detalhes dos campos do log do nosso serviço acima:
remote_ip_addr
: uma string com endereço IP da origem da requisição;access_token
: o token usado para acessar a API;x_tid
: um ID para identificar a requisição;user_agent
: ferramenta de onde partiu a requisição;method
: método HTTP usado na requisição;path
: caminho no meu serviço onde chegou a requisiçãotimestamp
: instante no tempo que a requisição chegou; estatus_code
: código HTTP de retorno dado pelo serviço.
Código do gerador de log
import random
import socket
import struct
import json
import uuid
import datetime
from random import randrange
def generate_ip() -> str:
return socket.inet_ntoa(struct.pack('>I', random.randint(1, 0xffffffff)))
IP_DATASET_SIZE = 100
IP_DATASET = [generate_ip() for _ in range(IP_DATASET_SIZE)]
LOG_SIZE = 1000
USER_AGENTS = [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:53.0) Gecko/20100101 Firefox/53.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393",
"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)",
"Mozilla/5.0 (Windows; U; MSIE 7.0; Windows NT 6.0; en-US)",
"Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)",
"Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)",
"curl/7.35.0",
"Wget/1.15 (linux-gnu)"
]
STATUSES = [200, 201, 301, 400, 401, 500]
PATHS = [
"/",
"/health",
"/security.txt",
"/robots.txt",
"/users",
"/users/1",
"/users/12",
"/users/35",
"/users/100",
"/admin/users"
]
METHODS = [
"GET",
"POST",
"PUT",
"DELETE"
]
dt = datetime.datetime(2020, 4, 30, 0, 0)
for i in range(LOG_SIZE):
random_ip_pos = randrange(-1, IP_DATASET_SIZE, 1)
random_user_agent_pos = randrange(-1, len(USER_AGENTS), 1)
random_status_pos = randrange(-1, len(STATUSES), 1)
random_path_pos = randrange(-1, len(PATHS), 1)
random_method_pos = randrange(-1, len(METHODS), 1)
payload = {
"remote_ip_addr": IP_DATASET[random_ip_pos],
"access_token": "secret_" + uuid.uuid4().hex,
"x_tid": str(uuid.uuid4()),
"user_agent": USER_AGENTS[random_user_agent_pos],
"method": METHODS[random_method_pos],
"path": PATHS[random_path_pos],
"timestamp": dt.strftime("%Y-%m-%d %H:%M:%S"),
"status_code": STATUSES[random_status_pos],
}
dt += datetime.timedelta(seconds=randrange(randrange(1, 60, 1)))
print(json.dumps(payload))
Analisando os logs
Contando a quantidade de status_code
$ python generate.py | jq .status_code | sort | uniq --count
14257 200
14179 201
14411 301
14360 400
14069 401
28724 500
Rank do user-agents
$ python generate.py | jq .user_agent | sort | uniq --count | sort -k1 -n -r
20113 "Wget/1.15 (linux-gnu)"
10030 "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1)"
10008 "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"
9993 "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393"
9992 "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36"
9988 "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:53.0) Gecko/20100101 Firefox/53.0"
9988 "curl/7.35.0"
9963 "Mozilla/5.0 (Windows; U; MSIE 7.0; Windows NT 6.0; en-US)"
9925 "Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)"
Descobrindo se teve conflitos de X-TID
$ python generate.py | jq .x_tid | sort | uniq --count | awk '{ if ($1 > 1) print($0) }'
$ echo $?
0
Como não teve retorno e a execução foi um sucesso, significa que não teve conflitos de X-TID.
Rank das classes de IP
Primeiro, vamos entender a decomposição decimal do IP, 172.16.254.1, por exemplo.
172 = 10101100 (8 bits)
16 = 00010000 (8 bits)
254 = 11111110 (8 bits)
1 = 00000001 (8 bits)
total de 32 bitss (4 bytes)
Agora, quais são as classes de IP:
- Classe A: Primeiro bit é 0 (zero)
- começa: 0.0.0.0
- termina: 127.255.255.255
- Classe B: Primeiros dois bits são 10
- começa: 128.0.0.0
- termina: 191.255.255.255
- Classe C: Primeiros três bits são 110
- começa: 192.0.0.0
- termina: 223.255.255.255
- Classe D: Primeiros quatro bits são: 1110
- começa: 224.0.0.0
- termina: 239.255.255.255
- Classe E: Primeiros quatro bits são 1111
- começa: 240.0.0.0
- termina: 255.255.255.254
Detalhes sobre as classes de IP aqui.
$ python generate.py | jq '.remote_ip_addr|split(".")[0]|tonumber' | awk '{ if ($1 <= 127) { print "A"; } else if ($1 <= 191) { print "B"; } else if ($1 <= 223) { print "C"; } else if ($1 <= 239) { print "D"; } else { print "E"; } }' | sort | uniq -c | sort -k2
49540 A
23552 B
10068 C
8991 D
7849 E
Conclusão
O terminal e suas ferramentas são extremamante poderosos mas é preciso conhecer e praticar para ter produtividade além de muito Google e Stack Overflow. Quais ferramentas que você usa e como?
Comments ()