Confesso que pra escrever o script pra lers os logs do servidor web, aquele que mostrei em acessos de robôs nos logs web, foi algo próximo do vudu. Tudo porque o formato gerado não é lá muito amigável.
Idem pras máquinas do trabalho. Então hoje eu resolvi dar uma olhada se tinha como escrever esses mesmos logs em json.
E tem.
O primeiro que olhei foi no nginx. E é bem fácil de fazer.
log_format logger-json escape=json '{"source": "nginx", "time": "$time_iso8601", "resp_body_size": $body_bytes_sent, "host": "$http_host", "address": "$remote_addr", "request_length": $request_length, "method": "$request_method", "uri": "$request_uri", "status": $status, "user_agent": "$http_user_agent", "resp_time": $request_time, "upstream_addr": "$upstream_addr"}';
[...]
server {
listen 443 ssl;
server_name api.company.com;
...
access_log /var/log/nginx/access.log logger-json;
...
}
Eu segui a receita desses dois links aqui:
https://www.velebit.ai/blog/nginx-json-logging/
https://nginx.org/en/docs/http/ngx_http_log_module.html#log_format
Pro Apache não tem um módulo que já gera tudo meio mastigado como no nginx. Mas você pode criar o formato do log como quiser.
LogFormat "{ \"time\":\"%{%Y-%m-%d}tT%{%T}t.%{msec_frac}tZ\", \"process\":\"%D\", \"filename\":\"%f\", \"remoteIP\":\"%a\", \"host\":\"%V\", \"request\":\"%U\", \"query\":\"%q\", \"method\":\"%m\", \"status\":\"%>s\", \"userAgent\":\"%{User-agent}i\", \"referer\":\"%{Referer}i\" }" combined
ErrorLogFormat "{ \"time\":\"%{%Y-%m-%d}tT%{%T}t.%{msec_frac}tZ\", \"function\" : \"[%-m:%l]\" , \"process\" : \"[pid %P:tid %T]\" , \"message\" : \"%M\" ,\ \"referer\"\ : \"%{Referer}i\" }"
E esse veio de dica do stack-overflow:
Eu não segui os mesmos parâmetros entre apache e nginx. Por enquanto estou só testando e estar com os campos bem definidos já é o suficiente pra mim.
> tail -1 /var/log/apache2/linux-br-access.log | jq .
{
"time": "2025-08-20T09:38:59.369Z",
"process": "241797",
"filename": "/var/www/linux-br.org/index.php",
"remoteIP": "54.36.149.72",
"host": "linux-br.org",
"request": "/date/2024/03/page/3/",
"query": "",
"method": "GET",
"status": "200",
"userAgent": "Mozilla/5.0 (compatible; AhrefsBot/7.0; +http://ahrefs.com/robot/)",
"referer": "-"
}
Agora ficou fácil de filtrar e pegar só os campos que interessam. E usando "jq".
> tail -1 /var/log/apache2/linux-br-access.log | jq ".userAgent"
"Mozilla/5.0 (compatible; AhrefsBot/7.0; +http://ahrefs.com/robot/)"
> tail -3 /var/log/apache2/linux-br-access.log | jq ".userAgent"
"Mozilla/5.0 (compatible; AhrefsBot/7.0; +http://ahrefs.com/robot/)"
"Mozilla/5.0 (compatible; SemrushBot/7~bl; +http://www.semrush.com/bot.html)"
"Mozilla/5.0 (compatible; AhrefsBot/7.0; +http://ahrefs.com/robot/)"
Abri um grupo novo, SRE - Site Reliability Engineer, e uma categoria nova sobre Ansible já que agora esse é meu mundo. Estou trabalhando nessa área mantendo servidores rodando na empresa nova.
Eu estou migrando um sistema que cria VMs de python, pela falta de suporte na biblioteca, pra outra forma via linha de comando. Mas essa parte não interessa muito. O interessante é sobre como fiz parte do trabalho.
E estou escrevendo a respeito porque foi difícil achar informação sobre isso na Internet. A maioria estava ou errada ou não funcionava como esperado.
Pra começar, vamos dar uma olhada na estrutura de dados que recebo do programa. Pra simular o mesmo no Ansible, vou trocar o comando por um: cat server.json (onde server.json contém esse dado). Meu objetivo aqui é pegar o IPv4 da interface pública pra depois provisionar o sistema com os programas que vamos usar.
{
"core_number": "1",
"hostname": "helio-testing.loureiro.eng.br",
"license": 0,
"memory_amount": "1024",
"plan": "1xCPU-1GB",
"progress": "0",
"state": "started",
"tags": [],
"title": "helio-testing.loureiro.eng.br (created via ansible)",
"uuid": "87271929-a137-4910-9648-75d05b6d3ecb",
"zone": "sweden-stockholm",
"boot_order": "disk",
"firewall": "off",
"host": 12345678,
"ip_addresses": [
{
"access": "internal",
"address": "192.168.0.1",
"family": "IPv4",
"part_of_plan": "no",
"ptr_record": "",
"server": "",
"mac": "",
"floating": "no",
"zone": ""
},
{
"access": "public",
"address": "1.2.3.4",
"family": "IPv4",
"part_of_plan": "yes",
"ptr_record": "",
"server": "",
"mac": "",
"floating": "no",
"zone": ""
}
],
"labels": {
"label": []
},
"metadata": "yes",
"nic_model": "virtio",
"networking": {
"interfaces": [
{
"index": 1,
"ip_addresses": [
{
"access": "",
"address": "1.2.3.4",
"family": "IPv4",
"part_of_plan": "no",
"ptr_record": "",
"server": "",
"mac": "",
"floating": "no",
"zone": ""
}
],
"mac": "a1:b2:c3:e4:f5:01",
"network": "fb29cb51-5dd7-4674-baac-b0253a725305",
"type": "public",
"bootable": "no",
"source_ip_filtering": "yes"
},
{
"index": 2,
"ip_addresses": [
{
"access": "",
"address": "192.168.0.1",
"family": "IPv4",
"part_of_plan": "no",
"ptr_record": "",
"server": "",
"mac": "",
"floating": "no",
"zone": ""
}
],
"mac": "a1:b2:c3:e4:f5:02",
"network": "92351420-66ec-46e9-98ee-149e3a588bdf",
"type": "utility",
"bootable": "no",
"source_ip_filtering": "yes"
}
]
},
"server_group": "",
"simple_backup": "no",
"storage_devices": [
{
"address": "virtio:0",
"storage": "fe128b6b-876c-4f7b-a070-3a8d6cec6159",
"storage_size": 10,
"storage_tier": "maxiops",
"storage_title": "helio-testing.loureiro.eng.br-OS",
"type": "disk",
"boot_disk": "0"
}
],
"timezone": "UTC",
"video_model": "vga",
"remote_access_enabled": "no",
"remote_access_type": "vnc",
"remote_access_host": "",
"remote_access_password": "abracadabra",
"remote_access_port": "0"
}
Então vamos começar com como pegar a saída desse comando no ansible. Pra isso basta chamar uma parte simples que registra o resultado.
---
- debug: msg="running the command and getting json as result"
- name: read json file
command: >
cat /tmp/server.json
register: result
failed_when: result.rc != 0
- debug:
msg="{{ result }}"
Não tem muito segredo aqui. Eu rodo o comando, que aqui eu troquei pelo cat e recebo o resultando em result. Eu olho se result.rc é diferente de zero pra saber se deu certo (não zero significa erro).
- name: parsing as json
set_fact:
json_result: "{{ result.stdout }}"
- debug:
msg=" from json_result "
msg="{{ json_result}}"
Aqui foi o primeiro pulo do gato. Pra receber o dado como json bastava só pegar de result.stdout. Em muitos lugares achei receitas exotéricas como {{ result | to_json }}. Todas erradas.
- name: getting IPs
set_fact:
json_ip_addresses: "{{ json_result.ip_addresses }}"
- debug: msg="{{ json_ip_addresses }}"
Do código json que recebo, eu filtro somente ip_addresses, que é a parte que me interessa.
- name: filtering public IPs
set_fact:
public_ip_addresses: "{{ json_ip_addresses | json_query(query) }}"
vars:
query: "[?access=='public']"
- debug: msg="{{ public_ip_addresses }}"
Em seguida eu aplico filtro de json usando a sintaxe de jpath. Na parte da query, que tem um array, eu quero somente aquela que tem a propriedade "access" com o valor "public". A query vai dentro de vars por causa das aspas simples e duplas.
- name: filtering IPv4 only
set_fact:
public_ipv4_address: "{{ public_ip_addresses | json_query(query) }}"
vars:
query: "[?family=='IPv4'].address"
- debug: msg="{{ public_ipv4_address }}"
No exemplo que tenho eu já não tenho mais nada, mas em alguns casos eu tenho um array com 2 elementos: IPv4 e IPv6. Então essa segunda query é pra filtrar qual estrutura tem a propriedade "family" com valor "IPv4". Dessa estrutura eu quero o valor de "address". E pronto! Está aí o IPv4 filtrado. O valor é 1.2.3.4, que é o esperado.
Agora rodando com Ansbile:
❯ ansible-playbook deploy.yml
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'
PLAY [get JSON] ***************************************************************************************************************************************************************************
TASK [getjson : debug] ********************************************************************************************************************************************************************
ok: [localhost] => {
"msg": "running from roles/getjson/tasks"
}
TASK [getjson : read json file] ***********************************************************************************************************************************************************
changed: [localhost]
TASK [getjson : debug] ********************************************************************************************************************************************************************
ok: [localhost] => {
"msg": {
"changed": true,
"cmd": [
"cat",
"../server.json"
],
"delta": "0:00:00.002636",
"end": "2024-11-23 16:02:30.099466",
"failed": false,
"failed_when_result": false,
"msg": "",
"rc": 0,
"start": "2024-11-23 16:02:30.096830",
"stderr": "",
"stderr_lines": [],
"stdout": "{\n \"core_number\": \"1\",\n \"hostname\": \"helio-testing.loureiro.eng.br\",\n \"license\": 0,\n \"memory_amount\": \"1024\",\n \"plan\": \"1xCPU-1GB\",\n \"progress\": \"0\",\n \"state\": \"started\",\n \"tags\": [],\n \"title\": \"helio-testing.loureiro.eng.br (created via ansible)\",\n \"uuid\": \"87271929-a137-4910-9648-75d05b6d3ecb\",\n \"zone\": \"sweden-stockholm\",\n \"boot_order\": \"disk\",\n \"firewall\": \"off\",\n \"host\": 12345678,\n \"ip_addresses\": [\n {\n \"access\": \"internal\",\n \"address\": \"192.168.0.1\",\n \"family\": \"IPv4\",\n \"part_of_plan\": \"no\",\n \"ptr_record\": \"\",\n \"server\": \"\",\n \"mac\": \"\",\n \"floating\": \"no\",\n \"zone\": \"\"\n },\n {\n \"access\": \"public\",\n \"address\": \"1.2.3.4\",\n \"family\": \"IPv4\",\n \"part_of_plan\": \"yes\",\n \"ptr_record\": \"\",\n \"server\": \"\",\n \"mac\": \"\",\n \"floating\": \"no\",\n \"zone\": \"\"\n }\n ],\n \"labels\": {\n \"label\": []\n },\n \"metadata\": \"yes\",\n \"nic_model\": \"virtio\",\n \"networking\": {\n \"interfaces\": [\n {\n \"index\": 1,\n \"ip_addresses\": [\n {\n \"access\": \"\",\n \"address\": \"1.2.3.4\",\n \"family\": \"IPv4\",\n \"part_of_plan\": \"no\",\n \"ptr_record\": \"\",\n \"server\": \"\",\n \"mac\": \"\",\n \"floating\": \"no\",\n \"zone\": \"\"\n }\n ],\n \"mac\": \"a1:b2:c3:e4:f5:01\",\n \"network\": \"fb29cb51-5dd7-4674-baac-b0253a725305\",\n \"type\": \"public\",\n \"bootable\": \"no\",\n \"source_ip_filtering\": \"yes\"\n },\n {\n \"index\": 2,\n \"ip_addresses\": [\n {\n \"access\": \"\",\n \"address\": \"192.168.0.1\",\n \"family\": \"IPv4\",\n \"part_of_plan\": \"no\",\n \"ptr_record\": \"\",\n \"server\": \"\",\n \"mac\": \"\",\n \"floating\": \"no\",\n \"zone\": \"\"\n }\n ],\n \"mac\": \"a1:b2:c3:e4:f5:02\",\n \"network\": \"92351420-66ec-46e9-98ee-149e3a588bdf\",\n \"type\": \"utility\",\n \"bootable\": \"no\",\n \"source_ip_filtering\": \"yes\"\n }\n ]\n },\n \"server_group\": \"\",\n \"simple_backup\": \"no\",\n \"storage_devices\": [\n {\n \"address\": \"virtio:0\",\n \"storage_encrypted\": \"yes\",\n \"part_of_plan\": \"\",\n \"storage\": \"fe128b6b-876c-4f7b-a070-3a8d6cec6159\",\n \"storage_size\": 50,\n \"storage_tier\": \"maxiops\",\n \"storage_title\": \"helio-testing.loureiro.eng.br-OS\",\n \"type\": \"disk\",\n \"boot_disk\": \"0\"\n }\n ],\n \"timezone\": \"UTC\",\n \"video_model\": \"vga\",\n \"remote_access_enabled\": \"no\",\n \"remote_access_type\": \"vnc\",\n \"remote_access_host\": \"\",\n \"remote_access_password\": \"abracadabra\",\n \"remote_access_port\": \"0\"\n}",
"stdout_lines": [
"{",
" \"core_number\": \"1\",",
" \"hostname\": \"helio-testing.loureiro.eng.br\",",
" \"license\": 0,",
" \"memory_amount\": \"1024\",",
" \"plan\": \"1xCPU-1GB\",",
" \"progress\": \"0\",",
" \"state\": \"started\",",
" \"tags\": [],",
" \"title\": \"helio-testing.loureiro.eng.br (created via ansible)\",",
" \"uuid\": \"87271929-a137-4910-9648-75d05b6d3ecb\",",
" \"zone\": \"sweden-stockholm\",",
" \"boot_order\": \"disk\",",
" \"firewall\": \"off\",",
" \"host\": 12345678,",
" \"ip_addresses\": [",
" {",
" \"access\": \"internal\",",
" \"address\": \"192.168.0.1\",",
" \"family\": \"IPv4\",",
" \"part_of_plan\": \"no\",",
" \"ptr_record\": \"\",",
" \"server\": \"\",",
" \"mac\": \"\",",
" \"floating\": \"no\",",
" \"zone\": \"\"",
" },",
" {",
" \"access\": \"public\",",
" \"address\": \"1.2.3.4\",",
" \"family\": \"IPv4\",",
" \"part_of_plan\": \"yes\",",
" \"ptr_record\": \"\",",
" \"server\": \"\",",
" \"mac\": \"\",",
" \"floating\": \"no\",",
" \"zone\": \"\"",
" }",
" ],",
" \"labels\": {",
" \"label\": []",
" },",
" \"metadata\": \"yes\",",
" \"nic_model\": \"virtio\",",
" \"networking\": {",
" \"interfaces\": [",
" {",
" \"index\": 1,",
" \"ip_addresses\": [",
" {",
" \"access\": \"\",",
" \"address\": \"1.2.3.4\",",
" \"family\": \"IPv4\",",
" \"part_of_plan\": \"no\",",
" \"ptr_record\": \"\",",
" \"server\": \"\",",
" \"mac\": \"\",",
" \"floating\": \"no\",",
" \"zone\": \"\"",
" }",
" ],",
" \"mac\": \"a1:b2:c3:e4:f5:01\",",
" \"network\": \"fb29cb51-5dd7-4674-baac-b0253a725305\",",
" \"type\": \"public\",",
" \"bootable\": \"no\",",
" \"source_ip_filtering\": \"yes\"",
" },",
" {",
" \"index\": 2,",
" \"ip_addresses\": [",
" {",
" \"access\": \"\",",
" \"address\": \"192.168.0.1\",",
" \"family\": \"IPv4\",",
" \"part_of_plan\": \"no\",",
" \"ptr_record\": \"\",",
" \"server\": \"\",",
" \"mac\": \"\",",
" \"floating\": \"no\",",
" \"zone\": \"\"",
" }",
" ],",
" \"mac\": \"a1:b2:c3:e4:f5:02\",",
" \"network\": \"92351420-66ec-46e9-98ee-149e3a588bdf\",",
" \"type\": \"utility\",",
" \"bootable\": \"no\",",
" \"source_ip_filtering\": \"yes\"",
" }",
" ]",
" },",
" \"server_group\": \"\",",
" \"simple_backup\": \"no\",",
" \"storage_devices\": [",
" {",
" \"address\": \"virtio:0\",",
" \"storage_encrypted\": \"yes\",",
" \"part_of_plan\": \"\",",
" \"storage\": \"fe128b6b-876c-4f7b-a070-3a8d6cec6159\",",
" \"storage_size\": 50,",
" \"storage_tier\": \"maxiops\",",
" \"storage_title\": \"helio-testing.loureiro.eng.br-OS\",",
" \"type\": \"disk\",",
" \"boot_disk\": \"0\"",
" }",
" ],",
" \"timezone\": \"UTC\",",
" \"video_model\": \"vga\",",
" \"remote_access_enabled\": \"no\",",
" \"remote_access_type\": \"vnc\",",
" \"remote_access_host\": \"\",",
" \"remote_access_password\": \"abracadabra\",",
" \"remote_access_port\": \"0\"",
"}"
]
}
}
TASK [getjson : parsing as json] **********************************************************************************************************************************************************
ok: [localhost]
TASK [getjson : debug] ********************************************************************************************************************************************************************
ok: [localhost] => {
"msg": {
"boot_order": "disk",
"core_number": "1",
"firewall": "off",
"host": 12345678,
"hostname": "helio-testing.loureiro.eng.br",
"ip_addresses": [
{
"access": "internal",
"address": "192.168.0.1",
"family": "IPv4",
"floating": "no",
"mac": "",
"part_of_plan": "no",
"ptr_record": "",
"server": "",
"zone": ""
},
{
"access": "public",
"address": "1.2.3.4",
"family": "IPv4",
"floating": "no",
"mac": "",
"part_of_plan": "yes",
"ptr_record": "",
"server": "",
"zone": ""
}
],
"labels": {
"label": []
},
"license": 0,
"memory_amount": "1024",
"metadata": "yes",
"networking": {
"interfaces": [
{
"bootable": "no",
"index": 1,
"ip_addresses": [
{
"access": "",
"address": "1.2.3.4",
"family": "IPv4",
"floating": "no",
"mac": "",
"part_of_plan": "no",
"ptr_record": "",
"server": "",
"zone": ""
}
],
"mac": "a1:b2:c3:e4:f5:01",
"network": "fb29cb51-5dd7-4674-baac-b0253a725305",
"source_ip_filtering": "yes",
"type": "public"
},
{
"bootable": "no",
"index": 2,
"ip_addresses": [
{
"access": "",
"address": "192.168.0.1",
"family": "IPv4",
"floating": "no",
"mac": "",
"part_of_plan": "no",
"ptr_record": "",
"server": "",
"zone": ""
}
],
"mac": "a1:b2:c3:e4:f5:02",
"network": "92351420-66ec-46e9-98ee-149e3a588bdf",
"source_ip_filtering": "yes",
"type": "utility"
}
]
},
"nic_model": "virtio",
"plan": "1xCPU-1GB",
"progress": "0",
"remote_access_enabled": "no",
"remote_access_host": "",
"remote_access_password": "abracadabra",
"remote_access_port": "0",
"remote_access_type": "vnc",
"server_group": "",
"simple_backup": "no",
"state": "started",
"storage_devices": [
{
"address": "virtio:0",
"boot_disk": "0",
"part_of_plan": "",
"storage": "fe128b6b-876c-4f7b-a070-3a8d6cec6159",
"storage_encrypted": "yes",
"storage_size": 50,
"storage_tier": "maxiops",
"storage_title": "helio-testing.loureiro.eng.br-OS",
"type": "disk"
}
],
"tags": [],
"timezone": "UTC",
"title": "helio-testing.loureiro.eng.br (created via ansible)",
"uuid": "87271929-a137-4910-9648-75d05b6d3ecb",
"video_model": "vga",
"zone": "sweden-stockholm"
}
}
TASK [getjson : getting IPs] **************************************************************************************************************************************************************
ok: [localhost]
TASK [getjson : debug] ********************************************************************************************************************************************************************
ok: [localhost] => {
"msg": [
{
"access": "internal",
"address": "192.168.0.1",
"family": "IPv4",
"floating": "no",
"mac": "",
"part_of_plan": "no",
"ptr_record": "",
"server": "",
"zone": ""
},
{
"access": "public",
"address": "1.2.3.4",
"family": "IPv4",
"floating": "no",
"mac": "",
"part_of_plan": "yes",
"ptr_record": "",
"server": "",
"zone": ""
}
]
}
TASK [getjson : filtering public IPs] *****************************************************************************************************************************************************
ok: [localhost]
TASK [getjson : debug] ********************************************************************************************************************************************************************
ok: [localhost] => {
"msg": [
{
"access": "public",
"address": "1.2.3.4",
"family": "IPv4",
"floating": "no",
"mac": "",
"part_of_plan": "yes",
"ptr_record": "",
"server": "",
"zone": ""
}
]
}
TASK [getjson : filtering IPv4 only] ******************************************************************************************************************************************************
ok: [localhost]
TASK [getjson : debug] ********************************************************************************************************************************************************************
ok: [localhost] => {
"msg": [
"1.2.3.4"
]
}
TASK [getjson : cloud server IP] ********************************************************************************************************************************************************
ok: [localhost]
TASK [getjson : debug] ********************************************************************************************************************************************************************
ok: [localhost] => {
"msg": "1.2.3.4"
}
PLAY RECAP *********************************************************************************************************************************************************************************
localhost : ok=15 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Bom ansible and happy coding 😄
Eu descrevi o uso do sensor temper com raspberrypi no artigo
monitorando a temperatura da sala dos servidores.
O que eu não contei ali foi que eu passei a monitorar outros parâmetros mostrado pelo programa sensor.
O sensor faz parte do pacote lm-sensor no Ubuntu.
Rodando o programa com parâmetro -j mostra a saída em format JSON.
❯ sensors -j
{
"coretemp-isa-0000":{
"Adapter": "ISA adapter",
"Package id 0":{
"temp1_input": 61.000,
"temp1_max": 100.000,
"temp1_crit": 100.000,
"temp1_crit_alarm": 0.000
},
"Core 0":{
"temp2_input": 52.000,
"temp2_max": 100.000,
"temp2_crit": 100.000,
"temp2_crit_alarm": 0.000
},
"Core 4":{
"temp6_input": 54.000,
"temp6_max": 100.000,
"temp6_crit": 100.000,
"temp6_crit_alarm": 0.000
},
"Core 8":{
"temp10_input": 61.000,
"temp10_max": 100.000,
"temp10_crit": 100.000,
"temp10_crit_alarm": 0.000
},
"Core 9":{
"temp11_input": 61.000,
"temp11_max": 100.000,
"temp11_crit": 100.000,
"temp11_crit_alarm": 0.000
},
"Core 10":{
"temp12_input": 61.000,
"temp12_max": 100.000,
"temp12_crit": 100.000,
"temp12_crit_alarm": 0.000
},
"Core 11":{
"temp13_input": 61.000,
"temp13_max": 100.000,
"temp13_crit": 100.000,
"temp13_crit_alarm": 0.000
},
"Core 12":{
"temp14_input": 59.000,
"temp14_max": 100.000,
"temp14_crit": 100.000,
"temp14_crit_alarm": 0.000
},
"Core 13":{
"temp15_input": 59.000,
"temp15_max": 100.000,
"temp15_crit": 100.000,
"temp15_crit_alarm": 0.000
},
"Core 14":{
"temp16_input": 59.000,
"temp16_max": 100.000,
"temp16_crit": 100.000,
"temp16_crit_alarm": 0.000
},
"Core 15":{
"temp17_input": 60.000,
"temp17_max": 100.000,
"temp17_crit": 100.000,
"temp17_crit_alarm": 0.000
}
},
"thinkpad-isa-0000":{
"Adapter": "ISA adapter",
"fan1":{
"fan1_input": 2191.000
},
"CPU":{
"temp1_input": 59.000
},
"GPU":{
ERROR: Can't get value of subfeature temp2_input: Can't read
},
"temp3":{
"temp3_input": 59.000
},
"temp4":{
"temp4_input": 0.000
},
"temp5":{
"temp5_input": 59.000
},
"temp6":{
"temp6_input": 59.000
},
"temp7":{
"temp7_input": 59.000
},
"temp8":{
ERROR: Can't get value of subfeature temp8_input: Can't read
}
},
"ucsi_source_psy_USBC000:001-isa-0000":{
"Adapter": "ISA adapter",
"in0":{
"in0_input": 0.000,
"in0_min": 0.000,
"in0_max": 0.000
},
"curr1":{
"curr1_input": 0.000,
"curr1_max": 0.000
}
},
"BAT0-acpi-0":{
"Adapter": "ACPI interface",
"in0":{
"in0_input": 12.909
},
"power1":{
"power1_input": 0.000
}
},
"iwlwifi_1-virtual-0":{
"Adapter": "Virtual device",
"temp1":{
"temp1_input": 42.000
}
},
"ucsi_source_psy_USBC000:002-isa-0000":{
"Adapter": "ISA adapter",
"in0":{
"in0_input": 0.000,
"in0_min": 0.000,
"in0_max": 0.000
},
"curr1":{
"curr1_input": 3.000,
"curr1_max": 0.000
}
},
"nvme-pci-0200":{
"Adapter": "PCI adapter",
"Composite":{
"temp1_input": 44.850,
"temp1_max": 85.850,
"temp1_min": -273.150,
"temp1_crit": 86.850,
"temp1_alarm": 0.000
},
"Sensor 1":{
"temp2_input": 47.850,
"temp2_max": 65261.850,
"temp2_min": -273.150
},
"Sensor 2":{
"temp3_input": 44.850,
"temp3_max": 65261.850,
"temp3_min": -273.150
}
},
"acpitz-acpi-0":{
"Adapter": "ACPI interface",
"temp1":{
"temp1_input": 59.000
}
}
}
Essa saída de comando é do laptop de trabalho, onde estou escrevendo esse artigo.
É possível ver que aparecem alguns erros como ERROR: Can't get value of subfeature temp2_input: Can't read
e que alguns dados não tem valor como em { "coretemp-isa-0000":{ "Adapter": "ISA adapter" } }.
Quando fiz pro servidor, eu acabei meio que escrevendo o código na mão.
sensors = shellExec("/usr/bin/sensors -j")
logger.debug(f"sensors: {sensors}")
jResp = json.loads(sensors)
resp = list()
resp.append("#HELP server_board_temperature_celsius the server board current temperatures")
resp.append("#TYPE server_board_temperature_celsius gauge")
device_1 = "nvme-pci-2b00"
device_2 = "k10temp-pci-00c3"
composite = jResp[device_1]["Composite"]["temp1_input"]
logger.info(f"device: {device_1}, composite, temperature: {composite}")
resp.append("server_board_temperature_celsius{device=\"" + device_1 + "\",sensor=\"composite\"} " + "%0.2f" % composite )
sensor_1 = jResp[device_1]["Sensor 1"]["temp2_input"]
resp.append("server_board_temperature_celsius{device=\"" + device_1 + "\",sensor=\"sensor_1\"} " + "%0.2f" % sensor_1 )
sensor_2 = jResp[device_1]["Sensor 2"]["temp3_input"]
resp.append("server_board_temperature_celsius{device=\"" + device_1 + "\",sensor=\"sensor_2\"} " + "%0.2f" % sensor_2 )
tctl = jResp[device_2]["Tctl"]["temp1_input"]
resp.append("server_board_temperature_celsius{device=\"" + device_2 + "\",sensor=\"tctl\"} " + "%0.2f" % tctl )
tccd3 = jResp[device_2]["Tccd3"]["temp5_input"]
resp.append("server_board_temperature_celsius{device=\"" + device_2 + "\",sensor=\"tccd3\"} " + "%0.2f" % tccd3 )
tccd5 = jResp[device_2]["Tccd5"]["temp7_input"]
resp.append("server_board_temperature_celsius{device=\"" + device_2 + "\",sensor=\"tccd5\"} " + "%0.2f" % tccd5 )
resp.append("")
Então essa semana eu gastei um tempo pra fazer um script mais genérico.
Aparecem agora todos os dados que saem com valor no comando sensor -j, mas em contrapartida não sei exatamente o que são.
Sem mais delongas, aqui o código:
#! /usr/bin/env -S uv run --script
#
# /// script
# dependencies = [
# "uvicorn",
# "fastapi"
# ]
# ///
import argparse
import sys
import subprocess
import logging
import json
import re
from fastapi import FastAPI
from fastapi.responses import PlainTextResponse
import uvicorn
DEFAULTS = {"port": 9090, "path": "/metrics"}
__version__ = "0.1.0"
logger = logging.getLogger(__file__)
consoleOutputHandler = logging.StreamHandler()
formatter = logging.Formatter(
fmt="[%(asctime)s] (%(levelname)s) %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
)
consoleOutputHandler.setFormatter(formatter)
logger.addHandler(consoleOutputHandler)
logger.setLevel(logging.INFO)
def shellExec(command: str) -> str:
"run a command and return its output"
try:
# result = subprocess.getoutput(command, errors=subprocess.DEVNULL)
result = subprocess.check_output(
command, stderr=subprocess.DEVNULL, encoding="utf-8"
)
logger.debug(f"shellExec: {result}")
return result
except Exception as e:
logger.error(f"Error executing shell command '{command}': {e}")
return f"Error: {e}"
class SensorMetrics:
open_metrics: dict = {}
def generateMetrics(self) -> list:
self.dataReset()
sensors_output = shellExec(["sensors", "-j"])
logger.debug(f"sensors: {sensors_output}")
sensorsJSON = json.loads(sensors_output)
# sensorsJSON = {
# "thinkpad-isa-0000": {
# "Adapter": "ISA adapter",
# "fan1": {"fan1_input": 2786.000},
# "CPU": {"temp1_input": 69.000},
# "GPU": {},
# "temp3": {"temp3_input": 69.000},
# "temp4": {"temp4_input": 0.000},
# "temp5": {"temp5_input": 69.000},
# "temp6": {"temp6_input": 69.000},
# "temp7": {"temp7_input": 69.000},
# "temp8": {},
# }
# }
self.getOpenMetrics([], sensorsJSON)
return []
def dataReset(self):
self.open_metrics = {}
def getOpenMetrics(self, header: list, data_dict: dict):
if isinstance(data_dict, dict):
for k, v in data_dict.items():
logger.debug(f"k={k}, v={v}")
if isinstance(v, dict):
self.getOpenMetrics(header + [k], v)
else:
logger.debug(
f"Value is not dictionary: header={header}, k={k}, v={v}"
)
try:
v = float(v)
except ValueError:
logger.debug(f"invalid value: {v}")
continue
metric_head = self.generateMetricHeader(header, k)
metric_description = " ".join(header)
metric_description += f" {k}"
if metric_head in self.open_metrics:
logger.error(f"metric name '{metric_head}' already exists")
self.open_metrics[metric_head] = {
"value": v,
"description": metric_description,
}
logger.debug(
f"Adding: metric_head={metric_head}, value={v}, description='{metric_description}'"
)
else:
logger.debug(f"Not dictionary: header={header}, data_dict={data_dict}")
def generateMetricHeader(self, header: list, key: str) -> str:
metric_header = "_".join(header)
metric_header = re.sub(" ", "_", metric_header)
return f"{metric_header}_{key}"
if __name__ == "__main__":
parse = argparse.ArgumentParser(
description="Script to expose the sensors as open metrics"
)
parse.add_argument(
"--port", type=int, default=DEFAULTS["port"], help="Port to listen"
)
parse.add_argument(
"--path", default=DEFAULTS["path"], help="The path to serve the metrics"
)
parse.add_argument(
"--version",
action=argparse.BooleanOptionalAction,
help="Print version and exit",
)
parse.add_argument(
"--printout",
action=argparse.BooleanOptionalAction,
help="Print the exposed metrics found in the system",
)
parse.add_argument("--loglevel", default="info", help="Logging level")
args = parse.parse_args()
if args.loglevel != "info":
logger.setLevel(args.loglevel.upper())
if args.version is True:
print(sys.argv[0], __version__)
sys.exit(0)
if args.printout is True:
mts = SensorMetrics()
mts.generateMetrics()
for k, v in mts.open_metrics.items():
description = v["description"]
print(f"#HELP {k} {description}")
print(f"#TYPE {k} gauge")
print(f"{k}: {v['value']}")
sys.exit(0)
app = FastAPI()
@app.get(args.path, response_class=PlainTextResponse)
async def metrics():
logger.info(f"serving web page on {args.path}")
mts = SensorMetrics()
mts.generateMetrics()
resp = list()
for k, v in mts.open_metrics.items():
description = v["description"]
resp.append(f"#HELP {k} {description}")
resp.append(f"#TYPE {k} gauge")
resp.append(f"{k}: {v['value']}")
return "\n".join(resp)
logger.info(f"starting service on port {args.port}")
uvicorn.run(app, host="127.0.0.1", port=args.port)
O código pode ser encontrado aqui:
Como pode ser visto no cabeçalho:
#! /usr/bin/env -S uv run --script
#
# /// script
# dependencies = [
# "uvicorn",
# "fastapi"
# ]
# ///
abracei com força a dica do querido Riverfount, que mostrou que o uv podia fazer isso.
E passei a adotar o pacote logging ao invés de enviar prints pro terminal.
logger = logging.getLogger(__file__)
consoleOutputHandler = logging.StreamHandler()
formatter = logging.Formatter(
fmt="[%(asctime)s] (%(levelname)s) %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
)
consoleOutputHandler.setFormatter(formatter)
logger.addHandler(consoleOutputHandler)
logger.setLevel(logging.INFO)
Eu sempre amarro esse singleton de logging, logger, com função do argparse pra poder modificar
o nível.
Começo sempre com info, mas usando o parâmetro --loglevel=debug eu mudo pro nível debug.
Torna prático e flexível pra gerenciar logs de funcionamento.
Além disso o logging usa STDERR pra enviar as mensagens.
Olhando agora pra parte de execução do script shell, onde sensors -j é chamado:
def shellExec(command: str) -> str:
"run a command and return its output"
try:
# result = subprocess.getoutput(command, errors=subprocess.DEVNULL)
result = subprocess.check_output(
command, stderr=subprocess.DEVNULL, encoding="utf-8"
)
logger.debug(f"shellExec: {result}")
return result
except Exception as e:
logger.error(f"Error executing shell command '{command}': {e}")
return f"Error: {e}"
Uso como função ao invés de método da classe. Acho que fica mais simples. E poder rodar script do shell dentro de uma classe não parece lá muito como uma propriedade da classe em si.
Notem que inicialmente eu tentei usar subprocess.getoutput( ).
O problema são aquelas mensagens de erros que eu comentei acima,
ERROR: Can't get value of subfeature temp2_input: Can't read,
que saem junto.
Isso quebra a chamada do json.loads( ).
Tentei usar o parâmetro errors=subprocess.DEVNULL, mas aparentemente o subprocess.getoutput( ) não herda isso.
Então não funciona.
O jeito foi usar subprocess.check_output( ).
A grande diferença é que o primeiro aceita string, então poderia usar simplemente "sensors -j" como argumento.
No subprocess.check_output( ), o parâmetro precisa ser uma lista.
Tem de ser passado como ["sensors", "-j"] que não é o fim do mundo, só um pouco mais chato.
A classe SensorMetrics está bem simples de entender no começo.
Acho que a única parte que precisa de um pouco mais de cuidado pra olhar é o método getOpenMetrics( ).
def getOpenMetrics(self, header: list, data_dict: dict):
if isinstance(data_dict, dict):
for k, v in data_dict.items():
logger.debug(f"k={k}, v={v}")
if isinstance(v, dict):
self.getOpenMetrics(header + [k], v)
else:
logger.debug(
f"Value is not dictionary: header={header}, k={k}, v={v}"
)
try:
v = float(v)
except ValueError:
logger.debug(f"invalid value: {v}")
continue
metric_head = self.generateMetricHeader(header, k)
metric_description = " ".join(header)
metric_description += f" {k}"
if metric_head in self.open_metrics:
logger.error(f"metric name '{metric_head}' already exists")
self.open_metrics[metric_head] = {
"value": v,
"description": metric_description,
}
logger.debug(
f"Adding: metric_head={metric_head}, value={v}, description='{metric_description}'"
)
else:
logger.debug(f"Not dictionary: header={header}, data_dict={data_dict}")
Esse é um método recursivo. Olhando pra uma estrutura como essa aqui:
{
"coretemp-isa-0000":{
"Package id 0":{
"temp1_input": 61.000,
"temp1_max": 100.000,
"temp1_crit": 100.000,
"temp1_crit_alarm": 0.000
}
}
}
O loop quebra o dicionário recebido em k, v, ou "key" e "value".
Na primeira vez que recebe, os dados ficam assim:
{"Package id 0":{"temp1_input": 61.000,"temp1_max": 100.000,"temp1_crit": 100.000,"temp1_crit_alarm": 0.000 }}
Então ele se chama de novo, mas passa no parâmetro header a primeira chave que encontrou: coretemp-isa-0000.
{"temp1_input": 61.000,"temp1_max": 100.000,"temp1_max": 100.000,"temp1_crit_alarm": 0.000 }
Como value ainda é um dictionário, testando com isinstance( ), então ele
adiciona a chave Package id 0 na lista de headers e roda mais uma vez:
Como é um for-loop, ele pega essas chaves e valores.
Daí como tem o histórico do que veio antes dentro de header, fica fácil montar a estrutura.
Basta juntar tudo com "_" como separador.
def generateMetricHeader(self, header: list, key: str) -> str:
metric_header = "_".join(header)
metric_header = re.sub(" ", "_", metric_header)
return f"{metric_header}_{key}"
E é isso que é feito no método generateMetricHeader( ).
Espero que agora tenha ficado um pouco mais claro o que foi que eu fiz nesse programa. E o porquê de não saber se o valor é temperatura em Celsius ou o que seja, uma vez que esse dado não existe na estrutura em JSON gerada. Ou se é tensão em Volts. Fica tudo no mesmo balaio.
Nota: a manpage do sensors descreve que toda temperatura é por padrão em °C pra temperatura.
E também descreve que eu deveria estar usando o parâmetro "-J", que sai no formato que preciso.
Isso fica pra versão 0.2.0 do script 😊.
Rodando simplemente pelo shell, temos a seguinte saída (que pode mudar de acordo com seu hardware):
❯ sensors
coretemp-isa-0000
Adapter: ISA adapter
Package id 0: +61.0°C (high = +100.0°C, crit = +100.0°C)
Core 0: +53.0°C (high = +100.0°C, crit = +100.0°C)
Core 4: +57.0°C (high = +100.0°C, crit = +100.0°C)
Core 8: +61.0°C (high = +100.0°C, crit = +100.0°C)
Core 9: +61.0°C (high = +100.0°C, crit = +100.0°C)
Core 10: +61.0°C (high = +100.0°C, crit = +100.0°C)
Core 11: +61.0°C (high = +100.0°C, crit = +100.0°C)
Core 12: +60.0°C (high = +100.0°C, crit = +100.0°C)
Core 13: +60.0°C (high = +100.0°C, crit = +100.0°C)
Core 14: +60.0°C (high = +100.0°C, crit = +100.0°C)
Core 15: +60.0°C (high = +100.0°C, crit = +100.0°C)
thinkpad-isa-0000
Adapter: ISA adapter
fan1: 2199 RPM
CPU: +60.0°C
GPU: N/A
temp3: +60.0°C
temp4: +0.0°C
temp5: +60.0°C
temp6: +60.0°C
temp7: +60.0°C
temp8: N/A
ucsi_source_psy_USBC000:001-isa-0000
Adapter: ISA adapter
in0: 0.00 V (min = +0.00 V, max = +0.00 V)
curr1: 0.00 A (max = +0.00 A)
BAT0-acpi-0
Adapter: ACPI interface
in0: 12.91 V
power1: 0.00 W
iwlwifi_1-virtual-0
Adapter: Virtual device
temp1: +43.0°C
ucsi_source_psy_USBC000:002-isa-0000
Adapter: ISA adapter
in0: 0.00 V (min = +0.00 V, max = +0.00 V)
curr1: 3.00 A (max = +0.00 A)
nvme-pci-0200
Adapter: PCI adapter
Composite: +45.9°C (low = -273.1°C, high = +85.8°C)
(crit = +86.8°C)
Sensor 1: +48.9°C (low = -273.1°C, high = +65261.8°C)
Sensor 2: +45.9°C (low = -273.1°C, high = +65261.8°C)
acpitz-acpi-0
Adapter: ACPI interface
temp1: +60.0°C
❯ ./sensors-open-metrics.py --printout
#HELP coretemp-isa-0000_Package_id_0_temp1_input coretemp-isa-0000 Package id 0 temp1_input
#TYPE coretemp-isa-0000_Package_id_0_temp1_input gauge
coretemp-isa-0000_Package_id_0_temp1_input: 66.0
#HELP coretemp-isa-0000_Package_id_0_temp1_max coretemp-isa-0000 Package id 0 temp1_max
#TYPE coretemp-isa-0000_Package_id_0_temp1_max gauge
coretemp-isa-0000_Package_id_0_temp1_max: 100.0
#HELP coretemp-isa-0000_Package_id_0_temp1_crit coretemp-isa-0000 Package id 0 temp1_crit
#TYPE coretemp-isa-0000_Package_id_0_temp1_crit gauge
coretemp-isa-0000_Package_id_0_temp1_crit: 100.0
#HELP coretemp-isa-0000_Package_id_0_temp1_crit_alarm coretemp-isa-0000 Package id 0 temp1_crit_alarm
#TYPE coretemp-isa-0000_Package_id_0_temp1_crit_alarm gauge
coretemp-isa-0000_Package_id_0_temp1_crit_alarm: 0.0
#HELP coretemp-isa-0000_Core_0_temp2_input coretemp-isa-0000 Core 0 temp2_input
#TYPE coretemp-isa-0000_Core_0_temp2_input gauge
coretemp-isa-0000_Core_0_temp2_input: 61.0
#HELP coretemp-isa-0000_Core_0_temp2_max coretemp-isa-0000 Core 0 temp2_max
#TYPE coretemp-isa-0000_Core_0_temp2_max gauge
coretemp-isa-0000_Core_0_temp2_max: 100.0
#HELP coretemp-isa-0000_Core_0_temp2_crit coretemp-isa-0000 Core 0 temp2_crit
#TYPE coretemp-isa-0000_Core_0_temp2_crit gauge
coretemp-isa-0000_Core_0_temp2_crit: 100.0
#HELP coretemp-isa-0000_Core_0_temp2_crit_alarm coretemp-isa-0000 Core 0 temp2_crit_alarm
#TYPE coretemp-isa-0000_Core_0_temp2_crit_alarm gauge
coretemp-isa-0000_Core_0_temp2_crit_alarm: 0.0
#HELP coretemp-isa-0000_Core_4_temp6_input coretemp-isa-0000 Core 4 temp6_input
#TYPE coretemp-isa-0000_Core_4_temp6_input gauge
coretemp-isa-0000_Core_4_temp6_input: 58.0
#HELP coretemp-isa-0000_Core_4_temp6_max coretemp-isa-0000 Core 4 temp6_max
#TYPE coretemp-isa-0000_Core_4_temp6_max gauge
coretemp-isa-0000_Core_4_temp6_max: 100.0
#HELP coretemp-isa-0000_Core_4_temp6_crit coretemp-isa-0000 Core 4 temp6_crit
#TYPE coretemp-isa-0000_Core_4_temp6_crit gauge
coretemp-isa-0000_Core_4_temp6_crit: 100.0
#HELP coretemp-isa-0000_Core_4_temp6_crit_alarm coretemp-isa-0000 Core 4 temp6_crit_alarm
#TYPE coretemp-isa-0000_Core_4_temp6_crit_alarm gauge
coretemp-isa-0000_Core_4_temp6_crit_alarm: 0.0
#HELP coretemp-isa-0000_Core_8_temp10_input coretemp-isa-0000 Core 8 temp10_input
#TYPE coretemp-isa-0000_Core_8_temp10_input gauge
coretemp-isa-0000_Core_8_temp10_input: 66.0
#HELP coretemp-isa-0000_Core_8_temp10_max coretemp-isa-0000 Core 8 temp10_max
#TYPE coretemp-isa-0000_Core_8_temp10_max gauge
coretemp-isa-0000_Core_8_temp10_max: 100.0
#HELP coretemp-isa-0000_Core_8_temp10_crit coretemp-isa-0000 Core 8 temp10_crit
#TYPE coretemp-isa-0000_Core_8_temp10_crit gauge
coretemp-isa-0000_Core_8_temp10_crit: 100.0
#HELP coretemp-isa-0000_Core_8_temp10_crit_alarm coretemp-isa-0000 Core 8 temp10_crit_alarm
#TYPE coretemp-isa-0000_Core_8_temp10_crit_alarm gauge
coretemp-isa-0000_Core_8_temp10_crit_alarm: 0.0
#HELP coretemp-isa-0000_Core_9_temp11_input coretemp-isa-0000 Core 9 temp11_input
#TYPE coretemp-isa-0000_Core_9_temp11_input gauge
coretemp-isa-0000_Core_9_temp11_input: 66.0
#HELP coretemp-isa-0000_Core_9_temp11_max coretemp-isa-0000 Core 9 temp11_max
#TYPE coretemp-isa-0000_Core_9_temp11_max gauge
coretemp-isa-0000_Core_9_temp11_max: 100.0
#HELP coretemp-isa-0000_Core_9_temp11_crit coretemp-isa-0000 Core 9 temp11_crit
#TYPE coretemp-isa-0000_Core_9_temp11_crit gauge
coretemp-isa-0000_Core_9_temp11_crit: 100.0
#HELP coretemp-isa-0000_Core_9_temp11_crit_alarm coretemp-isa-0000 Core 9 temp11_crit_alarm
#TYPE coretemp-isa-0000_Core_9_temp11_crit_alarm gauge
coretemp-isa-0000_Core_9_temp11_crit_alarm: 0.0
#HELP coretemp-isa-0000_Core_10_temp12_input coretemp-isa-0000 Core 10 temp12_input
#TYPE coretemp-isa-0000_Core_10_temp12_input gauge
coretemp-isa-0000_Core_10_temp12_input: 66.0
#HELP coretemp-isa-0000_Core_10_temp12_max coretemp-isa-0000 Core 10 temp12_max
#TYPE coretemp-isa-0000_Core_10_temp12_max gauge
coretemp-isa-0000_Core_10_temp12_max: 100.0
#HELP coretemp-isa-0000_Core_10_temp12_crit coretemp-isa-0000 Core 10 temp12_crit
#TYPE coretemp-isa-0000_Core_10_temp12_crit gauge
coretemp-isa-0000_Core_10_temp12_crit: 100.0
#HELP coretemp-isa-0000_Core_10_temp12_crit_alarm coretemp-isa-0000 Core 10 temp12_crit_alarm
#TYPE coretemp-isa-0000_Core_10_temp12_crit_alarm gauge
coretemp-isa-0000_Core_10_temp12_crit_alarm: 0.0
#HELP coretemp-isa-0000_Core_11_temp13_input coretemp-isa-0000 Core 11 temp13_input
#TYPE coretemp-isa-0000_Core_11_temp13_input gauge
coretemp-isa-0000_Core_11_temp13_input: 66.0
#HELP coretemp-isa-0000_Core_11_temp13_max coretemp-isa-0000 Core 11 temp13_max
#TYPE coretemp-isa-0000_Core_11_temp13_max gauge
coretemp-isa-0000_Core_11_temp13_max: 100.0
#HELP coretemp-isa-0000_Core_11_temp13_crit coretemp-isa-0000 Core 11 temp13_crit
#TYPE coretemp-isa-0000_Core_11_temp13_crit gauge
coretemp-isa-0000_Core_11_temp13_crit: 100.0
#HELP coretemp-isa-0000_Core_11_temp13_crit_alarm coretemp-isa-0000 Core 11 temp13_crit_alarm
#TYPE coretemp-isa-0000_Core_11_temp13_crit_alarm gauge
coretemp-isa-0000_Core_11_temp13_crit_alarm: 0.0
#HELP coretemp-isa-0000_Core_12_temp14_input coretemp-isa-0000 Core 12 temp14_input
#TYPE coretemp-isa-0000_Core_12_temp14_input gauge
coretemp-isa-0000_Core_12_temp14_input: 61.0
#HELP coretemp-isa-0000_Core_12_temp14_max coretemp-isa-0000 Core 12 temp14_max
#TYPE coretemp-isa-0000_Core_12_temp14_max gauge
coretemp-isa-0000_Core_12_temp14_max: 100.0
#HELP coretemp-isa-0000_Core_12_temp14_crit coretemp-isa-0000 Core 12 temp14_crit
#TYPE coretemp-isa-0000_Core_12_temp14_crit gauge
coretemp-isa-0000_Core_12_temp14_crit: 100.0
#HELP coretemp-isa-0000_Core_12_temp14_crit_alarm coretemp-isa-0000 Core 12 temp14_crit_alarm
#TYPE coretemp-isa-0000_Core_12_temp14_crit_alarm gauge
coretemp-isa-0000_Core_12_temp14_crit_alarm: 0.0
#HELP coretemp-isa-0000_Core_13_temp15_input coretemp-isa-0000 Core 13 temp15_input
#TYPE coretemp-isa-0000_Core_13_temp15_input gauge
coretemp-isa-0000_Core_13_temp15_input: 61.0
#HELP coretemp-isa-0000_Core_13_temp15_max coretemp-isa-0000 Core 13 temp15_max
#TYPE coretemp-isa-0000_Core_13_temp15_max gauge
coretemp-isa-0000_Core_13_temp15_max: 100.0
#HELP coretemp-isa-0000_Core_13_temp15_crit coretemp-isa-0000 Core 13 temp15_crit
#TYPE coretemp-isa-0000_Core_13_temp15_crit gauge
coretemp-isa-0000_Core_13_temp15_crit: 100.0
#HELP coretemp-isa-0000_Core_13_temp15_crit_alarm coretemp-isa-0000 Core 13 temp15_crit_alarm
#TYPE coretemp-isa-0000_Core_13_temp15_crit_alarm gauge
coretemp-isa-0000_Core_13_temp15_crit_alarm: 0.0
#HELP coretemp-isa-0000_Core_14_temp16_input coretemp-isa-0000 Core 14 temp16_input
#TYPE coretemp-isa-0000_Core_14_temp16_input gauge
coretemp-isa-0000_Core_14_temp16_input: 61.0
#HELP coretemp-isa-0000_Core_14_temp16_max coretemp-isa-0000 Core 14 temp16_max
#TYPE coretemp-isa-0000_Core_14_temp16_max gauge
coretemp-isa-0000_Core_14_temp16_max: 100.0
#HELP coretemp-isa-0000_Core_14_temp16_crit coretemp-isa-0000 Core 14 temp16_crit
#TYPE coretemp-isa-0000_Core_14_temp16_crit gauge
coretemp-isa-0000_Core_14_temp16_crit: 100.0
#HELP coretemp-isa-0000_Core_14_temp16_crit_alarm coretemp-isa-0000 Core 14 temp16_crit_alarm
#TYPE coretemp-isa-0000_Core_14_temp16_crit_alarm gauge
coretemp-isa-0000_Core_14_temp16_crit_alarm: 0.0
#HELP coretemp-isa-0000_Core_15_temp17_input coretemp-isa-0000 Core 15 temp17_input
#TYPE coretemp-isa-0000_Core_15_temp17_input gauge
coretemp-isa-0000_Core_15_temp17_input: 61.0
#HELP coretemp-isa-0000_Core_15_temp17_max coretemp-isa-0000 Core 15 temp17_max
#TYPE coretemp-isa-0000_Core_15_temp17_max gauge
coretemp-isa-0000_Core_15_temp17_max: 100.0
#HELP coretemp-isa-0000_Core_15_temp17_crit coretemp-isa-0000 Core 15 temp17_crit
#TYPE coretemp-isa-0000_Core_15_temp17_crit gauge
coretemp-isa-0000_Core_15_temp17_crit: 100.0
#HELP coretemp-isa-0000_Core_15_temp17_crit_alarm coretemp-isa-0000 Core 15 temp17_crit_alarm
#TYPE coretemp-isa-0000_Core_15_temp17_crit_alarm gauge
coretemp-isa-0000_Core_15_temp17_crit_alarm: 0.0
#HELP thinkpad-isa-0000_fan1_fan1_input thinkpad-isa-0000 fan1 fan1_input
#TYPE thinkpad-isa-0000_fan1_fan1_input gauge
thinkpad-isa-0000_fan1_fan1_input: 2191.0
#HELP thinkpad-isa-0000_CPU_temp1_input thinkpad-isa-0000 CPU temp1_input
#TYPE thinkpad-isa-0000_CPU_temp1_input gauge
thinkpad-isa-0000_CPU_temp1_input: 60.0
#HELP thinkpad-isa-0000_temp3_temp3_input thinkpad-isa-0000 temp3 temp3_input
#TYPE thinkpad-isa-0000_temp3_temp3_input gauge
thinkpad-isa-0000_temp3_temp3_input: 60.0
#HELP thinkpad-isa-0000_temp4_temp4_input thinkpad-isa-0000 temp4 temp4_input
#TYPE thinkpad-isa-0000_temp4_temp4_input gauge
thinkpad-isa-0000_temp4_temp4_input: 0.0
#HELP thinkpad-isa-0000_temp5_temp5_input thinkpad-isa-0000 temp5 temp5_input
#TYPE thinkpad-isa-0000_temp5_temp5_input gauge
thinkpad-isa-0000_temp5_temp5_input: 60.0
#HELP thinkpad-isa-0000_temp6_temp6_input thinkpad-isa-0000 temp6 temp6_input
#TYPE thinkpad-isa-0000_temp6_temp6_input gauge
thinkpad-isa-0000_temp6_temp6_input: 60.0
#HELP thinkpad-isa-0000_temp7_temp7_input thinkpad-isa-0000 temp7 temp7_input
#TYPE thinkpad-isa-0000_temp7_temp7_input gauge
thinkpad-isa-0000_temp7_temp7_input: 60.0
#HELP ucsi_source_psy_USBC000:001-isa-0000_in0_in0_input ucsi_source_psy_USBC000:001-isa-0000 in0 in0_input
#TYPE ucsi_source_psy_USBC000:001-isa-0000_in0_in0_input gauge
ucsi_source_psy_USBC000:001-isa-0000_in0_in0_input: 0.0
#HELP ucsi_source_psy_USBC000:001-isa-0000_in0_in0_min ucsi_source_psy_USBC000:001-isa-0000 in0 in0_min
#TYPE ucsi_source_psy_USBC000:001-isa-0000_in0_in0_min gauge
ucsi_source_psy_USBC000:001-isa-0000_in0_in0_min: 0.0
#HELP ucsi_source_psy_USBC000:001-isa-0000_in0_in0_max ucsi_source_psy_USBC000:001-isa-0000 in0 in0_max
#TYPE ucsi_source_psy_USBC000:001-isa-0000_in0_in0_max gauge
ucsi_source_psy_USBC000:001-isa-0000_in0_in0_max: 0.0
#HELP ucsi_source_psy_USBC000:001-isa-0000_curr1_curr1_input ucsi_source_psy_USBC000:001-isa-0000 curr1 curr1_input
#TYPE ucsi_source_psy_USBC000:001-isa-0000_curr1_curr1_input gauge
ucsi_source_psy_USBC000:001-isa-0000_curr1_curr1_input: 0.0
#HELP ucsi_source_psy_USBC000:001-isa-0000_curr1_curr1_max ucsi_source_psy_USBC000:001-isa-0000 curr1 curr1_max
#TYPE ucsi_source_psy_USBC000:001-isa-0000_curr1_curr1_max gauge
ucsi_source_psy_USBC000:001-isa-0000_curr1_curr1_max: 0.0
#HELP BAT0-acpi-0_in0_in0_input BAT0-acpi-0 in0 in0_input
#TYPE BAT0-acpi-0_in0_in0_input gauge
BAT0-acpi-0_in0_in0_input: 12.909
#HELP BAT0-acpi-0_power1_power1_input BAT0-acpi-0 power1 power1_input
#TYPE BAT0-acpi-0_power1_power1_input gauge
BAT0-acpi-0_power1_power1_input: 0.0
#HELP iwlwifi_1-virtual-0_temp1_temp1_input iwlwifi_1-virtual-0 temp1 temp1_input
#TYPE iwlwifi_1-virtual-0_temp1_temp1_input gauge
iwlwifi_1-virtual-0_temp1_temp1_input: 43.0
#HELP ucsi_source_psy_USBC000:002-isa-0000_in0_in0_input ucsi_source_psy_USBC000:002-isa-0000 in0 in0_input
#TYPE ucsi_source_psy_USBC000:002-isa-0000_in0_in0_input gauge
ucsi_source_psy_USBC000:002-isa-0000_in0_in0_input: 0.0
#HELP ucsi_source_psy_USBC000:002-isa-0000_in0_in0_min ucsi_source_psy_USBC000:002-isa-0000 in0 in0_min
#TYPE ucsi_source_psy_USBC000:002-isa-0000_in0_in0_min gauge
ucsi_source_psy_USBC000:002-isa-0000_in0_in0_min: 0.0
#HELP ucsi_source_psy_USBC000:002-isa-0000_in0_in0_max ucsi_source_psy_USBC000:002-isa-0000 in0 in0_max
#TYPE ucsi_source_psy_USBC000:002-isa-0000_in0_in0_max gauge
ucsi_source_psy_USBC000:002-isa-0000_in0_in0_max: 0.0
#HELP ucsi_source_psy_USBC000:002-isa-0000_curr1_curr1_input ucsi_source_psy_USBC000:002-isa-0000 curr1 curr1_input
#TYPE ucsi_source_psy_USBC000:002-isa-0000_curr1_curr1_input gauge
ucsi_source_psy_USBC000:002-isa-0000_curr1_curr1_input: 3.0
#HELP ucsi_source_psy_USBC000:002-isa-0000_curr1_curr1_max ucsi_source_psy_USBC000:002-isa-0000 curr1 curr1_max
#TYPE ucsi_source_psy_USBC000:002-isa-0000_curr1_curr1_max gauge
ucsi_source_psy_USBC000:002-isa-0000_curr1_curr1_max: 0.0
#HELP nvme-pci-0200_Composite_temp1_input nvme-pci-0200 Composite temp1_input
#TYPE nvme-pci-0200_Composite_temp1_input gauge
nvme-pci-0200_Composite_temp1_input: 45.85
#HELP nvme-pci-0200_Composite_temp1_max nvme-pci-0200 Composite temp1_max
#TYPE nvme-pci-0200_Composite_temp1_max gauge
nvme-pci-0200_Composite_temp1_max: 85.85
#HELP nvme-pci-0200_Composite_temp1_min nvme-pci-0200 Composite temp1_min
#TYPE nvme-pci-0200_Composite_temp1_min gauge
nvme-pci-0200_Composite_temp1_min: -273.15
#HELP nvme-pci-0200_Composite_temp1_crit nvme-pci-0200 Composite temp1_crit
#TYPE nvme-pci-0200_Composite_temp1_crit gauge
nvme-pci-0200_Composite_temp1_crit: 86.85
#HELP nvme-pci-0200_Composite_temp1_alarm nvme-pci-0200 Composite temp1_alarm
#TYPE nvme-pci-0200_Composite_temp1_alarm gauge
nvme-pci-0200_Composite_temp1_alarm: 0.0
#HELP nvme-pci-0200_Sensor_1_temp2_input nvme-pci-0200 Sensor 1 temp2_input
#TYPE nvme-pci-0200_Sensor_1_temp2_input gauge
nvme-pci-0200_Sensor_1_temp2_input: 50.85
#HELP nvme-pci-0200_Sensor_1_temp2_max nvme-pci-0200 Sensor 1 temp2_max
#TYPE nvme-pci-0200_Sensor_1_temp2_max gauge
nvme-pci-0200_Sensor_1_temp2_max: 65261.85
#HELP nvme-pci-0200_Sensor_1_temp2_min nvme-pci-0200 Sensor 1 temp2_min
#TYPE nvme-pci-0200_Sensor_1_temp2_min gauge
nvme-pci-0200_Sensor_1_temp2_min: -273.15
#HELP nvme-pci-0200_Sensor_2_temp3_input nvme-pci-0200 Sensor 2 temp3_input
#TYPE nvme-pci-0200_Sensor_2_temp3_input gauge
nvme-pci-0200_Sensor_2_temp3_input: 45.85
#HELP nvme-pci-0200_Sensor_2_temp3_max nvme-pci-0200 Sensor 2 temp3_max
#TYPE nvme-pci-0200_Sensor_2_temp3_max gauge
nvme-pci-0200_Sensor_2_temp3_max: 65261.85
#HELP nvme-pci-0200_Sensor_2_temp3_min nvme-pci-0200 Sensor 2 temp3_min
#TYPE nvme-pci-0200_Sensor_2_temp3_min gauge
nvme-pci-0200_Sensor_2_temp3_min: -273.15
#HELP acpitz-acpi-0_temp1_temp1_input acpitz-acpi-0 temp1 temp1_input
#TYPE acpitz-acpi-0_temp1_temp1_input gauge
acpitz-acpi-0_temp1_temp1_input: 60.0
Os dados aparecem no #HELP simplesmente sem os _.
Talvez eu devesse melhorar e fazer com que alguns valores fosse labels do valor maior.
Mas por enquanto está funcionando assim.
E pra rodar como serviço:
❯ ./sensors-open-metrics.py
[2025-12-12 19:54:43] (INFO) starting service on port 9090
INFO: Started server process [1078630]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://127.0.0.1:9090 (Press CTRL+C to quit)
[2025-12-12 19:54:53] (INFO) serving web page on /metrics
INFO: 127.0.0.1:52250 - "GET /metrics HTTP/1.1" 200 OK
Em outro terminal:
❯ curl localhost:9090/metrics
#HELP coretemp-isa-0000_Package_id_0_temp1_input coretemp-isa-0000 Package id 0 temp1_input
#TYPE coretemp-isa-0000_Package_id_0_temp1_input gauge
coretemp-isa-0000_Package_id_0_temp1_input: 62.0
#HELP coretemp-isa-0000_Package_id_0_temp1_max coretemp-isa-0000 Package id 0 temp1_max
#TYPE coretemp-isa-0000_Package_id_0_temp1_max gauge
coretemp-isa-0000_Package_id_0_temp1_max: 100.0
#HELP coretemp-isa-0000_Package_id_0_temp1_crit coretemp-isa-0000 Package id 0 temp1_crit
#TYPE coretemp-isa-0000_Package_id_0_temp1_crit gauge
coretemp-isa-0000_Package_id_0_temp1_crit: 100.0
#HELP coretemp-isa-0000_Package_id_0_temp1_crit_alarm coretemp-isa-0000 Package id 0 temp1_crit_alarm
[...]
Pra rodar como serviço do systemd em /etc/systemd/system/sensors-open-metrics.service:
[Unit]
Description=Temperature sensors
Wants=network-online.target
After=network-online.target nginx.service
[Service]
Restart=always
User=root
Group=root
WorkingDirectory=/tmp
ExecStart=/usr/local/bin/sensors-open-metrics.py
TimeoutStopSec=5s
[Install]
WantedBy=multi-user.target
E finalmente adicionando em /etc/alloy/config.alloy:
[...]
discovery.relabel "temperature_metrics" {
targets = array.concat(
[{
__address__ = "localhost:9090",
}],
)
rule {
source_labels = ["__address__"]
target_label = "instance"
replacement = "nome_do_servidor"
}
}
prometheus.scrape "temperature_metrics" {
targets = discovery.relabel.temperature_metrics.output
forward_to = [prometheus.remote_write.prod.receiver]
job_name = "temperature"
}
Daí é só evocar o Grafana e partir por gráficos.
Update: atualizado em
Sat Dec 13 02:43:59 PM CET 2025
com mais informação sobre a parte de script.
