helio.loureiro.eng.br
  • Home
  • Unix
  • Linux
  • Blog
  • Python
  • Programação
  • Tudo
  • Suécia
  1. You are here:  
  2. Home

Os artigos mais lidos de 2025

  • Configurando o grafana alloy pra monitorar VMs
  • Configurando traefik com ssh
  • Configurando o teclado Keychron C3 no Linux
  • Parâmetros de compilação pra Go!
  • Script pra verificar e atualizar os certificados dos domínios com letsencrypt

Home

lm-sensors como open metrics com python

Details
Written by: Helio Loureiro
Category: Python
Published: December 12, 2025
Hits: 63
  • json
  • grafana
  • alloy
  • open metrics
  • prometheus
  • sensors

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:

https://github.com/helioloureiro/homemadescripts/blob/master/sensors-open-metrics.py

Olhando o código mais de perto

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:

  • key: "coretemp-isa-0000"
  • value: {"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.

  • key: "Package id 0"
  • value: {"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:

  • key=temp1_input, value=61.000
  • key=temp1_max, value=100.000
  • key=temp1_max, value=100.000
  • key=temp1_crit_alarm, value=0.000

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

Executando o programa

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.

Placa ASUS Pro WS WRX80E-SAGE SE WIFI parte 2

Details
Written by: Helio Loureiro
Category: Linux
Published: December 12, 2025
Hits: 82
  • hardware
  • motherboard
  • ASUS

E a luta continua. Agora com 4 placas NVIDIA instaladas, a configuração foi ajustada pra tudo funcionar corretamente.

E tenho que agradecer totalmente ao DevOps Engineer anterior, Peter, que fez esse trabalho todo. Sem ele eu ainda estaria pastando com a configuração da BIOS.

O que foi alterado está na tabela abaixo:

BIOSEstadoNota
SVM ModeEnabledCPU Virtualization
BME DMS/A MitigationEnabled
Launch CSMEnabled
Boot from NetworkDisabled
Boot from Storage DevicesUEFI Only
Above 4G DecodingEnabled
Re-Size BAR SupportDisabled
SR-IOVEnabledPCI Virtualization
SPCIX16_1GEN 3Importante
SPCIX16_2GEN 3Importante
SPCIX16_3GEN 3Importante
SPCIX16_4GEN 3Importante
SPCIX16_5GEN 3Importante
SPCIX16_6GEN 3Importante
SPCIX16_7GEN 3Importante
WIFI 6DisabledEconomizar pros IMMOU Groups
Bluetooth ControllerDisabledEconomizar pros IMMOU Groups
HD Audio ControllerDisabledEconomizar pros IMMOU Groups

Com essa configuração eu pude remover o parâmetro pci=nommconf do grub e ter o sistema funcionando.

Por enquanto tudo está rodando redondinho na máquina.

E eu acabei desmontando e invertendo os lados da fonte pra ficar mais fácil de operar. Deixei do mesmo lado da entrada de I/O como cabos de rede, USB, etc.

Apesar da configuração pra todas as entradas EISA/PCI, estamos usando só as ímpares: 1, 3, 5 e 7.

Placa ASUS Pro WS WRX80E-SAGE SE WIFI e nvme não detectado

Details
Written by: Helio Loureiro
Category: Linux
Published: December 05, 2025
Hits: 146
  • linux
  • Ubuntu
  • nvme

Recebi a missão de montar uma máquina que será usada pra AI na firma. Na foto ainda está o esqueleto dela, que também montei. E com somente uma fonte.

Era o teste pra ver se ligava. E ligou.

Ontem fui instalar o sistema operacional, que será Ubuntu, e... cadê o nvme? Passei algum tempo pesquisando até que achei que é algo com a mother board, a ASUS Pro WS WRX80E-SAGE SE WIFI. O artigo que ajudou foi esse aqui:

https://forum.level1techs.com/t/solved-asus-pro-ws-wrx80e-sage-se-wifi-not-detecting-all-my-nvme-drives-in-proxmox/189373

tl;dr: é preciso passar o parâmetro pci=nommconf no boot pro kernel achar o nvme.

Quando estiver montada, provavelmente com metade das placas nvidia porque só recebemos 2 das 4 compradas, faço outro post com a foto e mais alguns dados.

Páginas de manual coloridas

Details
Written by: Helio Loureiro
Category: Linux
Published: December 01, 2025
Hits: 170
  • manpage

Essa é uma dica pra deixar a páginas do manual, vulgo man, coloridas.

É possível fazer em formato Bourne alike assim:

  
LESS_TERMCAP_mb=$(tput bold; tput setaf 2) # green
LESS_TERMCAP_md=$(tput bold; tput setaf 6) # cyan
LESS_TERMCAP_me=$(tput sgr0)
LESS_TERMCAP_so=$(tput bold; tput setaf 3; tput setab 4) # yellow on blue
LESS_TERMCAP_se=$(tput rmso; tput sgr0)
LESS_TERMCAP_us=$(tput smul; tput bold; tput setaf 7) # white
LESS_TERMCAP_ue=$(tput rmul; tput sgr0)
LESS_TERMCAP_mr=$(tput rev)
LESS_TERMCAP_mh=$(tput dim)
LESS_TERMCAP_ZN=$(tput ssubm)
LESS_TERMCAP_ZV=$(tput rsubm)
LESS_TERMCAP_ZO=$(tput ssupm)
LESS_TERMCAP_ZW=$(tput rsupm)
GROFF_NO_SGR=1

export LESS_TERMCAP_mb LESS_TERMCAP_md LESS_TERMCAP_me \
    LESS_TERMCAP_so LESS_TERMCAP_se LESS_TERMCAP_us \
    LESS_TERMCAP_ue LESS_TERMCAP_mr LESS_TERMCAP_mh \
    LESS_TERMCAP_ZN LESS_TERMCAP_ZV LESS_TERMCAP_ZO \
    LESS_TERMCAP_ZW GROFF_NO_SGR
  

Mas como uso fish, então adicionei as seguintes linhas em ~/.config/fish/conf.d/termcap.fish:

  
set -gx LESS_TERMCAP_mb (tput bold; tput setaf 2) # green
set -gx LESS_TERMCAP_md (tput bold; tput setaf 6) # cyan
set -gx LESS_TERMCAP_me (tput sgr0)
set -gx LESS_TERMCAP_so (tput bold; tput setaf 3; tput setab 4) # yellow on blue
set -gx LESS_TERMCAP_se (tput rmso; tput sgr0)
set -gx LESS_TERMCAP_us (tput smul; tput bold; tput setaf 7) # white
set -gx LESS_TERMCAP_ue (tput rmul; tput sgr0)
set -gx LESS_TERMCAP_mr (tput rev)
set -gx LESS_TERMCAP_mh (tput dim)
set -gx LESS_TERMCAP_ZN (tput ssubm)
set -gx LESS_TERMCAP_ZV (tput rsubm)
set -gx LESS_TERMCAP_ZO (tput ssupm)
set -gx LESS_TERMCAP_ZW (tput rsupm)
set -gx GROFF_NO_SGR 1    
  

Problema do ctrl+c no terminal

Details
Written by: Helio Loureiro
Category: Linux
Published: December 01, 2025
Hits: 166
  • Ubuntu
  • shell

Por algum motivo bizarro que não sei explicar, comecei a ter problema pra matar os programas rodando no terminal com ctrl+c.

  
❯ ping 10.4.6.101
PING 10.4.6.101 (10.4.6.101) 56(84) bytes of data.
^C^[^C^[^C^[^C^[^C^\^\^\^\

^C^C^Cfish: Job 1, 'ping 10.4.6.101' terminated by signal SIGKILL (Forced quit)    
  

Tentei de tudo: ctrl+alt+c, ctrl+alt+\, etc. Nenhum resultado adiantou e precisei sempre abrir outro terminal e mandar um kill no processo.

Mas hoje eu achei um artigo que corrigiu o problema:

https://unix.stackexchange.com/questions/18589/ctrlc-does-not-work-in-gnome-terminal

Então bastou um simples stty sane pra resolver de vez o problema.

  
❯ stty sane
❯ ping 10.4.6.101
PING 10.4.6.101 (10.4.6.101) 56(84) bytes of data.
^C
--- 10.4.6.101 ping statistics ---
2 packets transmitted, 0 received, 100% packet loss, time 1014ms
  

Adicionando backup do DB no GoToSo

Details
Written by: Helio Loureiro
Category: Blog
Published: November 29, 2025
Hits: 175
  • fediverso
  • GoToSo
  • systemd
  • gotosocial

Com tudo rodando redondo na minha bolha, bolha minha, resta melhorar a resiliência com backups. Até agora apostei no zfs como saída pra qualquer problema. Mas isso é jogar com a sorte.

Fiz um pequeno e simples script de backup que executa dentro do container, que roda via podman, e faz o pg_dumpall do postgres.

  
#! /usr/bin/env bash
#
#pg_dumpall -U $POSTGRES_USER -f full-backup-$(date "+%Y-%m-%dT%H:%M:%S").sql
#
RETENTION=12

timestamp=$(date "+%Y-%m-%dT%H:%M:%S")
current_dir=$(readlink -f $0)
current_dir=$(dirname $current_dir)
program=$0
program=$(basename $program)
backup_file="full-backup-$timestamp.sql"


echo "Backup on: $timestamp"

podman exec postgres \
        bash -c "pg_dumpall -U \$POSTGRES_USER -f /var/lib/postgresql/backup/$backup_file"

bzip2 $current_dir/postgresql/backup/$backup_file

for filename in $(find $current_dir/postgresql/backup -print | grep bz2 | sort | sed "1,${RETENTION}d")
do
        echo "Removing: $filename"
        rm -f $filename
done
  

E coloquei RETENTION=12 pra manter os últimos 12 backups. Mas por quê 12? Sei lá. Pareceu um número legal.

Eu poderia rodar tudo via crontab, como geralmente faço. Mas nesse caso eu resolvi fazer com timer do systemd. Então primeiro criei o serviço de backup e depois o timer que ativa esse serviço.

  
❯ systemctl --user --force --full edit gotosocial-backup.service
❯ systemctl --user --force --full edit gotosocial-backup.timer
  

Assim são criados o serviço e o timer em modo usuário.

O gotosocial-backup.service:

  
[Unit]
Wants=gotosocial.service
After=gotosocial.service

[Service]
Type=oneshot
ExecStart=/home/helio/gotosocial/run-backup.sh
WorkingDirectory=/home/helio/gotosocial

[Install]
WantedBy=default.target    
  

O gotosocial-backup.timer:

  
[Unit]
Description=GoToSo daily backup

[Timer]
OnCalendar=daily
Persistent=true

[Install]
WantedBy=timers.target    
  

Após serem criados, eles ficam em seu diretório de usuário: /home/helio/.config/systemd/user/gotosocial-backup.{service,timer}

Daí é só habilitar ambos. Ao fazer isso, um backup será gerado pois o systemd vai rodar o serviço ao ser ativado.

  
❯ systemctl enable --now --user gotosocial-backup.service
❯ systemctl enable --now --user gotosocial-backup.timer    
    

Claro que ainda não testei restaurar nenhum desses backups. Vou deixar pra fazer isso quando tiver algum problema de verdade. No momento eles estão salvos em /home/helio/gotosocial/postgresql/backup que é onde apontei como volume dentro compose.yaml do podman.

  
❯ ls -1 /home/helio/gotosocial/postgresql/backup/
 full-backup-2025-11-29T13:17:38.sql.bz2
 full-backup-2025-11-29T13:46:32.sql.bz2
 full-backup-2025-11-29T13:47:14.sql.bz2    
  

E também não testei meu "retention". Daqui 9 dias eu conto se deu certo.

E o ano do Linux no desktop continua com força total

Details
Written by: Helio Loureiro
Category: Linux
Published: November 16, 2025
Hits: 347
  • Linux desktop
  • Ano do Linux no desktop

E o progresso do Linux no desktop continua. Mais e mais canais no YouTube passaram a usar Linux pra uma ou outra coisa.

Se antes Linux era a coisa de nerd e geeks, agora virou mainstream.

Não que pudesse ser diferente. Basta ver o que é Android hoje em dia. Não é nicho. É mainstream.

Eu já comentei do canal do PewDiePie.

Passei a acompanhar também outro canal, o Switch and Click. Está uma delícia acompanhar as aventuras dela com Linux. De Mint ela já passou pra arch. E agora Omarchy.

E pra finalizar, o canal do Leon e da Nice. Esses, Windowzeros raizes. Mas vou dizer que a grande ajuda foi terem já migrado pro macOS. Isso "azeitou" a mudança ou uso em paralelo do Linux.

E por último, um último canal recomendado recentement no grupo Linux Brasil do Telegram. O cara instala Linux numa máquina super limitada e... funciona. Não que isso seja surpresa.

Habilitando Prometheus pra coletar as métricas do GoToSo

Details
Written by: Helio Loureiro
Category: Observability
Published: October 25, 2025
Hits: 495
  • open metrics
  • prometheus
  • GoToSo

Numa discussão no fediverso falamos sobre habilitar as métricas do GoToSo, também conhecido como GoToSocial. Fiz algumas mudanças e consegui expor essas métricas.

O compose.yaml do GoToSo:

  
services:
  gotosocial:
    image: docker.io/superseriousbusiness/gotosocial:latest
    container_name: gotosocial
    user: 1000:1000
    networks:
      - gotosocial
    environment:
      GTS_HOST: bolha.linux-br.org
      GTS_DB_TYPE: postgres
      GTS_CONFIG_PATH: /gotosocial/config.yaml
      [...]
      OTEL_METRICS_PRODUCERS: prometheus
      OTEL_METRICS_EXPORTER: prometheus
      OTEL_EXPORTER_PROMETHEUS_HOST: 0.0.0.0
      OTEL_EXPORTER_PROMETHEUS_PORT: 9090
      [...]
    ports:
      - "8080:8080"
      - "9090:9090"
    [...]
  

A config.yaml também do GoToSo:

  
[...]
media-emoji-local-max-size: 250KiB
media-emoji-remote-max-size: 250KiB
advanced-rate-limit-requests: 0
metrics-enabled: true
  

Uma vez que isso estava habilitado e o container reiniciado, foi só verificar a porta 9090.

  
❯ curl -s localhost:9090/metrics | head -10
# HELP go_config_gogc_percent Heap size target percentage configured by the user, otherwise 100.
# TYPE go_config_gogc_percent gauge
go_config_gogc_percent{otel_scope_name="go.opentelemetry.io/contrib/instrumentation/runtime",otel_scope_schema_url="",otel_scope_version="0.63.0"} 100
# HELP go_goroutine_count Count of live goroutines.
# TYPE go_goroutine_count gauge
go_goroutine_count{otel_scope_name="go.opentelemetry.io/contrib/instrumentation/runtime",otel_scope_schema_url="",otel_scope_version="0.63.0"} 167
# HELP go_memory_allocated_bytes_total Memory allocated to the heap by the application.
# TYPE go_memory_allocated_bytes_total counter
go_memory_allocated_bytes_total{otel_scope_name="go.opentelemetry.io/contrib/instrumentation/runtime",otel_scope_schema_url="",otel_scope_version="0.63.0"} 1.6433066284e+11
# HELP go_memory_allocations_total Count of allocations to the heap by the application.    
  

Em seguida subi um container, também com podman, pra coletar esses dados. Junto com um prometheus-exporter pra coletar dados da máquina.

compose.yaml:

  
services:
  prometheus:
    image: quay.io/prometheus/prometheus
    container_name: prometheus
    environment:
      TZ: Europe/Stockholm
    ports:
      - "9000:9090"
    volumes:
      - data:/prometheus
      - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
    restart: unless-stopped
    extra_hosts:
      - localserver:192.168.1.2
        
volumes:
  data:
  

prometheus.yml:

  
global:
  scrape_interval: 15s
  evaluation_interval: 15s

scrape_configs:
  - job_name: "prometheus"
    static_configs:
      - targets: 
        - "localhost:9090"
        labels:
          app: "prometheus"

  - job_name: "mimir"
    static_configs:
      - targets:
        - "localserver:9100"
        labels:
          app: "mimir"

  - job_name: "gotoso"
    static_configs:
      - targets:
        - "localserver:9090"
        labels:
          app: "gotoso"    
  

Isso já faz subir e você pode olhar no target health.

Daí é deixar o Prometheus coletar os dados e depois olhar os gráficos.

Ninguém me segurou, mas também não instalei o Grafana.

Por enquanto...

Selfies de 1998 até 2025

Details
Written by: Helio Loureiro
Category: Blog
Published: October 25, 2025
Hits: 486
  • youtube
  • selfies
  • Google Photos

Um pequeno vídeo juntando todas a fotos que o Google Photos mostrou com a busca por "selfie".

Minha bolha, bolha minha

Details
Written by: Helio Loureiro
Category: Blog
Published: October 17, 2025
Hits: 595
  • containers
  • fediverso
  • bolha
  • podman

E a bolha está de pé. Ou quase isso.

A primeira semana em operação foi erro 502 o tempo todo. Achei que o problema era como estava funcionando pelo systemd. Então criei um serviço novo só pra ela.

  
# /etc/systemd/user/podman-compose@.service

[Unit]
Description=GoToSocial as container service
StartLimitIntervalSec=0


[Service]
Type=simple
User=helio
Group=helio
#WorkingDirectory=/home/helio/gotosocial
ExecStart=/home/helio/gotosocial/entrypoint.sh start
ExecStop=/home/helio/gotosocial/entrypoint.sh stop
Restart=always
RestartSec=30

[Install]
WantedBy=default.target    
  

Depois achei que era o enviroment. Comentei a parte de WorkingDirectory, como pode ser visto acima. Também troquei o podman-compose up por esse script entrypoint.sh.

  
#! /usr/bin/env bash

GOTOSOCIAL_DIR="/home/helio/gotosocial"

start_gotosocial() {
    echo "Starting gotosocial" 
    cd $GOTOSOCIAL_DIR
    /usr/bin/podman pull docker.io/superseriousbusiness/gotosocial:latest
    /usr/bin/podman pull docker.io/library/postgres:latest
    /usr/bin/podman-compose down
    sleep 5
    /usr/bin/podman-compose up
}

stop_gotosocial() {
    echo "Stopping GoToSocial"
    cd $GOTOSOCIAL_DIR
    /usr/bin/podman-compose down
}

case $1 in
        start) start_gotosocial ;;
        stop) stop_gotosocial ;;
        restart) $0 stop
             sleep 30
                     $0 start
                     ;;
        *) echo "Unknown option: $1" 
           exit 1
esac    
  

Os podman pull estavam antes no serviço do systemd. Joguei tudo pra dentro do script. E o resultado foi: 502.

Então comecei a considerar que tinha feito algo errado no compose.yml.

  
services:
  gotosocial:
    image: docker.io/superseriousbusiness/gotosocial:latest
    container_name: gotosocial
    user: 1000:1000
    networks:
      - gotosocial
    environment:
      # Change this to your actual host value.
      GTS_HOST: bolha.linux-br.org
      GTS_DB_TYPE: postgres
      GTS_CONFIG_PATH: /gotosocial/config.yaml
      # Path in the GtS Docker container where the
      # Wazero compilation cache will be stored.
      GTS_WAZERO_COMPILATION_CACHE: /gotosocial/.cache
      ## For reverse proxy setups:
      GTS_TRUSTED_PROXIES: "127.0.0.1,::1,172.18.0.0/16"

      ## Set the timezone of your server:
      TZ: Europe/Stockholm
    ports:
      - "127.0.0.1:8080:8080"
    volumes:
      - data:/gotosocial/storage
      - cache:/gotosocial/.cache
      - ~/gotosocial/config.yaml:/gotosocial/config.yaml
    restart: unless-stopped
    healthcheck:
      test: wget --no-vebose --tries=1 --spider http://localhost:8080/readyz
      interval: 10s
      retries: 5
      start_period: 30s
    depends:
      - postgres
        
  postgres:
    image: docker.io/library/postgres:latest
    container_name: postgres
    networks:
      - gotosocial
    environment:
      POSTGRES_PASSWORD: *****
      POSTGRES_USER: gotosocial
      POSTGRES_DB: gotosocial
    restart: unless-stopped
    volumes:
      - ~/gotosocial/postgresql:/var/lib/postgresql
    ports:
      - "5432:5432"
    healthcheck:
      test: pg_isready
      interval: 10s 
      timeout: 5s
      retries: 5
      start_period: 120s

networks:
  gotosocial:
    ipam:
      driver: default
      config:
        - subnet: "172.18.0.0/16"
          gateway: "172.18.0.1"
volumes:
  data:
  cache:
  

Nada de muito fantástico. Um postgres rodando junto com um gotosocial. Algumas configurações de proxy, que é o nginx da máquina, e é isso. E continuava o 502.

Mas se eu entrava na máquina, e rodava uma sessão de tmux e dentro dela chamava o podman-compose up, daí tudo funcionava. Dei então uma olhada no erro.

  
Oct 15 10:16:56 mimir entrypoint.sh[1895291]: podman-compose version: 1.0.6
Oct 15 10:16:56 mimir entrypoint.sh[1895291]: ['podman', '--version', '']
Oct 15 10:16:56 mimir entrypoint.sh[1895291]: using podman version: 4.9.3
Oct 15 10:16:56 mimir entrypoint.sh[1895291]: ** excluding:  set()
Oct 15 10:16:56 mimir entrypoint.sh[1895291]: ['podman', 'ps', '--filter', 'label=io.podman.compose.project=gotosocial', '-a', '--format', '{{ index .Labels "io.podman.compose.config-hash"}}']
Oct 15 10:16:56 mimir entrypoint.sh[1895303]: time="2025-10-15T10:16:56+02:00" level=warning msg="RunRoot is pointing to a path (/run/user/1000/containers) which is not writable. Most likely podman will fail."
Oct 15 10:16:56 mimir entrypoint.sh[1895303]: Error: default OCI runtime "crun" not found: invalid argument
Oct 15 10:16:57 mimir entrypoint.sh[1895291]: Traceback (most recent call last):
Oct 15 10:16:57 mimir entrypoint.sh[1895291]:   File "/usr/bin/podman-compose", line 33, in 
Oct 15 10:16:57 mimir entrypoint.sh[1895291]:     sys.exit(load_entry_point('podman-compose==1.0.6', 'console_scripts', 'podman-compose')())
Oct 15 10:16:57 mimir entrypoint.sh[1895291]:              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Oct 15 10:16:57 mimir entrypoint.sh[1895291]:   File "/usr/lib/python3/dist-packages/podman_compose.py", line 2941, in main
Oct 15 10:16:57 mimir entrypoint.sh[1895291]:     podman_compose.run()
Oct 15 10:16:57 mimir entrypoint.sh[1895291]:   File "/usr/lib/python3/dist-packages/podman_compose.py", line 1423, in run
Oct 15 10:16:57 mimir entrypoint.sh[1895291]:     cmd(self, args)
Oct 15 10:16:57 mimir entrypoint.sh[1895291]:   File "/usr/lib/python3/dist-packages/podman_compose.py", line 1754, in wrapped
Oct 15 10:16:57 mimir entrypoint.sh[1895291]:     return func(*args, **kw)
Oct 15 10:16:57 mimir entrypoint.sh[1895291]:            ^^^^^^^^^^^^^^^^^
Oct 15 10:16:57 mimir entrypoint.sh[1895291]:   File "/usr/lib/python3/dist-packages/podman_compose.py", line 2038, in compose_up
Oct 15 10:16:57 mimir entrypoint.sh[1895291]:     compose.podman.output(
Oct 15 10:16:57 mimir entrypoint.sh[1895291]:   File "/usr/lib/python3/dist-packages/podman_compose.py", line 1098, in output
Oct 15 10:16:57 mimir entrypoint.sh[1895291]:     return subprocess.check_output(cmd_ls)
Oct 15 10:16:57 mimir entrypoint.sh[1895291]:            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Oct 15 10:16:57 mimir entrypoint.sh[1895291]:   File "/usr/lib/python3.12/subprocess.py", line 466, in check_output
Oct 15 10:16:57 mimir entrypoint.sh[1895291]:     return run(*popenargs, stdout=PIPE, timeout=timeout, check=True,
Oct 15 10:16:57 mimir entrypoint.sh[1895291]:            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Oct 15 10:16:57 mimir entrypoint.sh[1895291]:   File "/usr/lib/python3.12/subprocess.py", line 571, in run
Oct 15 10:16:57 mimir entrypoint.sh[1895291]:     raise CalledProcessError(retcode, process.args,
Oct 15 10:16:57 mimir entrypoint.sh[1895291]: subprocess.CalledProcessError: Command '['podman', 'ps', '--filter', 'label=io.podman.compose.project=gotosocial', '-a', '--format', '{{ index .Labels "io.podman.compose.config-hash"}}']' returned non-zero exit status 125.
Oct 15 10:16:57 mimir systemd[1]: gotosocial.service: Main process exited, code=exited, status=1/FAILURE
Oct 15 10:16:57 mimir systemd[1]: gotosocial.service: Failed with result 'exit-code'.
Oct 15 10:16:57 mimir systemd[1]: gotosocial.service: Consumed 1.481s CPU time.
Oct 15 10:17:27 mimir systemd[1]: gotosocial.service: Scheduled restart job, restart counter is at 1280.
Oct 15 10:17:27 mimir systemd[1]: Started gotosocial.service - GoToSocial as container service.
Oct 15 10:17:27 mimir entrypoint.sh[1895707]: Starting gotosocial
Oct 15 10:17:30 mimir entrypoint.sh[1895781]: podman-compose version: 1.0.6
Oct 15 10:17:30 mimir entrypoint.sh[1895781]: ['podman', '--version', '']
Oct 15 10:17:30 mimir entrypoint.sh[1895781]: using podman version: 4.9.3
Oct 15 10:17:30 mimir entrypoint.sh[1895781]: ** excluding:  set()
Oct 15 10:17:30 mimir entrypoint.sh[1895781]: podman stop -t 10 postgres
Oct 15 10:17:31 mimir entrypoint.sh[1895781]: exit code: 0
Oct 15 10:17:31 mimir entrypoint.sh[1895781]: podman stop -t 10 gotosocial
Oct 15 10:17:31 mimir entrypoint.sh[1895781]: exit code: 0
Oct 15 10:17:31 mimir entrypoint.sh[1895781]: podman rm postgres
Oct 15 10:17:31 mimir entrypoint.sh[1895781]: exit code: 0
Oct 15 10:17:31 mimir entrypoint.sh[1895781]: podman rm gotosocial
Oct 15 10:17:31 mimir entrypoint.sh[1895781]: exit code: 0
Oct 15 10:17:36 mimir entrypoint.sh[1895920]: podman-compose version: 1.0.6
Oct 15 10:17:36 mimir entrypoint.sh[1895920]: ['podman', '--version', '']
Oct 15 10:17:36 mimir entrypoint.sh[1895920]: using podman version: 4.9.3
Oct 15 10:17:36 mimir entrypoint.sh[1895920]: ** excluding:  set()
Oct 15 10:17:36 mimir entrypoint.sh[1895920]: ['podman', 'ps', '--filter', 'label=io.podman.compose.project=gotosocial', '-a', '--format', '{{ index .Labels "io.podman.compose.config-hash"}}']
Oct 15 10:17:36 mimir entrypoint.sh[1895920]: podman volume inspect gotosocial_data || podman volume create gotosocial_data
Oct 15 10:17:36 mimir entrypoint.sh[1895920]: ['podman', 'volume', 'inspect', 'gotosocial_data']
Oct 15 10:17:36 mimir entrypoint.sh[1895920]: podman volume inspect gotosocial_cache || podman volume create gotosocial_cache
Oct 15 10:17:36 mimir entrypoint.sh[1895920]: ['podman', 'volume', 'inspect', 'gotosocial_cache']
Oct 15 10:17:36 mimir entrypoint.sh[1895920]: ['podman', 'network', 'exists', 'gotosocial_gotosocial']
Oct 15 10:17:36 mimir entrypoint.sh[1895920]: podman create --name=gotosocial --label io.podman.compose.config-hash=4f4b10e0c67c04b7b4f2392784b378735d4378d9d411f1405cf3819c6207bd1a --label io.podman.compose.project=gotosocial --label io.podman.compose.version=1.0.6 --label PODMAN_SYSTEMD_UNIT=This email address is being protected from spambots. You need JavaScript enabled to view it. --label com.docker.compose.project=gotosocial --label com.docker.compose.project.working_dir=/home/helio/gotosocial --label com.docker.compose.project.config_files=compose.yaml --label com.docker.compose.container-number=1 --label com.docker.compose.service=gotosocial -e GTS_HOST=bolha.linux-br.org -e GTS_DB_TYPE=postgres -e GTS_CONFIG_PATH=/gotosocial/config.yaml -e GTS_WAZERO_COMPILATION_CACHE=/gotosocial/.cache -e GTS_TRUSTED_PROXIES=127.0.0.1,::1,172.18.0.0/16 -e TZ=Europe/Stockholm -v gotosocial_data:/gotosocial/storage -v gotosocial_cache:/gotosocial/.cache -v /home/helio/gotosocial/config.yaml:/gotosocial/config.yaml --net gotosocial_gotosocial --network-alias gotosocial -p 127.0.0.1:8080:8080 -u 1000:1000 --restart unless-stopped --healthcheck-command /bin/sh -c 'wget --no-vebose --tries=1 --spider http://localhost:8080/readyz' --healthcheck-interval 10s --healthcheck-start-period 30s --healthcheck-retries 5 docker.io/superseriousbusiness/gotosocial:latest
Oct 15 10:17:36 mimir entrypoint.sh[1895920]: exit code: 0

    
  

A parte final, com podman create, é o systemd reiniciando o serviço. O problema está on início, onde há um crash de python: subprocess.CalledProcessError: Command '['podman', 'ps', '--filter', 'label=io.podman.compose.project=gotosocial', '-a', '--format', '{{ index .Labels "io.podman.compose.config-hash"}}']' returned non-zero exit status 125.

Eu entrava na máquina e rodava o comando pra ver o resultado:

  
❯ podman ps --filter 'label=io.podman.compose.project=gotosocial' -a --format '{{ index .Labels "io.podman.compose.config-hash"}}'
4f4b10e0c67c04b7b4f2392784b378735d4378d9d411f1405cf3819c6207bd1a
4f4b10e0c67c04b7b4f2392784b378735d4378d9d411f1405cf3819c6207bd1a    
  

E mostrava os containers rodando (porque tinha sido reiniciados pelo systemd). Eu ficava com aquela cara de "ué!?".

via GIPHY

No início do erro, tem essa outra mensagem aqui: Error: default OCI runtime "crun" not found: invalid argument . Então fui olhar se era algum problema nesse crun. E está instalado (acho que veio como dependência do podman.

  
❯ which crun
/usr/bin/crun
❯ dpkg -S /usr/bin/crun
crun: /usr/bin/crun
  

Busquei sobre erros do GoToSocial mesmo. E nada.

Olhando pra todo lado tentando descobrir o que poderia ser, reparei em outro erro: msg="RunRoot is pointing to a path (/run/user/1000/containers) which is not writable. Most likely podman will fail." .

Isso soou promissor. Então de repente o pointing path não estava disponível pra escrita. Poderia ser... systemd? Com isso eu comecei a buscar algo relacionado com timeout ou user logout. Acabei encontrando o artigo abaixo:

https://www.reddit.com/r/podman/comments/192kqgf/podman_container_stops_after_user_logout/

Nesse artigo alguém comenta que pode ser uma opção de container linger. Segui a referência que tinha sobre isso.

ttps://www.freedesktop.org/software/systemd/man/latest/loginctl.html#:~:text=Enable%2Fdisable%20user%20lingering%20for,to%20run%20long%2Drunning%20services

loginctl? Faz até sentido isso. Mas o podman não deveria descrever isso na documentação? Então fui buscar e achei isso aqui:

https://docs.podman.io/en/latest/markdown/podman-system-service.1.html

Pra deixar bem ilustrado onde aparece a referência de linger na documentação:

Algo que é vital pra funcionar como serviço aparece como... exemplo??? Os caras tão de brincation uite me.

Mas no fim era isso mesmo. Bastou um sudo logictl enable-user helio pra ter o container rodando depois que eu saio da sessão.

Se eu tivesse decido rodar com docker compose, eu provavelmente não teria o mesmo problema uma vez que roda com o privilégio de root. Então fica mais essa lição aqui. E mesmo tendo lendo a documentação, sempre aparecem alguns pontos que a porra da documentação só dá um peteleco em cima e dentro dos exemplos ainda por cima.

Mas está funcionando. Minha bolha, bolha minha.

Uma bolha pra chamar de minha

Details
Written by: Helio Loureiro
Category: Blog
Published: October 03, 2025
Hits: 587

Faz algum tempo que venho pensando e criar meu próprio servidor no fediverso. O Augusto Campos postou sobre como fez a mesma coisa com uns NUC, aqueles computadores compactos. Eu fiquei empolgado com a ideia e resolvi ir pra frente.

Minha primeira ideia foi de rodar tudo num raspberry pi 5. Mas daí veio primeiro dilema: preço.

Por 1.500 coroas suecas é um pouco demais pra algo sem disco. É divertido, mas está longe do que eu queria.

Então resolvi olhar se tinha algo refurbished, pra pagar barato e ajudar a natureza (será que ajuda mesmo?). E achei algo bem interessantes na Amazon mesmo.

1.600 coroas suecas por 16 GB de RAM, 512 GB de SSD? E ainda uma CPU Xeon? E mesmo placa de vídeo???

Bora lá!

Comprei a máquina. E ela chegou ontem.

Primeira coisa que fiz, logo depois que tirei da caixa, foi abrir pra ver como estava dentro. Tem bastante espaço sobrando dentro pra aumentar a RAM.

E até mesmo espaço pra mais discos. Já estou pensando em comprar um HDD de 8 TB pra colocar também como storage pra backups.

E ainda veio uma plaquinha de vídeo marota. Coisa linda.

E como extra ainda vieram juntos teclado e mouse. Tem cara de gamer mas olhando mais de perto o teclado...


Esperava um switch mecânico blue, mas no lugar esse switch esquisito aí. Tem cara de coisa barata. Mas tem luzinha colorida. Já vale como brinde gratuito. E falando de brindes...

Veio também esse dongle bluetooth. Eu tenho um parecido no desktop que uso aqui, que roda archlinux btw.

Com o brinquedo em mão, já fui pra instalação. Fui de Ubuntu mesmo. E com zfs como filesystem.


A máquina está instalada e decidi usar o hostname como mimir. Não muito criativo, mas um símbolo nórdico de conhecimento. Tá valendo.

Eu ainda estou vendo como vou fazer tudo. A máquina está ligada e conectada na rede. Está aqui no meu quarto, ao lado da impressora. Talvez eu depois mude pra outro lugar mas como está silenciosa, então não é problema por enquanto.

Dos desafios pra subir a máquina estão as limitações de um ambiente home based: não tenho backup de nada, não tem no-break e só conectividade por IPv6. Além de que não posso ter serviço de email configurado uma vez que a porta de SMTP (25) fica bloqueada.

Também não decidi ainda o que vou rodar. Estou pensando em Pixelfed pra uma forma de postar fotos. Mas nada decidido ainda. Continuo testando.

Mas logo devo anunciar minha própria rede social. Aguardem.

Expondo logs com python, fastapi and uvicorn

Details
Written by: Helio Loureiro
Category: Python
Published: October 02, 2025
Hits: 683
  • logs
  • fastapi

Essa semana recebi uma missão: permitir acesso aos logs de um container numa VM em que a pessoa não pode conectar.

Pense em algumas opções e a que me pareceu mais apropriada foi criar um pequeno serviço com python, fastapi e uvicorn. E deixar disponível como acesso http.

Então fiz um programa bem simples:

  
#! /usr/bin/env python3

import uvicorn
from fastapi import FastAPI
from fastapi.responses import PlainTextResponse, StreamingResponse
import subprocess

# https://stackoverflow.com/questions/4417546/constantly-print-subprocess-output-while-process-is-running
def shellExec(command: list[str]):
    popen = subprocess.Popen(command, stdout=subprocess.PIPE, universal_newlines=True)
    for stdout_line in iter(popen.stdout.readline, ""):
        yield stdout_line
    popen.stdout.close()
    return_code = popen.wait()
    if return_code:
        raise subprocess.CalledProcessError(return_code, command)

def getContainerLogs():
    for line in shellExec(["docker", "logs", "ubuntu", "-f"]):
        yield  line

app = FastAPI()
@app.get("/logs", response_class=PlainTextResponse)
async def getLogs():
    return StreamingResponse(getContainerLogs(), media_type="text/plain")


if __name__ == '__main__':
    uvicorn.run(app, host="0.0.0.0", port=8080)    
  

O programa então roda o comando docker logs ubuntu -f pra ficar lendo os logs vindo do container "ubuntu". Nada muito fantástico.

E como deixar rodando?

Eu podia criar um container que pudesse acessar /var/run/docker.socket pra ler info dos containers rodando. E os logs. Mas fui pela simplicidade e só criei um serviço do systemd mesmo.

  
[Unit]
Description=Stream logs from ubuntu container
Wants=network-online.target
After=network-online.target docker.service

[Service]
User=helio
Group=hackerz
Restart=always
WorkingDirectory=/home/helio/bin
ExecStart=/home/helio/bin/stream-logs-container.py
# If running the Agent in scraping service mode, you will want to override this value with
# something larger to allow the Agent to gracefully leave the cluster. 4800s is recommend.
TimeoutStopSec=5s

[Install]
WantedBy=multi-user.target    
  

Daí bastou ativar e partir pro abraço.

  
❯ sudo systemctl enable --now stream-logs-container.service
Password:
❯ curl localhost:8080/logs
mariadb 12:38:12.20 INFO  ==> 
mariadb 12:38:12.21 INFO  ==> Welcome to the Bitnami mariadb container
mariadb 12:38:12.21 INFO  ==> Subscribe to project updates by watching https://github.com/bitnami/containers
mariadb 12:38:12.21 INFO  ==> Did you know there are enterprise versions of the Bitnami catalog? For enhanced secure software supply chain features, unlimited pulls from Docker, LTS support, or application customization, see Bitnami Premium or Tanzu Application Catalog. See https://www.arrow.com/globalecs/na/vendors/bitnami/ for more information.
mariadb 12:38:12.22 INFO  ==> 
mariadb 12:38:12.22 INFO  ==> ** Starting MariaDB setup **
mariadb 12:38:12.25 INFO  ==> Validating settings in MYSQL_*/MARIADB_* env vars
mariadb 12:38:12.26 INFO  ==> Initializing mariadb database
mariadb 12:38:12.28 INFO  ==> Updating 'my.cnf' with custom configuration
mariadb 12:38:12.29 INFO  ==> Setting slow_query_log option
mariadb 12:38:12.35 INFO  ==> Setting long_query_time option
mariadb 12:38:12.37 INFO  ==> Installing database
mariadb 12:38:13.91 INFO  ==> Starting mariadb in background
2025-10-02 12:38:13 0 [Note] Starting MariaDB 10.11.11-MariaDB source revision e69f8cae1a15e15b9e4f5e0f8497e1f17bdc81a4 server_uid RV0GswTTbCaNJgiFfL+XFbloFPM= as process 98
2025-10-02 12:38:13 0 [Note] InnoDB: Compressed tables use zlib 1.2.13
2025-10-02 12:38:13 0 [Note] InnoDB: Number of transaction pools: 1
2025-10-02 12:38:13 0 [Note] InnoDB: Using crc32 + pclmulqdq instructions
2025-10-02 12:38:14 0 [Note] InnoDB: Using Linux native AIO
2025-10-02 12:38:14 0 [Note] InnoDB: Initializing buffer pool, total size = 128.000MiB, chunk size = 2.000MiB
2025-10-02 12:38:14 0 [Note] InnoDB: Completed initialization of buffer pool
2025-10-02 12:38:14 0 [Note] InnoDB: Buffered log writes (block size=512 bytes)
2025-10-02 12:38:14 0 [Note] InnoDB: End of log at LSN=45502
2025-10-02 12:38:14 0 [Note] InnoDB: 128 rollback segments are active.
2025-10-02 12:38:14 0 [Note] InnoDB: Setting file './ibtmp1' size to 12.000MiB. Physically writing the file full; Please wait ...
2025-10-02 12:38:14 0 [Note] InnoDB: File './ibtmp1' size is now 12.000MiB.
^C
  

Como ele fica lendo sem parar os logs, é preciso um "ctrl+c" pra sair.

Fim das câimbras no pedal

Details
Written by: Helio Loureiro
Category: Blog
Published: September 21, 2025
Hits: 932
  • ciclismo
  • eletrólitos

Conformes as pedaladas foram ficando mais longas, eu comecei a ter um problema recorrente: câimbras.

Sempre depois de 60 Km e quando pedalando próximo de 20 Km/h (ou acima). E uma vez que a câimbra chega, não tem como fazer parar. Só resta pedalar bem mais devagar. E isso pode ser um problema numa pedalada planejada de 100 Km.

O que fiz? Não, não perguntei ao ChatGPT.

Mas busquei no Youtube.

E achei um vídeo falando do assunto.

Gosto bastante dos vídeos desse canal GCN. Dão várias dicas boas. E essa não foi exceção.

Eu passei a usar o que indicaram no vídeo: eletrólitos. Eu estou usando esse eletrólito aqui no momento:


https://www.tcmcykel.se/p/energi/under-aktivitet/sis-go-hydro-tabletter-berry-20x4g

Basicamente ele adiciona açúcar, pra dar energia, mantém hidratado porque é uma mistura com água, e tem um pouco de sal. E você pode até mesmo fazer em casa uma vez que a receita é a mesma que soro caseira.

E vou dizer que isso mudou muito a forma como pedalo hoje em dia. Antes eu tinha o receio de ter a câimbra no meio do caminho e ia mais leve. Agora eu estou me arrebentando de pedalar e sinto os músculos fadigados, mas nada de câimbra. E as pedaladas passaram de acima 60 Km pra mais de 100 Km.

Não sei se o eletrólito funciona da mesma forma pra todo mundo. Os motivos de câimbras podem ser diferentes pra cada pessoa. Mas acho que vale a tentativa de usar pra quem está sofrendo com isso.

  • Bugigangas que comprei no Aliexpress e deram certo
  • Monitorando a temperatura da sala dos servidores
  • As métricas do nginx no grafana
  • Expondo as métricas do nginx pro prometheus
  • Convertendo os logs do servidor web pra json

Page 1 of 37

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

Estatísticas

  • Users 2
  • Articles 482
  • Articles View Hits 3490142

Imagem aleatória