2023年12月25日月曜日

emacs の lsp-mode で The following clients were selected based on priority エラー

emacs の lsp-mode で The following clients were selected based on priority エラー

概要

emacs + lsp-mode でうまく lsp サーバが起動できない場合の対処方法を紹介します

環境

  • macOS 11.7.10
  • emacs 29.1
  • lsp-mode 20231223.811

状況

emacs で ruby ファイルを起動した際に lsp サーバが見つからずに以下のようなエラーになる場合があります

Command "steep langserver --log-level warn" is not present on the path.                                                                      
Command "stree lsp" is not present on the path.
Command "ruby-lsp" is not present on the path.
Command "srb typecheck --lsp --disable-watchman" is not present on the path.
Command "bundle exec solargraph stdio" is present on the path.
Command "semgrep lsp" is not present on the path.
Command "rubocop --lsp" is not present on the path.
Command "steep langserver --log-level warn" is not present on the path.
Command "stree lsp" is not present on the path.
Command "ruby-lsp" is not present on the path.
Command "srb typecheck --lsp --disable-watchman" is not present on the path.
Command "bundle exec solargraph stdio" is present on the path.
Command "semgrep lsp" is not present on the path.
Command "rubocop --lsp" is not present on the path.
Found the following clients for /Users/hawk/data/repo/ruby-fndb/rank/app.rb: (server-id ruby-ls, priority -1)
The following clients were selected based on priority: (server-id ruby-ls, priority -1)

よくある対処方法としては emacs を起動しているシェルで PATH に solargraph や rubocop などがない場合に emacs から lsp が起動できないケースがありますが今回はしっかり PATH に通っている状態です

ログをよく見るとコマンドのチェックが二回行われています

原因

emacs に追加した lsp セッションパス内に複数の Gemfile があるとその配下全てでコマンドのチェックを行うため親ディレクトリなどに Gemfile があると 2 つの solargraph コマンドが見つかりどちらを起動したらいいのかわからないため該当のエラーになります

対応方法

一旦セッションファイルを作成して再度正しいプロジェクトのパスでセッションを登録しましょう

これで追加する際に親プロジェクトからではなく子プロジェクトを直接登録しましょう
以下の場合はドットを入力します

  • rm ~/.emacs.d/.lsp-session-v1
  • emacs app.rb
app.rb is not part of any project.                                                                                                           
                                                                                                                                             
i ==> Import project root ~/data/repo/ruby-fndb/                                                                                             
I ==> Import project by selecting root directory interactively                                                                               
. ==> Import project at current directory /Users/hawk/data/repo/ruby-fndb/rank/                                                      
d ==> Do not ask again for the current project by adding ~/data/repo/ruby-fndb/ to lsp-session-folders-blocklist                             
D ==> Do not ask again for the current project by selecting ignore path interactively                                                        
n ==> Do nothing: ask again when opening other files from the current project                                                                
                                                                                                                                             
Select action: .

これで再度 lsp が起動されエラーにならないことを確認します

最後に

emacs + lsp で起動しない場合は大抵の場合コマンドがインストールされていなかったり参照できなかったりが多いです
今回は参照はできるが複数の lsp サーバコマンドがある場合のエラーになります

ちなみに lsp-mode の ruby モードだと最優先で実行されるのは現状だと solargraph のようです

2023年12月22日金曜日

Python から jq を使う方法

Python から jq を使う方法

概要

コマンドラインからではなく Python プログラム内で jq を使う方法を紹介します

環境

  • macOS 14.2
  • Python 3.11.6
  • jq.py 1.6.0

サンプルコード

import jq

# キーを指定して値を参照
a = {"name": "hawk", "age": 10}
print(jq.compile(".name").input_value(a).first())

# 配列をループして順番に参照
a = {"langs": ["ruby", "python", "javascript", "swift"]}
for lang in jq.compile(".langs[]").input_value(a):
    print(lang)

# 配列の長さを取得
a = {"langs": ["ruby", "python", "javascript", "swift", "elisp"]}
print(len(jq.compile(".langs[]").input_value(a).all()))

# all や first は参照した値の型を適切に返すが text は文字列として返す
print(type(jq.compile(".langs[]").input_value(a).all()))
print(type(jq.compile(".langs").input_value(a).text()))

上記をコマンドでやった場合

echo '{"name": "hawk", "age": 10}' | jq .name

echo '{"langs": ["ruby", "python", "javascript", "swift"]}' | jq '.langs[]'

echo '{"langs": ["ruby", "python", "javascript", "swift", "elisp"]}' | jq '.langs | length'

最後に

JSON のパースだけであれば Python デフォルトの json モジュールを使ってもできますが dict を直接扱うのが嫌な場合や複雑な場合には jq.py を使うことできれいに書けることがあるかもしれません

参考サイト

2023年12月21日木曜日

Ubuntu に最新の git をインストールする方法

Ubuntu に最新の git をインストールする方法

概要

いつも忘れるのでメモ

環境

  • Ubuntu 22.04
  • git 2.43.0

インストール方法

  • sudo add-apt-repository ppa:git-core/ppa
  • sudo apt update
  • sudo apt install git

動作確認

  • git --version
git version 2.43.0

更にバージョンアップする場合は

  • sudo apt update
  • sudo apt upgrade

メモ

すでに git がインストールされている場合でも apt remove git はしないで OK です

参考サイト

2023年12月18日月曜日

Prometheus の query_range で最大プロット可能なステップ数を計算する方法

Prometheus の query_range で最大プロット可能なステップ数を計算する方法

概要

前回 query_range をコールする方法を紹介しました
その際にプロット数の最大値が 11,000 個ということを紹介しました
指定した範囲に応じて最大ステップ数が変わるので今回はそれを考慮したコードを紹介します

環境

  • Python 3.11.3
  • requests 2.31.0

サンプルコード

ポイントは step = int(range / 11000) + 1 の部分です
レンジ内における最大プロット数に収まる step 数を計算します

from datetime import datetime, timedelta, timezone

import requests

proxies = {"http": "192.168.100.2:3128"}
end = datetime.now(timezone.utc)
start = end - timedelta(hours=60)
end_s = end.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
start_s = start.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
# 60時間の範囲で指定可能な最大ステップ数を計算する
# query_range のプロット数が11,000を超えないようにする
range = (end - start).total_seconds()
# 1秒分加算することで指定可能なstep数の最大値になる
step = int(range / 11000) + 1
params = {
    "query": 'instance:node_filesystem_avail:ratio{mountpoint="/var/opt/gitlab"}',
    "start": start_s,
    "end": end_s,
    "step": f"{step}s",
}
schema = "http"
hostname = "192.168.100.1"
port = "9090"
path = "/api/v1/query_range"
url = f"{schema}://{hostname}:{port}{path}"

res = requests.get(url, params=params, proxies=proxies)

# ステップ数とレンジによりプロット数が11,000を超えていないことを確認
# print(res.json())
print(f"step -> {step}s")
print(f"range -> {range}")
print(len(res.json()["data"]["result"][0]["values"]))

注意事項

上記場合一つ考慮が足りない点があります
例えば上記の場合 60 時間分のレンジ内で 11,000 プロット取れるステップ数 (20s) を計算していますがもし 60 時間分のデータがまだない場合にはもっと細かいステップ数を指定することができます

例えば 30 時間分のデータしかない場合には単純に倍のステップ数を指定することができます (10s)
これを考慮する場合には指定のレンジ内で取得したデータの実際のレンジ情報を取得しその実際のレンジ情報からステップ数を計算する必要があるので 2 回 API をコールする必要が出てきます
正確に出したい場合にはその方法が必要になるかなと思います

また scrape_interval が 30s などになっている場合はそもそもデータが 30 秒おきに格納されていることになるので step=30s 以下を指定しても結果的には step=30s と同じデータになるはずです

最後に

Promethues の UI でネットワークを見ると実際に query_range をコールしているクエリが見えるのでそれを参考にしてもいいかもしれません

2023年12月14日木曜日

Promethues の API を requests でコールする

Promethues の API を requests でコールする

概要

過去にライブラリを使った方法を紹介しました
今回は requests だけでコールしてみました

環境

  • Python 3.11.3
  • requests 2.31.0

サンプルコード

API は query_range を使います

from datetime import datetime, timedelta, timezone

import requests

proxies = {"http": "192.168.100.2:3128"}
# 30分前から現在時刻まで取得
now = datetime.now(timezone.utc)
end = now.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
start = (now - timedelta(minutes=30)).strftime("%Y-%m-%dT%H:%M:%S.%fZ")
params = {
    "query": 'instance:node_filesystem_avail:ratio{mountpoint="/var/opt/gitlab"}',
    "start": start,
    "end": end,
    "step": "60s",
}
schema = "http"
hostname = "192.168.100.1"
port = "9090"
path = "/api/v1/query_range"
url = f"{schema}://{hostname}:{port}{path}"

res = requests.get(url, params=params, proxies=proxies)
print(res.json())

注意事項

step=60s にしていますが query_range API の場合、取得できる最大プロットの数が 11,000 になるのでそれを超えなければ更に短い時間でも OK です
query API であれば制限はないようです
一週間取得する場合には 60s からでないと取得できません
また Prometheus はデフォルトだと 15 日間しかデータを保存していません

最後に

メトリックを取得するだけならかなり簡単にコールできるので専用のライブラリはいらないかもです

参考サイト

2023年12月13日水曜日

Alertmanager でノードが再起動したときにアラートを上げる方法とその効果

Alertmanager でノードが再起動したときにアラートを上げる方法とその効果

概要

ノードが再起動した際にアラートを上げるルールを紹介します
またその効果を紹介します

環境

  • Gitlab 16.3.6
  • Alertmanager 2.30.4

ルール

groups:
- name: Custom
  rules:
  - alert: NodeHasRebooted
    annotations:
      description: Node has rebooted
      summary: Node {{ (or $labels.node $labels.instance) }} has rebooted {{ $value }} seconds ago.
    expr: (time() - node_boot_time_seconds < 600) and (time() - 600 - (node_boot_time_seconds offset 10m) > 600)
    labels:
      severity: critical

効果

  • ノードが再起動した際に必ずアラートします
  • それにより再起動時に RESOLVED メールが届かないという問題が解消します
  • 再起動アラートは10分間なので10分間ノードが起動すると解消します
  • もし他のアラートが上がっている状態でノードが再起動しそのアラートがノード再起動時に解消していると RESOLVED メールが届かないことがあります
  • それをノード再起動アラートを必ず起こすことで解消メールが送信させることができます

最後に

600sec 以下だと time との差が短すぎてアラートが上がらないことがあるので少し長めに設定しましょう

参考サイト

2023年12月12日火曜日

omnibus-gitlab の AlertManager でログレベルをデバッグにする方法

omnibus-gitlab の AlertManager でログレベルをデバッグにする方法

概要

機能として提供されていないので直接ファイルを編集する必要があります

環境

  • Gitlab 16.3.6

runit ファイルの編集

--log.level=debug を追記します

  • docker cp root_gitlab_1:/opt/gitlab/service/alertmanager/run .
  • vim run
#!/bin/sh
exec 2>&1

umask 077
exec chpst -P -e /opt/gitlab/etc/alertmanager/env \
  -U gitlab-prometheus:gitlab-prometheus \
  -u gitlab-prometheus:gitlab-prometheus \
  /opt/gitlab/embedded/bin/alertmanager --web.listen-address=0.0.0.0:9093 --storage.path=/var/opt/gitlab/alertmanager/data --config.file=/var/opt/gitlab/alertmanager/alertmanager.yml --log.level=debug

あとは再度コンテナに配置し再起動します

  • docker cp run root_gitlab_1:/opt/gitlab/service/alertmanager/run
  • docker-compose exec gitlab gitlab-ctl restart alertmanager

再起動後ログが debug になっていることを確認します

  • tail -f /path/to/gitlab_mnt/log/alertmanager/current

prometheus の場合は

root_gitlab_1:/opt/gitlab/service/prometheus/run ファイルを同様に編集して配置してあげましょう

コンテナが再起動すると元に戻ってしまう

omnibus-gitlab はコンテナが再起動すると reconfigure が走ってしまいます
reconfigure が走ると内部で chef が実行されるため手動で変更した runit のファイルが上書きされログレベルの設定がなくなります

なのでそれの対処として直接 cookbooks を編集する方法も紹介します

sv-alertmanager-run.erb の編集

  • docker cp root_gitlab_1:/opt/gitlab/embedded/cookbooks/monitoring/templates/sv-alertmanager-run.erb .
  • vim sv-alertmanager-run.erb
#!/bin/sh
exec 2>&1
<%= render("mount_point_check.erb") %>
umask 077
exec chpst -P -e <%= @options[:env_dir] %> \
  -U <%= node['monitoring']['prometheus']['username'] %>:<%= node['monitoring']['prometheus']['group'] %> \
  -u <%= node['monitoring']['prometheus']['username'] %>:<%= node['monitoring']['prometheus']['group'] %> \
  /opt/gitlab/embedded/bin/alertmanager <%= @options[:flags] %> --log.level=debug
  • docker cp sv-alertmanager-run.erb root_gitlab_1:/opt/gitlab/embedded/cookbooks/monitoring/templates/sv-alertmanager-run.erb

これでコンテナが再作成 (down -> up) されない限り runit の実行ファイルに debug オプションが付与され続けます

Prometheus の場合は /opt/gitlab/embedded/cookbooks/monitoring/templates/sv-prometheus-run.erb を編集してください

最後に

omnibus-gitlab に MR して docker-compose から変更できるようにしてもいいのかもしれません

参考サイト

2023年12月8日金曜日

SSH のローカルポートフォーワードで channel 2: open failed: connect failed: Temporary failure in name resolution エラー

SSH のローカルポートフォーワードで channel 2: open failed: connect failed: Temporary failure in name resolution エラー

概要

対処方法を紹介します

環境

  • Windows10

対処方法 .ssh/config の Host と Hostname を IP 指定にする

接続できる例

Host 192.168.100.1
     Hostname 192.168.0.1
     Port 22
     User root
     IdentityFile /path/to/secret.pem
     ProxyCommand ssh proxy /bin/nc %h %p
  • ssh -N -L 5000:192.168.100.1:9090 192.168.100.1

接続できない例

Host ins01
     Hostname 192.168.0.1
     Port 22
     User root
     IdentityFile /path/to/secret.pem
     ProxyCommand ssh proxy /bin/nc %h %p
  • ssh -N -L 5000:ins01:9090 ins01

最後に

普通に ssh する場合は Host と Hostname が違っていても問題ないのですがポートフォーワードする場合は IP で指定したほうが無難のようです

2023年12月7日木曜日

Gitlab の Promethues を使って死活監視をするときに使えそうなメトリックス

Gitlab の Promethues を使って死活監視をするときに使えそうなメトリックス

概要

Gitlab 内蔵の Promethues を使って Gitlab 自体の死活監視をする方法を紹介します
使えそうなメトリックスとダメそうなメトリックスがあったのでそれも紹介します

環境

  • Gitlab 16.3.6
  • Prometheus 2.46.0

使えそうなメトリックス

  • gitlab_sli:gitlab_component_errors:rate{job="gitlab-workhorse"} > 0

が一番いいかなと思います
gitlab に定期的にアクセスして 500 が帰ってきたときの割合を返してくれるメトリックスです

これは gitlab が提供してくれる recording alert で実際の中身は sum(rate(http_request_duration_seconds_count{job="gitlab-rails",status=~"5.."}[1m])) になっています

使えなさそうなメトリック

  • avg_over_time(up[5m]) * 100 < 50

これも gitlab が提供してくれるアラートルールなのですが up を使っています
up だと各プロセスではなく Promethues が使用している targets つまり exporter のプロセス監視をしているだけなので実際の redis や postgres, rails (puma) がダウンしても exporter に影響がないためアラートがあがりません

なのでこの ServiceDown というアラートを死活監視として使うことはおすすめしません

実際にプロセス監視ができそうなのが redis_up というクエリだけで他のプロセスに関しては死活監視できそうなクエリはデフォルトの gitlab のメトリックはありませんでした

最後に

自分で blackbox_exporter を追加しても OK です
が面倒なのでデフォルトの gitlab + Promethues で死活監視できる方法を紹介しました

あとは自作の exporter を作ってもいいですがそれも面倒かなと思います

参考サイト

2023年12月6日水曜日

bundler の with と without オプションは非推奨になっていた

bundler の with と without オプションは非推奨になっていた

概要

推奨方法を紹介します
結論としては環境変数を使いましょう

環境

  • macOS 11.7.10
  • Ruby 3.2.2
  • bundler 2.3.7

今まで

  • bundle install --with development
  • bundle install --without development

これから

  • bundle config set with '' && bundle config set without 'development' && bundle install
  • bundle config set without '' && bundle config set with 'development' && bundle install

環境変数を使うことはできなさそう

BUNDLE_WITHOUT or BUNDLER_WITHOUT を設定すればいけるという記事を何個か見たのですがうまく動作しませんでした

もし BUNDLE_WITHOUT を使いたい場合は結局 bundle config set しないといけないので上記と同じ手順を踏むことになりそうです

最後に

なんか劣化したような気がします

参考サイト

2023年12月5日火曜日

bundler-audit で Gemfile 内で使っている gem の脆弱性チェックをする

bundler-audit で Gemfile 内で使っている gem の脆弱性チェックをする

概要

過去にtrivyを使って脆弱性スキャンを紹介しました
今回は gem に特化したツールを紹介します

環境

  • macOS 11.7.10
  • Ruby 3.2.2
    • bundler-audit 0.9.1

インストール

  • vim Gemfile
gem 'bundler-audit'
  • bundle config path vendor
  • bundle install

使ってみる

まずは最新の脆弱性情報を取得します

  • bundle exec bundler-audit update
Download ruby-advisory-db ...
Cloning into '/Users/user01/.local/share/ruby-advisory-db'...
remote: Enumerating objects: 11402, done.
remote: Counting objects: 100% (1519/1519), done.
remote: Compressing objects: 100% (349/349), done.
remote: Total 11402 (delta 1237), reused 1228 (delta 1160), pack-reused 9883
Receiving objects: 100% (11402/11402), 1.82 MiB | 8.14 MiB/s, done.
Resolving deltas: 100% (6446/6446), done.
ruby-advisory-db:
  advisories:   827 advisories
  last updated: 2023-11-30 12:36:04 -0800
  commit:       d821bf162550302abd1fa1fe15007f3012b76f32

ローカルにキャッシュされます

あとは実行すれば OK です

  • bundle exec bundler-audit
No vulnerabilities found

と表示された場合は特に脆弱な gem をプロジェクト内で使用していないことになります

更新とスキャンを同時に行う

  • bundle exec bundler-audit check --update

こっちを使ったほうがいいかもです

最後に

あとはこれを CI に追加すれば gem のセキュリティチェックができるようになります
当然ですが CVE ベースなのでゼロデイ攻撃などには対応していません

trivy でもできそうですがすでに bundler-audit を使っている場合はこれで十分かもしれないです

参考サイト

2023年12月4日月曜日

global な環境にインストールした gem をすべて削除する方法

global な環境にインストールした gem をすべて削除する方法

概要

Homebrew などでインストールした gem コマンドでインストールするとすべての環境に共通の場所に gem がインストールされます
まちがってグローバルな環境にいろいろな gem をインストールしてしまった場合に一旦すべてを削除する方法を紹介します

環境

  • macOS 14.1.2
  • Ruby 3.2.2

コマンド

  • gem uninstall -I -a -x --user-install --force

rbenv など使っている場合は必ず system になるパスで上記を実行しましょう

トラブルシューティング

アンインストール中に以下のようなエラーになり削除できないケースがあります

ERROR:  While executing gem ... (Gem::InstallError)
    test-unit is not installed in GEM_HOME, try:
        gem uninstall -i /opt/homebrew/Cellar/ruby/3.2.2_1/lib/ruby/gems/3.2.0 test-unit

そんな場合は素直に記載のコマンドを実行してアンインストールしましょう

  • gem uninstall -i /opt/homebrew/Cellar/ruby/3.2.2_1/lib/ruby/gems/3.2.0 test-unit

再度必要なものだけインストールする

  • gem install bundler
  • gem install solargraph
  • gem install rubocop

最後に

アンインストール後に gem list でインストール済みの gem の一覧を見ると結構あるので実は削除されていないものがあるような気もします

参考サイト

2023年12月1日金曜日

Promethues + Alertmanager + node_exporter の最小構成 docker compose

Promethues + Alertmanager + node_exporter の最小構成 docker compose

概要

メモがてら残しておきます

環境

  • Ubuntu 22.04
  • docker 24.0.4

compose.yml

  • vim compose.yml
services:
  prometheus:
    image: prom/prometheus
    container_name: prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
    ports:
      - 9090:9090
    restart: unless-stopped
    volumes:
      - ./prometheus:/etc/prometheus
      - prom_data:/prometheus
  node_exporter:
    image: quay.io/prometheus/node-exporter:latest
    container_name: node_exporter
    command:
      - '--path.rootfs=/host'
    network_mode: host
    pid: host
    restart: unless-stopped
    volumes:
      - '/:/host:ro,rslave'
  alertmanager:
    image: prom/alertmanager
    container_name: alertmanager
    volumes:
      - ./alertmanager:/etc/alertmanager
    command: "--config.file=/etc/alertmanager/alertmanager.yml"
    ports:
      - 9093:9093
    restart: unless-stopped
volumes:
  prom_data:

Promethues

  • vim promethues/prometheus.yml
alerting:
  alertmanagers:
  - static_configs:
    - targets:
      - alertmanager:9093

rule_files:
- '/etc/prometheus/*.rules'

scrape_configs:
- job_name: 'node_exporter'
  static_configs:
  - targets:
    - '192.168.100.1:9100'
  • vim prometheus/node.rules
groups:
- name: Node
  rules:
  - record: instance:node_cpus:count
    expr: count without(cpu, mode) (node_cpu{mode="idle"})

Alertmanager

  • vim alertmanager/alertmanager.yml
oute:
  receiver: default-receiver
  routes:
  - receiver: email
    matchers:
    - alertname=""
    group_wait: 30s
    group_interval: 5m
    repeat_interval: 4h
receivers:
- name: default-receiver
- name: email
  email_configs:
  - send_resolved: true
    to: your_mail_address@here
    from: your_from_address@here
    hello: localhost
    smarthost: smtp.ess.nifcloud.com:465
    auth_username: xxx
    auth_password: xxx
    headers:
      Subject: '{{ template "my.email.text.subject" . }}'
    text: '{{ template "my.email.text.body" . }}'
    require_tls: false
templates:
- /etc/alertmanager/*.tmpl
  • vim alertmanager/my_email_text.tmpl
{{ define "my.email.text.body" }}
{{ range .Alerts.Firing }}
{{ range .Annotations.SortedPairs }}
- {{ .Name }} = {{ .Value }}
{{ end }}
{{ end }}
{{ end }}

{{ define "my.email.text.subject" }}
{{ $alerts := "" }}
{{ $instance_id := (index .Alerts 0).Labels.instance_id }}
{{ range .Alerts }}
{{ $alerts = (printf "%v %v" $alerts .Labels.alertname) }}
{{ end }}
[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] ({{ $instance_id }} ->{{ $alerts }})
{{ end }}

動作確認

  • docker compose up -d
  • docker compose ps
NAME                IMAGE                                     COMMAND                  SERVICE             CREATED             STATUS                    PORTS
alertmanager        prom/alertmanager                         "/bin/alertmanager -…"   alertmanager        13 minutes ago      Up 13 minutes             0.0.0.0:9093->9093/tcp, :::9093->9093/tcp
node_exporter       quay.io/prometheus/node-exporter:latest   "/bin/node_exporter …"   node_exporter       13 minutes ago      Up 13 minutes
prometheus          prom/prometheus                           "/bin/prometheus --c…"   prometheus          13 minutes ago      Up 13 minutes             0.0.0.0:9090->9090/tcp, :::9090->9090/tcp

最後に

あとは必要に応じて node.rules ファイルや prometheus.yml でメトリックスの取得先を変更すれば OK です

my_email_text.tmpl を変更すればテキストメールのテンプレートも変更できます

2023年11月30日木曜日

Gitlab の alertmanager でテンプレートファイルを使用する方法

Gitlab の alertmanager でテンプレートファイルを使用する方法

概要

前回メール本文のテンプレートをカスタマイズする方法を紹介しました
今回は直接テンプレートを docker-compose に記載せずテンプレートファイルを使ってカスタムしてみたいと思います

環境

  • Gitlab 16.3.6
  • Alertmanager 0.25.0

my_email_html.tmpl

まずは html メールのカスタマイズからしていきます
html テンプレートのベースはこれを使います
リンクの部分を削除するのとテンプレート名を「my.email.html」という名前に変更しています

  • vim my_email_html.tmpl
{{ define "email.default.subject" }}{{ template "__subject" . }}{{ end }}
{{ define "my.email.html" }}
<!--
Style and HTML derived from https://github.com/mailgun/transactional-email-templates


The MIT License (MIT)

Copyright (c) 2014 Mailgun

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-->
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
<head>
<meta name="viewport" content="width=device-width">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>{{ template "__subject" . }}</title>
<style>
@media only screen and (max-width: 640px) {
  body {
    padding: 0 !important;
  }

  h1,
h2,
h3,
h4 {
    font-weight: 800 !important;
    margin: 20px 0 5px !important;
  }

  h1 {
    font-size: 22px !important;
  }

  h2 {
    font-size: 18px !important;
  }

  h3 {
    font-size: 16px !important;
  }

  .container {
    padding: 0 !important;
    width: 100% !important;
  }

  .content {
    padding: 0 !important;
  }

  .content-wrap {
    padding: 10px !important;
  }

  .invoice {
    width: 100% !important;
  }
}
</style>
</head>

<body itemscope itemtype="https://schema.org/EmailMessage" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; height: 100%; line-height: 1.6em; background-color: #f6f6f6; width: 100%;">

<table class="body-wrap" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; background-color: #f6f6f6; width: 100%;" width="100%" bgcolor="#f6f6f6">
  <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
    <td style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top;" valign="top"></td>
    <td class="container" width="600" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; display: block; max-width: 600px; margin: 0 auto; clear: both;" valign="top">
      <div class="content" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; max-width: 600px; margin: 0 auto; display: block; padding: 20px;">
        <table class="main" width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; background-color: #fff; border: 1px solid #e9e9e9; border-radius: 3px;" bgcolor="#fff">
          <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
            {{ if gt (len .Alerts.Firing) 0 }}
            <td class="alert alert-warning" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; vertical-align: top; font-size: 16px; color: #fff; font-weight: 500; padding: 20px; text-align: center; border-radius: 3px 3px 0 0; background-color: #E6522C;" valign="top" align="center" bgcolor="#E6522C">
              {{ .Alerts | len }} alert{{ if gt (len .Alerts) 1 }}s{{ end }} for {{ range .GroupLabels.SortedPairs }}
                {{ .Name }}={{ .Value }}
              {{ end }}
            </td>
            {{ else }}
            <td class="alert alert-good" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; vertical-align: top; font-size: 16px; color: #fff; font-weight: 500; padding: 20px; text-align: center; border-radius: 3px 3px 0 0; background-color: #68B90F;" valign="top" align="center" bgcolor="#68B90F">
              {{ .Alerts | len }} alert{{ if gt (len .Alerts) 1 }}s{{ end }} for {{ range .GroupLabels.SortedPairs }}
                {{ .Name }}={{ .Value }} 
              {{ end }}
            </td>
            {{ end }}
          </tr>
          <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
            <td class="content-wrap" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; padding: 30px;" valign="top">
              <table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
                <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
                  <td class="content-block" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; padding: 0 0 20px;" valign="top">
                  </td>
                </tr>
                {{ if gt (len .Alerts.Firing) 0 }}
                <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
                  <td class="content-block" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; padding: 0 0 20px;" valign="top">
                    <strong style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">[{{ .Alerts.Firing | len }}] Firing</strong>
                  </td>
                </tr>
                {{ end }}
                {{ range .Alerts.Firing }}
                <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
                  <td class="content-block" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; padding: 0 0 20px;" valign="top">
                    <strong style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">Labels</strong><br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
                    {{ range .Labels.SortedPairs }}{{ .Name }} = {{ .Value }}<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">{{ end }}
                    {{ if gt (len .Annotations) 0 }}<strong style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">Annotations</strong><br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">{{ end }}
                    {{ range .Annotations.SortedPairs }}{{ .Name }} = {{ .Value }}<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">{{ end }}
                  </td>
                </tr>
                {{ end }}

                {{ if gt (len .Alerts.Resolved) 0 }}
                  {{ if gt (len .Alerts.Firing) 0 }}
                <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
                  <td class="content-block" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; padding: 0 0 20px;" valign="top">
                    <br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
                    <hr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
                    <br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
                  </td>
                </tr>
                  {{ end }}
                <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
                  <td class="content-block" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; padding: 0 0 20px;" valign="top">
                    <strong style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">[{{ .Alerts.Resolved | len }}] Resolved</strong>
                  </td>
                </tr>
                {{ end }}
                {{ range .Alerts.Resolved }}
                <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
                  <td class="content-block" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; padding: 0 0 20px;" valign="top">
                    <strong style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">Labels</strong><br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
                    {{ range .Labels.SortedPairs }}{{ .Name }} = {{ .Value }}<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">{{ end }}
                    {{ if gt (len .Annotations) 0 }}<strong style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">Annotations</strong><br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">{{ end }}
                    {{ range .Annotations.SortedPairs }}{{ .Name }} = {{ .Value }}<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">{{ end }}
                  </td>
                </tr>
                {{ end }}
              </table>
            </td>
          </tr>
        </table>

        <div class="footer" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; clear: both; color: #999; padding: 20px;">
          <table width="100%" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
            <tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
            </tr>
          </table>
        </div></div>
    </td>
    <td style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top;" valign="top"></td>
  </tr>
</table>

</body>
</html>

{{ end }}

テンプレートファイルの配置

コンテナで動作する Gitlab が参照できるようにします
今回はホストマシンの一部のパスをマウントしてコンテナが参照できるようにしています

  • mkdir /path/to/opt/alertmanager/template
  • cp my_email_html.tmpl /path/to/opt/alertmanager/template

gitlab.rb

あとはテンプレートを参照するように alertmanager の設定を書き換えます

html の部分を定義した template を使うように変更します
また配置したテンプレートファイルを読み込むように alertmanager['templates'] を定義します

  • vim .gitlab.rb
alertmanager['listen_address'] = '0.0.0.0:9093'
alertmanager['receivers'] = [
  {
    name: 'email',
    email_configs: [
      to: 'your_to@mail',
      from: 'your_from@mail',
      smarthost: 'smtp.ess.nifcloud.com:465',
      auth_username: 'your_access_key',
      auth_password: 'your_secret_key',
      require_tls: false,
      headers: {
        subject: '[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] ({{ $$alerts := "" }}{{ $$instance_id := (index .Alerts 0).Labels.instance_id }}{{ range .Alerts }}{{ $$alerts = (printf "%v %v" $$alerts .Labels.alertname) }}{{ end }}{{ $$instance_id }} ->{{ $$alerts }})'
      },
      html: '{{ template "my.email.html" . }}',
      send_resolved: true
    ]
  }
]
alertmanager['routes'] = [
  {
    receiver: 'email',
    group_wait: '30s',
    group_interval: '5m',
    repeat_interval: '4h',
    matchers: [ 'instance_id = hoge' ]
  }
]
alertmanager['templates'] = ['/var/opt/gitlab/alertmanager/template/*.tmpl']
# alertmanager['default_receiver'] = 'email'

動作確認

あとは redis などを停止すればアラートがきます

  • docker-compose exec gitlab gitlab-ctl stop redis

こんな感じで alertmanager へのリンクが削除されているのが確認できると思います

テキストメールのテンプレート化

次にテキストメールでもテンプレート化してみます
テンプレート化する内容は前回の本文とタイトルの内容をテンプレート化してみます

今回は1つのテンプレートファイルに2つの define を使って本文とタイトル用のテンプレートを定義します

  • vim my_email_text.tmpl
{{ define "my.email.text.body" }}
{{ range .Alerts.Firing }}
{{ range .Annotations.SortedPairs }}
- {{ .Name }} = {{ .Value }}
{{ end }}
{{ end }}
{{ end }}

{{ define "my.email.text.subject" }}
{{ $alerts := "" }}
{{ $instance_id := (index .Alerts 0).Labels.instance_id }}
{{ range .Alerts }}
{{ $alerts = (printf "%v %v" $alerts .Labels.alertname) }}
{{ end }}
[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] ({{ $instance_id }} ->{{ $alerts }})
{{ end }}

コンテナが参照できるパスにコピーします

  • cp my_email_text.tmpl /path/to/opt/alertmanager/template

gitlab.rb でテキストメールの本文とタイトルにテンプレートを使うように変更します
html メールは無効化するのでブランクを設定します

  • vim gitlab.rb
alertmanager['listen_address'] = '0.0.0.0:9093'
alertmanager['receivers'] = [
  {
    name: 'email',
    email_configs: [
      to: 'your_to@mail',
      from: 'your_from@mail',
      smarthost: 'smtp.ess.nifcloud.com:465',
      auth_username: 'your_access_key',
      auth_password: 'your_secret_key',
      require_tls: false,
      headers: {
        subject: '{{ template "my.email.text.subject" . }}'
      html: '',
      text: '{{ template "my.email.text.body" . }}',
      send_resolved: true
    ]
  }
]
alertmanager['routes'] = [
  {
    receiver: 'email',
    group_wait: '30s',
    group_interval: '5m',
    repeat_interval: '4h',
    matchers: [ 'instance_id = hoge' ]
  }
]
alertmanager['templates'] = ['/var/opt/gitlab/alertmanager/template/*.tmpl']
# alertmanager['default_receiver'] = 'email'

あとは再起動して再度 redis を停止すればテンプレートファイル化された内容でテキストメールのアラートが届くことが確認できると思います

最後に

docker-compose に直接テンプレート構文を使うこともできますがテンプレートの内容が長くなり一行では管理しづらい場合などはテンプレートファイル化してあげるといいのかなと思います

参考サイト

2023年11月29日水曜日

Gitlab の alertmanager でメール本文をカスタマイズする方法

Gitlab の alertmanager でメール本文をカスタマイズする方法

概要

前回 routes を設定する方法を紹介しました
今回はメールの本文やタイトルをカスタムする方法を紹介します

環境

  • Gitlab 16.3.6
  • Alertmanager 0.25.0

gitlab.rb

alertmanager['receivers'] の email_configs 内にある headers.subject, text を編集します
デフォルトの html メールは無効にするためブランクを設定しておきます

メールの内容には golang のテンプレート構文が使えます
注意するのは変数の定義で golang ではダラー1つで定義しますが今回は docker-compose 内でテンプレートを定義するためダラーを2つにする必要があります

使用できるテンプレート文字列やテンプレート構文に関しては各種公式を参考にしてください

alertmanager['listen_address'] = '0.0.0.0:9093'
alertmanager['receivers'] = [
  {
    name: 'email',
    email_configs: [
      to: 'your_to@mail',
      from: 'your_from@mail',
      smarthost: 'smtp.ess.nifcloud.com:465',
      auth_username: 'your_access_key',
      auth_password: 'your_secret_key',
      require_tls: false,
      headers: {
        subject: '[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] ({{ $$alerts := "" }}{{ $$instance_id := (index .Alerts 0).Labels.instance_id }}{{ range .Alerts }}{{ $$alerts = (printf "%v %v" $$alerts .Labels.alertname) }}{{ end }}{{ $$instance_id }} ->{{ $$alerts }})'
      },
      html: '',
      text: '{{ range .Alerts.Firing }}{{ range .Annotations.SortedPairs }}  - {{ .Name }} = {{ .Value }}{{ "\n" }}{{ end }}{{ end }}',

      send_resolved: true
    ]
  }
]
alertmanager['routes'] = [
  {
    receiver: 'email',
    group_wait: '30s',
    group_interval: '5m',
    repeat_interval: '4h',
    matchers: [ 'instance_id = hoge' ]
  }
]

custom.rules

groups:
- name: Custom
  rules:
  - alert: RedisDown
    expr: avg_over_time(redis_up[5m]) * 100 < 50
    labels:
      severity: critical
      instance_id: hoge
    annotations:
      description: The Redis service is not responding for more than 50% of the time for 5 minutes.
      summary: The Redis service is not responding

動作確認

実際に redis をダウンさせるとカスタムした本文とタイトルでテキストメールが届くと思います

最後に

Gitlab 内の alertmanager のメール本文をカスタムする方法を紹介しました
golang の知識が必要になると alertmanager に送信させる json の内容も把握する必要があります

alertmanager に送信される json 情報はどこかで確認することができるのだろうか

参考サイト

2023年11月28日火曜日

amtool を使って alertmanager のテンプレートの確認をする方法

amtool を使って alertmanager のテンプレートの確認をする方法

概要

メールなどを送信する際に事前にテンプレートの内容がちゃんと展開できるか確認することができます

環境

  • Ubuntu 22.04
  • amtool 0.26.0

インストール

  • go install github.com/prometheus/alertmanager/cmd/amtool@latest

data.json

{
  "Status": "firing",
  "Alerts": [
    {
      "Status": "firing",
      "Labels": {
        "alertname": "alert1",
        "instance_id": "hoge"
      },
      "Annotations": {
        "runbook": "https://1.example.com/",
        "description": "desc1"
      }
    },
    {
      "Status": "firing",
      "Labels": {
        "alertname": "alert2",
        "instance_id": "hoge"
      },
      "Annotations": {
        "runbook": "https://2.example.com/",
        "description": "desc2"
      }
    }
  ],
  "GroupLabels": {
    "cluster": "cluster-A"
  }
}

試す

template.text に好きなテンプレート文字列を渡してテストできます
テスト用のデータは template.data で渡します

  • ~/go/bin/amtool template render --template.glob=/dev/null --template.text='{{ $alerts := "" }}{{ $instance_id := (index .Alerts 1).Labels.instance_id }}{{ range .Alerts }}{{ $alerts = (printf "%v %v" $alerts .Labels.alertname) }}{{ end }}{{ $instance_id }} ->{{ $alerts }}' --template.data=data.json

結果は以下のようになります

hoge -> alert1 alert2

最後に

サンプルは golang のテンプレートの記述に慣れていればもっと簡潔に書けるかなと思います

書いているときに一行だと大変なので複数行で記載してから一行にするといいかなと思います

参考サイト

2023年11月27日月曜日

Mac でファイルを共有する方法

Mac でファイルを共有する方法

概要

ざっくりメモ

環境

  • M2Mac mini (共有する側
  • Mac Book Air (共有される側

共有フォルダの指定

M2Mac mini 上での作業

  • 環境設定からファイル共有
  • 共有フォルダの指定
  • 共有するユーザの追加と権限の確認

共有フォルダにアクセス

Mac Book Air 上での作業

  • Finder
  • ネットワーク
  • M2Mac mini があることを確認
  • ダブルクリックし接続
  • 共有したフォルダにアクセスできることを確認

トラブルシューティング

  • 同一ネットワーク上にいるか
  • 共有したいユーザの設定があっているか、権限はあっているか

2023年11月24日金曜日

Gitlab の Promethues に独自のルールを設定する方法

Gitlab の Promethues に独自のルールを設定する方法

概要

rules ファイルを作成してコンテナ側にマウントすれば OK です
今回は簡単な独自ルールを作成して設定してみました

環境

  • Gitlab 16.3.6

custom.rules

groups:
- name: MyCustomRule
  rules:
  - alert: CpuHighUsage
    expr: instance:node_cpu_utilization:ratio > 0.5
    for: 30m
    annotations:
      description: Current cpu usage is {{ $value }}.
      summary: CPU usage exceeded 50%.

instance:node_cpu_utilization:ratio は Gitlab のルールでデフォルトで提供されている Recoding rule の一つで CPU の使用率を取得することができる値です

コンテナ内の /var/opt/gitlab/prometheus/rules/ に配置

  • cp custom.rules /mnt/opt/gitlab/prometheus/rules

ローカル領域のマウントでもボリュームのマウントでも OK です

Omnibus Gitlab のデフォルトでは /var/opt/gitlab/prometheus/rules/ 配下のルールを見るのでそこに配置しましょう

gitlab.rb

もし別の場所に rules ファイルを配置する場合は配列に追加しましょう

prometheus['rules_files'] = ['/var/opt/gitlab/prometheus/rules/*.rules']

動作確認

  • docker-compose down
  • docker-compose up -d

であとは Promethues の rules の設定を確認して追加されていることを確認しましょう

参考サイト