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:
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.
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
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:
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
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 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.
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...
Um pequeno vídeo juntando todas a fotos que o Google Photos mostrou com a busca por "selfie".
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é!?".
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:
Nesse artigo alguém comenta que pode ser uma opção de container linger.
Segui a referência que tinha sobre isso.
loginctl?
Faz até sentido isso.
Mas o podman não deveria descrever isso na documentação?
Então fui buscar e achei isso aqui:
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.
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.
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.
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:
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.
Já faz algum tempo que venho trocando o hábito de comprar na Amazon pelo de comprar na Aliexpress. A vantagem é que geralmente é mais barato na Aliexpress. A desvantagem é que nem tudo dá pra trocar ou devolver. E demora mais pra chegar, se bem que os chineses estão fazendo um excelente trabalho nesse ponto. E tem o fator qualidade.
Então essas compras invariavelmente te levam a adquirir coisas que não duram muito ou estragam rapidamente.
Mas isso está mudando. E muito. Então vou listar aqui coisas que comprei ultimamente e estão sendo muito úteis e a qualidade é excelente. Vou botar os links mas... na Aliexpress os vendedores podem desaparecer do dia pra noite. Faz parte do "ethos" da Aliexpress. Vamos então às dicas.
Não é lá um produto que exigem muita coisa, mas é algo que ajuda bastante nos dias de chuva pra não deixar sua cabeça molhada. E custa pouco.
Essa bomba portátil ajudou a diminuir o peso e volume da bomba elétrica USB que eu carregava antes. Eu sempre levo uma outra bomba manual pro caso de ficar sem gás já que usou o cilindro uma vez, ele não segura o gás depois.
Eu tenho uma mochila maior que levo nas viagens mais longas. Mas uso essa pras pedaladas mais próximas. Como eu tenho bagageiro, prendo nele ao invés de prender no banco.
Conforme vamos pedalando, essa fita do guidão vai gastando. Troquei por essa da Aliexpress e não tenho reclamações. Fuciona bem e a opção de cores é maior.
Quando pedalamos no verão, depois de 2 ou 3 horas, a água das garrafas viram chá. Então eu comprei essa térmica pra experimentar. E funciona! Mantém o líquido gelado por mais de 12 horas. O problema foi tamanho: diâmetro maior que o que cabia no porta-garrafa da bike. Atualmente eu carrego ela como segunda garrafa na mochina traseira.
Eu não sei muito bem como descrever essas peças de roupa de ciclismo em português que em tradução literal ficariam como "aquecedores de pernas e braços", mas são extensões de roupas pra te manter quente. Talvez não muito necessário em climas quentes como do Brasil, mas essenciais aqui na Europa nórdica. E ocupam pouco espaço na mala.
Eu uso essa camiseta como primeira camada. Não tem outra funcionalidade além da de absorver suor e evitar aquela gotícula que escorre das costas e entra no seu rego e só vai parar quando chega no seu c*. Pra quem não usa bib pra pedalar, aquela roupa de ciclismo, ou usa essa roupa com roupas de baixo (calcinha ou cueca) - o que não deveriam fazer, não é algo tão necessário assim.
Essa é a fita que uso pra fazer o acabamento da outra fita, que descrevi acima, no guidão. Usei recentemente pra cobrir o cabo do power bank, que é laranja, e deixar quase todo preto.
Como a outra garrafa térmica que comprei primeiro tem o diâmetro muito grande, acabei comprando essa. Ela é hoje em dia minha garrafa principal e segura bem a temperatura. Talvez por até 10h ou mais. E com o diâmetro menor, encaixa sem problemas no porta-garrafas da bike.
A maioria dos porta-garrafas você tira por cima a garrafa. Como eu uso um porta trecos na parte de baixo do frame da bike, pra carregar o power bank, precisei comprar um que eu pudesse tirar e colocar a garrafa de lado. Esse permite garrafas com diâmetro maior mas eu achei que a garrafa térmica de 1l não fica firme nela e resolvi não arriscar.
Item obrigatório aqui, que chove bastante. A vantagem desses paralamas é que são presos por borrachas. Então é fácil colocar e tirar.
Deve ter nome melhor que "porta-trecos" mas essa mochila que é um porta-trecos tem o diferencial de poder parafusar no frame. E como minha bike, uma Trek, tem essa entrada pros parafusos, esse porta-treco funcionou maravilhosamente.
Eu fiz um review no Aliexpress sentando a lenha nesse power bank. Fui muito rápido no meu teste pra fazer tal review. O problema é que o cabo não entra muito bem e precisa encaixar certo. Feito isso corretamente, segura bem a carga. Alimenta a minha GoPro por 12h. Até mais. Tem a funcionalidade de lâmpada, mas eu não uso. E encaixa no conector de Garmin. A parte de cima, onde coloco o computador, não é muito firme e faz barulho. Mas não cai. Nem solta.
Não é bem relacionado com a bike, mas como uso um setup Garmin, digamos que até faz parte. Eu não gosto de usar a pulseira de borracha e prefiro essas de velcro.
Acho que nem preciso descrever muito aqui. Talvez só uma nota: meu amigo comprou uma semelhante pela Temu e a qualidade parece inferior.
Também acho que não preciso descrever muito aqui. Funciona bem pra absorver o suor.
Aqui capacete não é ítem obrigatório. Mas campainha é. E essa fica legal em drop bar de road e gravel bikes.
Se realmente protege contra raios UV, eu não sei. Mas tem funcionado. Tem uns vãos nas bordas que incomodam no início. Mas depois de algumas pedaladas você acostuma e não nota. Eu não costumo botar as alças laterais atrás da orelha porque vão machucando ao longo do caminho. Eu coloco ao lado e preciso que fique firme pra não cair (o capacete ajuda a segurar). E esses óculos funcionaram bem pra isso.
Esse espelho ajuda muito quando estamos na estrada. Posso ver se quem está atrás não está distante e ver se carros estão chegando. E quando fica fechado, não fica aparente que está ali no guidão.
Esse aqui não tem um uso muito prático. É só estético mesmo. Eu tenho a tampa das válvulas de alumínio azul também. Então combina e fica charmoso.
Quando ligo o power bank ou no telefone ou na GoPro, esse é o cabo que uso. Ele estica bastante e não fica solto.
Comprei só pra combinar com a cor da bike. Funcionam bem.
Eu passei a usar esse mount de Garmin pra segurar o telefone. Fica firme. E posso encaixar o computador também. E os power banks que tenho. Podia ter em azul pra combinar com a bike.
Mais fácil pra prender o tênis.
Eu preciso trocar os pedais a cada 6 meses. Talvez até em menos tempo. Esses são baratos e leves.
São engraçadas. Mas não absorvem bem o suor. Mas são engraçadas.
Essas luvas com alcochoamento mais grosso (6mm) ajudam bastante nas pedaladas mais longas.
Como eu tenho mount de Garmin na bike, uso esse case pra segurar o telefone. Fica firme e absorve bem a trepidação.
Um dos produtos desapareceu. Então vou deixar ele aqui pra caso apareça de novo. É um power bank com lâmpada. Tenho usado mais como lâmpada frontal.
A descrição era: ThinkRider Bicycle Light For Garmin Gopro Bike Headlight Type-C Usb Charging Bike Flashlight Bicycle Lamp Black, 100-300 Lumen.
Agora não tem mais desculpa pra nãp pedalar. Bugigangas legais é que não vão faltar.
Agora dando uma olhada na Aliexpress, eu achei o powerbank com lâmpada que mencionei. Mudou de loja/fabricante mas parece o mesmo produto.
Por motivos que não cabem aqui, temos alguns servidores instalados em uma sala na empresa. E essa sala tem um ar-condicionado pra manter a temperatura sob controle.
Um belo dia estou olhando os dados no Grafana e noto que essas máquinas não reportaram dados (não temos alertas enviados pelo Grafana, mas o porquê disso fica pra um outro dia). Entro na sala e a temperatura estava simplemente... 32°C. Era verão na Suécia, que é curto mas tem seus dias bem quentes. E o hardware das máquinas desligaram pra proteção.
Entre entrar em contato com técnico do ar-condicionado e deixar a sala aberta pra ventilar, ficamos com aquele gosto amargo de não ter nenhum dado sobre a temperatura.
A solução? raspberrypi!
Ele tem um sensor que é vendido na Internet.
O sensor já chegou mas não o raspberrypi. O motivo deve ser porque compramos um modelo que funciona como KVM e tem algumas coisas a mais.
Então aqui a descrição de como botar o serviço pra funcionar em Linux.
Existe um software descrito na página do produto que aponta pro seguinte repositório no GitHub:
Mas o repositório parece abandonado. Já faz 7 anos que ninguém manda nenhum commit. E o código não funciona com a versão mais moderna do sensor.
O que fazer? Patch!
Então corrigi o programa e criei um fork do repo original.
Então temos o software pronto pra funcionar. Ou quase.
Antes é preciso corrigir as permissões de leitura e escrita do dispositivo.
E pra isso eu criei uma pequena regra no udev em
/etc/udev/rules.d/90-temperature-sensor.rules
KERNEL=="hidraw[0-9]*", SUBSYSTEM=="hidraw", SUBSYSTEMS=="usb", ATTRS{idVendor}=="3553", ATTRS{idProduct}=="a001", MODE="0666", SYMLINK+="temper"
Pegando a saída do kernel:
# dmesg | grep -i temper
[ 5.152423] usb 1-10.4: Product: TEMPer2
[ 6.769788] input: PCsensor TEMPer2 as /devices/pci0000:00/0000:00:14.0/usb1/1-10/1-10.4/1-10.4:1.0/0003:3553:A001.0005/input/input14
[ 6.826541] hid-generic 0003:3553:A001.0005: input,hidraw4: USB HID v1.11 Keyboard [PCsensor TEMPer2] on usb-0000:00:14.0-10.4/input0
[ 6.826720] input: PCsensor TEMPer2 as /devices/pci0000:00/0000:00:14.0/usb1/1-10/1-10.4/1-10.4:1.1/0003:3553:A001.0006/input/input15
[ 6.827067] hid-generic 0003:3553:A001.0006: input,hidraw5: USB HID v1.10 Device [PCsensor TEMPer2] on usb-0000:00:14.0-10.4/input1
[ 939.507362] usb 1-10.4: Product: TEMPer2
[ 939.521617] input: PCsensor TEMPer2 as /devices/pci0000:00/0000:00:14.0/usb1/1-10/1-10.4/1-10.4:1.0/0003:3553:A001.000B/input/input26
[ 939.580825] hid-generic 0003:3553:A001.000B: input,hidraw4: USB HID v1.11 Keyboard [PCsensor TEMPer2] on usb-0000:00:14.0-10.4/input0
[ 939.581808] input: PCsensor TEMPer2 as /devices/pci0000:00/0000:00:14.0/usb1/1-10/1-10.4/1-10.4:1.1/0003:3553:A001.000C/input/input27
[ 939.582035] hid-generic 0003:3553:A001.000C: input,hidraw5: USB HID v1.10 Device [PCsensor TEMPer2] on usb-0000:00:14.0-10.4/input1
É possível ver que o dispositivo aparece como dois devices:
/dev/hidraw4
e
/dev/hidraw5.
Eu tentei usar a permissão 0644 primeiro, mas essa não funcionou pra ler os dados. Então tive de mudar pra 0666 mesmo sendo algo que só lê informação.
Feita essa etapa, ainda não estamos prontos pra rodar o programa temper.py.
Ainda é preciso instalar a dependência: serial.
Se seu sistema é baseado em debian/ubuntu:
> sudo apt install -y python3-serial
Se não for, talvez seja mais fácil fazer com virtualenv.
E pra isso eu atualmente uso o uv
> uv venv venv
> source venv/bin/activate
(venv)> uv pip install serial
Tendo tudo pronto, chegamos ao momento da verdade:
(venv)> ./temper.py
Bus 001 Dev 011 3553:a001 TEMPer2_V4.1 25.6C 78.0F - 22.8C 73.1F -
Pegando a saída como JSON permite ver melhor o que é cada um desses resultados.
(venv)> ./temper.py --json
[
{
"vendorid": 13651,
"productid": 40961,
"manufacturer": "PCsensor",
"product": "TEMPer2",
"busnum": 1,
"devnum": 11,
"devices": [
"hidraw4",
"hidraw5"
],
"firmware": "TEMPer2_V4.1",
"hex_firmware": "54454d506572325f56342e3100000000",
"hex_data": "808009f64e200000800108e94e200000",
"internal temperature": 25.5,
"external temperature": 22.81
}
]
Então a primeira temperatura lida, de /dev/hidraw4, é 25.5°C interna do dispositivo.
A segunda, /dev/hidraw5, é de 22.81°C e externa, do cabo.
Temos as leituras e os dados. Como mandar isso pro Grafana?
Eu primeiramente tentei fazer em shell script e mandar o dados pro mimir, que é onde eu agrego as métricas.
Fracassei miseravelmente.
Não existe uma forma muito fácil de enviar um dados pro lá. O formato que o alloy usa é protobuf, que é um dado comprimido em snappy, etc.
Qual outra alternativa?
Expor o dado como open metric pro alloy pegar e enviar.
Pode parecer simples mas... precisamos de um servidor web pra isso.
Algo que temos fácil em python.
Então usando uvicorn e fastapi podemos ter tudo funcionando.
E é possível importar o temper.py como módulo.
E é preciso incrementar nosso virtualenv (ou pacotes) com esses pacotes:
> sudo apt install -y python3-uvicorn python3-fastapi
ou
(venv)> uv pip install uvicorn
(venv)> uv pip install fastapi
Sem mais delongas, eis aqui o código do monitor.py:
#! /usr/bin/env python3
import subprocess
import argparse
import logging
import threading
import time
try:
import temper
except ImportError as e:
print(f"Error importing temper module: {e}")
print("Make sure python3-serial is installed: sudo apt-get install python3-serial")
exit(1)
import uvicorn
from fastapi import FastAPI
from fastapi.responses import PlainTextResponse
CELSIUS = "\u2103"
DEFAULT_PORT = 8000
TEMPERATURE_MAX = 25.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:
return subprocess.getoutput(command)
except Exception as e:
logger.error(f"Error executing shell command '{command}': {e}")
return f"Error: {e}"
app = FastAPI()
# Global temperature monitor instance (will be set in main)
temperature_monitor = None
@app.get("/metrics", response_class=PlainTextResponse)
async def metrics():
if temperature_monitor is None or temperature_monitor.temperature_current is None:
return ""
temperature_current = temperature_monitor.temperature_current
logger.info(f"/metrics: {temperature_current}{CELSIUS}")
data_lines = list()
data_lines.append("#HELP server_room_temperature_celsius the room with servers current temperature")
data_lines.append("#TYPE server_room_temperature_celsius gauge")
data_lines.append(f"server_room_temperature_celsius {temperature_current}")
data_lines.append("")
return "\n".join(data_lines)
class TemperatureMonitor:
'A class that handle the temperature monitoring'
port: int = DEFAULT_PORT
temperature_max: float = TEMPERATURE_MAX
temperature_current: float|None = None
alert_lock: bool = False
def __init__(self, port=None, temperature_max=None) -> None:
if port:
self.port = port
if temperature_max:
self.temperature_max = temperature_max
def monitor(self) -> None:
th = threading.Thread(target=self.webserver)
th.daemon = True # Make it a daemon thread
th.start()
try:
while True:
self.update()
time.sleep(15)
except KeyboardInterrupt:
logger.info("Monitoring stopped by user")
except Exception as e:
logger.error(f"Error in monitoring loop: {e}")
raise
def webserver(self) -> None:
uvicorn.run(app, host="127.0.0.1", port=self.port)
def update(self) -> None:
'Read the output from the command'
try:
tp = temper.Temper().read()
if not tp or len(tp) == 0:
logger.warning("No temperature devices found")
self.temperature_current = None
return
self.temperature_current = tp[0].get('external temperature')
if self.temperature_current is not None:
logger.info(f"🌡️ current temperature: {self.temperature_current}{CELSIUS}")
else:
logger.warning("External temperature reading is None")
except Exception as e:
logger.error(f"Error reading temperature sensor: {e}")
self.temperature_current = None
if __name__ == '__main__':
parse = argparse.ArgumentParser(description="script to monitor temperature")
parse.add_argument("--loglevel", default="info", help="the logging level (default=info)")
parse.add_argument("--tempmax", type=float, default=TEMPERATURE_MAX, help="maximum temperature before raising alert")
parse.add_argument("--port", type=int, default=DEFAULT_PORT, help="port to listen the service")
args = parse.parse_args()
# Validate log level
valid_log_levels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']
if args.loglevel.upper() not in valid_log_levels:
print(f"Invalid log level: {args.loglevel}. Valid options: {', '.join(valid_log_levels)}")
exit(1)
if args.loglevel.upper() != "INFO":
logger.setLevel(args.loglevel.upper())
# Validate temperature threshold
if args.tempmax <= 0:
print("Temperature threshold must be greater than 0")
exit(1)
# Validate port number
if not (1 <= args.port <= 65535):
print("Port must be between 1 and 65535")
exit(1)
# Create temperature monitor instance
temperature_monitor = TemperatureMonitor(args.port, args.tempmax)
temperature_monitor.monitor()
Eu tenho integrado uma parte de alerta que usa outro sistema, mas removi pra deixar o código fazendo somente o que é preciso.
No alloy, adicionei as seguintes linhas:
discovery.relabel "temperature_sensor" {
targets = array.concat(
[{
__address__ = "localhost:8000",
}],
)
rule {
source_labels = ["__address__"]
target_label = "instance"
replacement = "temper"
}
}
prometheus.scrape "temperature_sensor" {
targets = discovery.relabel.temperature_sensor.output
forward_to = [prometheus.remote_write.prod.receiver]
job_name = "agent"
}
Tudo pronto.
Ou quase.
Falta rodar o monitor.py que mostrei acima como serviço.
E pra isso usamos o systemd.
Basta criar o arquivo /etc/systemd/system/temperature-monitor.service
e iniciar.
[Unit]
Description=Temperature monitoring service
After=network.target
[Service]
User=helio
Group=helio
WorkingDirectory=/home/helio/temperature-sensor
ExecStart=/home/helio/temperature-sensor/monitor.sh
Restart=always
[Install]
WantedBy=multi-user.target
O script monitor.sh é pra somente ler o virtualenv corretamente:
#! /usr/bin/env bash
die() {
echo "ERROR: $@" >&2
echo "[$(date)] exiting with error"
exit 1
}
program="$0"
root_dir=$(readlink -f $program)
root_dir=$(dirname $root_dir)
cd $root_dir
source venv/bin/activate || \
die "failed to read virtualenv"
exec ./monitor.py
E iniciando o serviço:
> sudo systemctl daemon-reload
> sudo systemctl enable --now temperature-monitor
O que resta é criar um gráfico pra métrica server_room_temperature_celsius
e partir pro abraço.
Update: [Fri Sep 12 05:30:00 PM CEST 2025] acabo de perceber que o repositório que fiz fork é na verdade um fork de outro, que parece ser bem mais completo.
Page 1 of 37