oss o11y 最強セット

題名の通り。 o11yを実施するためのoss最強セットです。 構築していきたいと思います。

環境

hugo_server : Grafana, Gateway

hugo_server -> jhonny : Openvpn tunneling

jhonny : Prometheus, Opensearch, (maybe kafka?)

集めたいデータ

GSLB周り

  1. GSLBのHCデータ
  2. GSLBのアクセスログ
  3. GSLB clusterの各ノードの状態

openvpn周り

  1. vpnserverのログ
  2. vpnserverのステータス

構築

Grafana

  1. apt install grafana-serverでインストール
  2. sudo systemctl enable grafana-serverでデーモン化
  3. sudo vim /etc/grafana/grafana.ini でホストヘッダーを設定
  4. sudo vim /etc/nginx/nginx.confでプロキシの設定
  5. grafanaにアクセス。初期ユーザ、パスワード:admin,admin

prometheus

  1. 公式からprometheusのbinary + 設定ファイルのtarをダウンロード ref
  2. 設定ファイルやbinaryを移動する /etc/prometheus/prometheus.yml、/var/lib/prometheus/data/、
  3. ユニットファイルを作る(/lib/systemd/system/prometheus.service)
[Unit]
Description=Prometheus Server
Wants=network-online.target
After=network-online.target

[Service]
User=root
Group=root
ExecStart=/usr/local/bin/prometheus \
  --config.file=/etc/prometheus/prometheus.yml \
  --storage.tsdb.path=/var/lib/prometheus/data \
  --web.listen-address=0.0.0.0:9090

Restart=always
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target
  1. sudo systemctl daemon-reload
  2. sudo systemctl start prometheus

という感じですね。完璧ですー。

prometheusの設定ファイル

  1. 基本設定 詳細はこちらを見てほしいのですが 大事なのだけ持ってきますかね。 prometheusはhttp epにスクレイピングをしに行くpull型のmetricsデータベース。 なので、prometheus.yamlにどのEPをスクレイピングしに行くかを書く必要がある。
scrape_configs:
- job_name: node
  static_configs:
  - targets: ['localhost:9100']

こんな感じだね。scraper_configsを書いて、その中にtargetsを書いてという感じ。 job_name。これは大事ね。promethesuではあるデータに対して次々とラベルを張っていくんだよね。 例えば、targetsもラベル。これが複数ある時、例えば今回はlocalhost:9100だけど、これが複数になってくるとこれを指定しないとデータが書けないので バグる

rate(node_disk_written_bytes_total[1m])/1024/1024

これと以下は同じ

rate(node_disk_written_bytes_total{device="dm-0", instance="localhost:9100", job="node"}[1m])/1024/1024

はい、そんな感じですー。ラベルが増えると、そのラベルの軸で複数の値が取れることになるから。一つに絞らないといけなくなるってことですね。 理解です。

  1. retentionの設定方法 以下の記述があるので、デフォルトでは15日間保存してくれるみたいだね
storage.tsdb.retention.time: How long to retain samples in storage.
If neither this flag nor storage.tsdb.retention.size is set, 
the retention time defaults to 15d. 
Supported units: y, w, d, h, m, s, ms.
  1. 認証が必要なEPからのスクレイピング方法

こんな感じでベーシック認証を使った方法

scrape_configs:
  - job_name: "example"
    metrics_path: "/metrics"
    static_configs:
      - targets: ["example.com:9090"]
    basic_auth:
      username: "user"
      password: "password"

mTLSを使った方法など

scrape_configs:
  - job_name: "example"
    scheme: https
    static_configs:
      - targets: ["example.com:9090"]
    tls_config:
      ca_file: "/etc/prometheus/ca.crt"
      cert_file: "/etc/prometheus/client.crt"
      key_file: "/etc/prometheus/client.key"

まあいろんな方法があるってこと 認証はEP側で書けないといけない。これが少し大変かもね。

prometheusにpushする方法

prometheusはpull型の時系列データ監視システムだけど、ここにpushすることもできるんだよね。 これはだいぶ便利だよね。やり方を教えます。

prometheus.ymlに以下を書きます。

remote_write:
  - url: "http://prometheus:9090/api/v1/write"

OpenSearchのインストール

Docker composeで一発です。以下でまず、compose.ymlをインストールします。

wget https://raw.githubusercontent.com/opensearch-project/documentation-website/2.19/assets/examples/docker-compose.yml

.envにadmin用のパスワードを書き込みます

OPENSEARCH_INITIAL_ADMIN_PASSWORD=password

/etc/sysctlに以下を書き込みます。

echo "vm.max_map_count=262144" | sudo tee -a /etc/sysctl.conf

javaのヒープのサイズが小さすぎるので大きくします。

この時minとmaxの大きさが同じでないとエラーが発生します。

vim docker-compose.yml
      - bootstrap.memory_lock=true # Disable JVM heap memory swapping
      - "OPENSEARCH_JAVA_OPTS=-Xms8g -Xmx8g" # Set min and max JVM heap sizes to at least 50% of system RAM

bootstrap.memory_lockが関係なるのかな?

起動します。

ray@jhonny:~/opensearch$ sudo nerdctl compose -f ./docker-compose.yml up -d 

起動したら以下のポートでdashboardのログイン画面に行けます。

http://192.168.1.7:5601/

さあて、opensearchの構築が終わったわけだが、ここからですね。

そもそもOpensearchで何ができるのか??

Opensearchの一番の使い道は全文検索です。 chatGPTが競馬の例で出してくれたから非常にわかりやすかった。 オルフェーブルについて検索したいとしよう。

まず例としてmysqlで検索をかけてみよう。 mysqlで検索をかけたいときには以下のように select * from horses where name = ${inputdata} で返すよね。しかしね、これ完全一致じゃないですか? likeで検索とかもできるけど、これも精度はたかが知れている。 完全一致でないと検索がうまくいかないシステムはよくあるけど、正直くそだよね。

そこでopensearchですよ。opensearchでは全文検索をかけて一致度を返してくれる。 はずれはない。それがすごい。 で、opensearchのindexには以下のように { horsename : “オルフェーブル”, id : “1” } みたいな感じで、idを入れておくのよね。 全文検索でヒットしたレコードのidがわかれば後はそのidで

select * from horses where id = id

ってとってこられるのよ。 これはすごい話ですわ。めっちゃ便利ですわ。

初期設定など

はい、上にも書いたかな?.evnでadminの初期パスワードを決められる。 で、ハンバーガマークから、Management > Security > Internal users で新しいユーザを登録できます。 backend role でadminロールを新しいユーザに付与すればこのユーザはもうadminと同じ権限を 持っています。完璧ですね。

基本的な概念とその説明

mysqlとの比較で

MySQLOpenSearch説明
DatabaseIndexMySQLのデータベースは、OpenSearchのインデックスに相当します。データはインデックス単位で管理されます。
TableType(非推奨)/ Document以前はTypeという概念がありましたが、現在は非推奨。基本的にMySQLのテーブルに相当するものはなく、インデックス内に直接JSONドキュメントを保存します。
RowDocumentMySQLのレコードに相当するのが、OpenSearchのドキュメント(JSON形式のデータ)です。
ColumnFieldMySQLのカラムは、OpenSearchのフィールド(JSONのキー)に相当します。
SQL QueryDSL (Domain Specific Language)OpenSearchはSQLではなく、専用のクエリDSL(JSON形式)を使用してデータを検索します。SQLプラグインを使えばSQLライクなクエリも可能。
Primary Key_idMySQLの主キーに相当するのがOpenSearchの_id(デフォルトで自動生成される一意のID)です。
Indexing (INSERT)Index APIMySQLのINSERTは、OpenSearchのIndex APIを使ってデータを追加します。
SELECTSearch APIMySQLのSELECTは、OpenSearchのSearch APIで実行します。
UPDATEUpdate APIMySQLのUPDATEに相当しますが、ドキュメント単位の更新が基本です。
DELETEDelete APIMySQLのDELETEと同様に、ドキュメント単位またはクエリベースでデータを削除できます。

indexの作り方

ハンバーガーマーク > Management > Index Management からIndexを作れます。

"my_index" → インデックスの名前(MySQLでいうデータベース名)
"number_of_shards": 1 → シャード(データ分割)の数
"number_of_replicas": 1 → レプリカ(バックアップ)の数
"mappings" → フィールドの型定義(MySQLのカラム定義に近い)

mappingsはoptionalです。会社でもいろいろと動的に変えられたよね。

はい、“mappings”(マッピング)は定義しなくても大丈夫です。 OpenSearchは動的マッピング(Dynamic Mapping) をサポートしており、デフォルトでは データを登録した際に自動でフィールドの型を推測して定義 します。 つまり、明示的に “mappings” を指定しなくても、最初にデータを投入すれば OpenSearch 側で適切な型を決めてくれます。

indexにデータを送信する方法

ちょっと、k8sとかをインストールしていた関係で9200 -> 9200にポートフォワーディングされなくなっていたんだけど、 その辺の設定を全部消してdockerをインストールしなおしたらうまくいくようになった。

ちなみに、dockerをインストールして走らせるためにはcontianerdが動いてないといけないので 頭に入れておくように。これで地味に時間食った。

余談はここまでにしておいてここからが本番です。 indexにログを送信する方法。まずはindex patternを作ります。 作ったindexと紐づける感じになりますね。今回でいうとtestで作ったのでそれで紐づけます。 後は、

curl -X POST "https://192.168.1.7:9200/test/_doc"      -u admin:passwordd     -H "Content-Type: application/json"      -d '{
           "level": "INFO",
           "message": "This is a test log",
           "service": "my-app"
         }' -k | jq .

で、ベーシック認証を突破するためのクレデンシャルをつけてポストするだけです。 マジでこれだけ。 しかし気を付けてほしいのが「タイムスタンプフィールドを必ず入れて送信する」ということ。 これがないとkibanaで表示されなくなります。なので、最終的に送信するログはこんな感じになると思います!!

もし @timestamp を完全に省略したい場合は、マッピングを text や keyword 型に変更することで、OpenSearch が date 型を強制しないようにできます。 これは、indexを作成するときに選べます。

ray@jhonny:~/node_exporter-1.9.0.linux-amd64$ curl -X POST "https://192.168.1.7:9200/test/_doc"      -u "admin:password"      -H "Content-Type: application/json"      -d '{
           "timestamp": "'$(date -u +"%Y-%m-%dT%H:%M:%SZ")'",   
           "level": "ERROR",
           "message": "ちゅろすうますぎぃ!!",
           "service": "Rays app"
         }' -k | jq .

いやーー、これで会社で使っているログプラットフォームと同じものが作れたのマジで熱いんですけど。 すごいですねーーー。

log収集agentのvectorからopensearchに送る方法

まあ、nginxを例にするのが一番いいですかね。

まずはnginxのログフォーマットを見てみましょう。

log_format detailed '$remote_addr|$remote_user|$time_local|'
                    '$request|$status|$body_bytes_sent|'
                    '$http_referer|$http_user_agent|'
                    '$request_time|$upstream_response_time|'
                    '$host|$server_name|'
                    '$connection|$connection_requests|'
                    '$http_x_forwarded_for|$http_x_real_ip|'
                    '$request_method|$request_uri|$server_protocol|'     
                    '$http_accept|$http_accept_language|$http_accept_encoding';

access_log /var/log/nginx/access.log detailed;

そして、対応するvector.yamlはこんな感じになります。

sources:
  nginx_logs:
    type: file
    include:
      - "/var/log/nginx/access.log"
    read_from: "end"

transforms:
  nginx_parser:
    type: remap
    inputs:
      - nginx_logs
    source: |
      parsed, err = parse_regex(.message, r'^(?P<remote_addr>[^|]*)\|(?P<remote_user>[^|]*)\|(?P<time_local>[^|]*)\|(?P<request>[^|]*)\|(?P<status>\d+)\|(?P<body_bytes_sent>\d+)\|(?P<http_referer>[^|]*)\|(?P<http_user_agent>[^|]*)\|(?P<request_time>\d+\.\d+)\|(?P<upstream_response_time>[-\d\.]*)\|(?P<host>[^|]*)\|(?P<server_name>[^|]*)\|(?P<connection>\d+)\|(?P<connection_requests>\d+)\|(?P<http_x_forwarded_for>[^|]*)\|(?P<http_x_real_ip>[^|]*)\|(?P<request_method>[^|]*)\|(?P<request_uri>[^|]*)\|(?P<server_protocol>[^|]*)\|(?P<http_accept>[^|]*)\|(?P<http_accept_language>[^|]*)\|(?P<http_accept_encoding>[^|]*)\s*$')

      if err == null {
        .timestamp = now()
        del(.message)
        . = merge(., parsed)
      } else {
        log("Failed to parse the log", level:"error")
      }

      del(.file)



sinks:
  opensearch:
    type: elasticsearch
    inputs:
      - nginx_parser
    endpoint: "https://192.168.1.7:9200"
    compression: "none"
    tls:
      verify_certificate: false
    mode: "bulk"
    bulk:
      index: "access_log_index_name"
    suppress_type_name: true
    auth:
      strategy: "basic"
      user: "admin"
      password: "password"

ついでにログローテートについて

ログローテのタイミングでaccess.log -> access.log.1にmvされて新しいaccess.logが作られ、600にパーミッションが戻ってしまう。 そうするとまたvectorが読めなくなってしまう。これの防ぎ方。

sudo vim  /etc/logrotate.d/nginx


/var/log/nginx/*.log {
        daily
        missingok
        rotate 14
        compress
        delaycompress
        notifempty
        create 0644 www-data adm
        sharedscripts
        prerotate
                if [ -d /etc/logrotate.d/httpd-prerotate ]; then \
                        run-parts /etc/logrotate.d/httpd-prerotate; \
                fi \
        endscript
        postrotate
                invoke-rc.d nginx rotate >/dev/null 2>&1
        endscript
}

create 0644に変更してください。 ログローテを強制的にするには以下のコマンド

sudo logrotate -f /etc/logrotate.d/nginx