FreeBSDでOpenObserveクラスタ構築

elasticsearchとlogstash(とJava)に嫌気が差したので、MinIO+OpenObserveに引っ越した。

  • minioは go install github.com/minio/minio@latestでさくっと入る。
  • OpenObserveのクラスタ構成のためにetcdが必要。こいつはpkgがあるので楽ちん。
  • OpenObserveはソースを拾ってきてビルドする。rust-nightlyが必要。WebUIのコードはFreeBSDでサポートされないCypressが含まれているので、package.jsonをいじって外す。
  • 各サーバに全コンポーネントをつっこむなら簡単。そうでない場合はetcd/minioをhaproxyとかで生きているサービスに飛ばすこと。
  • MinIO

    特に書くことはない。やることはこれだけ。

    go install github.com/minio/minio@latest
    go install github.com/minio/mc@latest
    

    jail上で起動すると、root disk判定が誤判定するので、 MINIO_ROOTDISK_THRESHOLD_SIZE=1 など環境変数を設定しておく。
    もちろん保存先を間違えると大変な目に遭うので注意。

    etcd

    pkg install coreos-etcd34 で入る。
    ちなみにrc scriptがないので、自分で daemon を呼ぶ。

    起こし方は素直にhttps://etcd.io/docs/v3.5/op-guide/clustering/のガイドを読むのがいい。

    一応残しておくと、試した設定はこんな感じ。
    各ホストで調整が必要なURLが多いので注意する。
    quorumを満たしていないと起動しないことと、一度起動できちゃうと無視される設定があるので注意する。
    変だなと思ったら rm -rf /var/db/etcd して起動しなおすほうが早い。

    name: openobserve1
    data-dir: "/var/db/etcd/data"
    wal-dir: "/var/db/etcd/wal"
    strict-reconfig-check: true
    
    initial-cluster-state: new
    #initial-cluster-state: existing
    initial-cluster-token: openobserve-etcd
    initial-cluster: "openobserve1=http://10.0.0.1:2380,openobserve2=http://10.0.0.2:2380"
    initial-advertise-peer-urls: "http://10.0.0.1:2380"
    listen-peer-urls: "http://10.0.0.1:2380"
    advertise-client-urls: "http://10.0.0.1:2379"
    listen-client-urls: "http://10.0.0.1:2379,http://127.0.0.1:2379"
    

    OpenObserve

    ビルドはサーバプロセス(rust)とWebUI(node.js)の2本立て。それぞれビルドする。

    サーバ側はrustup.rsを使ってrustのnightlyを入れておく。pkg install rust-nightlyでも良さそうだが、どうもいつもcargoがコケるのでrustupを使っている。

    rust(とcargo)を入れたらサーバをビルドする。
    そのままだとsimdjsonのビルドが失敗するので、.cargo/config.toml に以下を書き加える。

    [target.x86_64-unknown-freebsd]
    rustflags = ["-C", "target-feature=+sse2,+ssse3,+sse4.1,+sse4.2"]
    

    書き加えたら、cargo build --releaseでビルドする。

    WebUIはnodeとnpmでビルドする。
    ちなみにpkg install node npm したものだが、node18-18.16.0とnpm-node18-9.7.2になっている。
    まずはビルドに必要がなく、FreeBSDがunsupportedなCypressをpackage.jsonからもぎ取っておく。diffだとこんな感じ:

    *** web/package.json.orig       Mon Sep 11 08:34:13 2023
    --- web/package.json    Mon Sep 11 08:37:21 2023
    ***************
    *** 14,21 ****
          "copy": "cd dist && mkdir src && cd src && mkdir assets && cd .. && cd .. && cp -r src/assets/* dist/src/assets/",
          "test:unit": "vitest --environment jsdom --root src/",
          "test:unit:coverage": "vitest --coverage",
    -     "test:e2e": "start-server-and-test preview :4173 'cypress run --e2e'",
    -     "test:e2e:dev": "start-server-and-test 'vite dev --port 4173' :4173 'cypress open --e2e'",
          "build-only": "vite build",
          "build-only-alpha1": "vite build --mode=alpha1",
          "build-only-cloud-prod": "vite build --mode=production",
    --- 14,19 ----
    ***************
    *** 52,59 ****
          "vuex": "^4.0.2"
        },
        "devDependencies": {
    -     "@cypress/vue": "^4.0.0",
    -     "@cypress/webpack-dev-server": "^2.0.0",
          "@quasar/vite-plugin": "^1.0.10",
          "@rushstack/eslint-patch": "^1.1.0",
          "@types/d3-hierarchy": "^3.1.2",
    --- 50,55 ----
    ***************
    *** 71,82 ****
          "@vue/test-utils": "^2.0.2",
          "@vue/tsconfig": "^0.1.3",
          "c8": "^7.11.3",
    -     "cypress": "^10.3.0",
    -     "cypress-localstorage-commands": "^2.2.1",
          "dotenv": "^16.0.3",
          "eslint": "^8.5.0",
          "eslint-config-prettier": "^8.5.0",
    -     "eslint-plugin-cypress": "^2.12.1",
          "eslint-plugin-vue": "^9.5.1",
          "fs-extra": "^11.1.1",
          "happy-dom": "^6.0.4",
    --- 67,75 ----
    

    変更したら、npm inpm run buildでビルドする。
    distに成果物が入るので、ここをnginxなどで公開する。(後述)

    それぞれビルドしたら起動する。
    WebUI側はstatic contentsなので、nginxなどでserveする。
    nginx.confはこんな感じだろうか:

    worker_processes 1;
    events {
        worker_connections 1024;
    }
    http {
        include mime.types;
    
        sendfile on;
        keepalive_timeout 65;
    
        server {
            listen 80;
            server_name openobserve.coco.local;
            location / {
                proxy_pass http://127.0.0.1:5080;
            }
            location /assets {
                alias /openobserve/web/dist/assets;
                index index.html;
            }
            location /web {
                alias /openobserve/web/dist;
                index index.html;
            }
        }
    }
    

    サーバプロセスは環境変数を設定して起こす。
    一覧はこちら: https://openobserve.ai/docs/environment-variables/
    たとえばこんな感じ。
    ZO_LOCAL_MODE_STORAGEはたぶん要らない。ZO_TELEMETRYとZO_PROMETHEUS_ENABLEDはお好みで。
    今回はminio/etcd/openobserveを全サーバにそれぞれ入れたので127.0.0.1になっているが、そこは環境に合わせて適宜調整。

    ZO_LOCAL_MODE="false"
    ZO_LOCAL_MODE_STORAGE="s3"
    ZO_GRPC_ORG_HEADER_KEY="cluster"
    ZO_TELEMETRY="false"
    ZO_PROMETHEUS_ENABLED="true"
    ZO_ETCD_ADDR="http://127.0.0.1:2379"
    ZO_ROOT_USER_EMAIL="user@localhost.local"
    ZO_ROOT_USER_PASSWORD="super-complex-password"
    ZO_S3_ACCESS_KEY=""
    ZO_S3_SECRET_KEY=""
    ZO_S3_REGION_NAME="any"
    ZO_S3_BUCKET_NAME="openobserve"
    ZO_S3_SERVER_URL="http://127.0.0.1:9000"
    ZO_S3_PROVIDER="minio"
    

    これらをexportしたら引数なしでopenobserveを起動すればよい。

    ところでOpenObserveのクラスタ構成はガイドがなくてやや不親切。
    ただ、k8s用helm chartがあるので、こいつを見ながら設定していた。

    https://github.com/openobserve/openobserve-helm-chart/blob/main/values.yaml

    OpenObserveではまったところ

    ちなみにZO_GRPC_ORG_HEADER_KEYの値を設定し忘れて検索するとエラーになる。これが非常に分かりづらい。
    openobserveには以下のログが出ていた。

    [2023-09-18T01:38:46Z INFO  openobserve::service::search] service:search:enter; org_id="default" stream_type=Logs                                                                                
    [2023-09-18T01:38:46Z INFO  openobserve::service::search] service:search:cluster; org_id="default"                                                                                               
    [2023-09-18T01:38:46Z INFO  openobserve::service::search::sql] service:search:sql:new; org_id="default"                                                                                          
    [2023-09-18T01:38:46Z INFO  openobserve::service::search] get_file_list; stream_type=Logs time_level=Unset org_id="default" stream_name="default"                                                
    [2023-09-18T01:38:46Z INFO  openobserve::service::search] search->file_list: time_range: Some((1694914712941000, 1695001112941000)), num: 114, offset: 39
    [2023-09-18T01:38:46Z INFO  openobserve::service::search] service:search:cluster:grpc_search; org_id="default"
    [2023-09-18T01:38:46Z INFO  openobserve::service::search] service:search:cluster:grpc_search; org_id="default"
    [2023-09-18T01:38:46Z INFO  openobserve::service::search] service:search:cluster:grpc_search; org_id="default"
    [2023-09-18T01:38:46Z INFO  openobserve::handler::grpc::request::search] grpc:search:enter; org_id="default"
    [2023-09-18T01:38:46Z INFO  openobserve::service::search::grpc] service:search:grpc:search; org_id="default"
    [2023-09-18T01:38:46Z INFO  openobserve::service::search::sql] service:search:sql:new; org_id="default"
    [2023-09-18T01:38:46Z ERROR openobserve::service::search] search->grpc: node: 2, search err: Status { code: Unauthenticated, message: "No valid auth token", metadata: MetadataMap { headers: {"c
    ontent-type": "application/grpc", "date": "Mon, 18 Sep 2023 01:38:46 GMT"} }, source: None }                             
    [2023-09-18T01:38:46Z INFO  openobserve::service::search::grpc] service:search:grpc:in_wal; org_id="default" stream_name="default" stream_type=Logs
    [2023-09-18T01:38:46Z INFO  openobserve::service::search::grpc] service:search:grpc:in_storage; org_id="default" stream_name="default" stream_type=Logs
    [2023-09-18T01:38:46Z INFO  openobserve::service::search::grpc::storage] service:search:grpc:storage:enter; org_id="default" stream_name="default"
    [2023-09-18T01:38:46Z INFO  openobserve::service::search::grpc::storage] search->storage: org default, stream default, load file_list num 39
    [2023-09-18T01:38:46Z INFO  openobserve::service::search::grpc::storage] search->storage: org default, stream default, load files 39, scan_size 17390808, compressed_size 1598007
    [2023-09-18T01:38:46Z INFO  tracing::span] service:search:grpc:storage:cache_parquet_files;
    [2023-09-18T01:38:46Z INFO  openobserve::service::search::grpc::wal] service:search:wal:enter; org_id="default" stream_name="default"
    [2023-09-18T01:38:46Z INFO  openobserve::service::search::grpc::wal] service:search:grpc:wal:get_file_list; org_id="default" stream_name="default"
    [2023-09-18T01:38:46Z INFO  openobserve::service::search::grpc::wal] wal->search: load files 2, scan_size 34366
    [2023-09-18T01:38:46Z INFO  openobserve::service::search::grpc::wal] service:search:grpc:wal:datafusion; org_id="default" stream_name="default" stream_type=Logs
    [2023-09-18T01:38:46Z INFO  openobserve::service::search::grpc::storage] search->storage: org default, stream default, load files 39, into memory cache done
    [2023-09-18T01:38:46Z INFO  openobserve::service::search::grpc::storage] service:search:grpc:storage:datafusion; org_id="default" stream_name="default" stream_type=Logs
    [2023-09-18T01:38:46Z INFO  tracing::span] datafusion::storage::memory::list;
    [2023-09-18T01:38:46Z INFO  openobserve::service::search::datafusion::exec] Query took 0.008 seconds.
    [2023-09-18T01:38:46Z INFO  openobserve::service::search::datafusion::exec] Query all took 0.009 seconds.
    [2023-09-18T01:38:46Z INFO  openobserve::service::search::datafusion::exec] Query took 0.011 seconds.
    [2023-09-18T01:38:46Z INFO  openobserve::service::search::datafusion::exec] Query all took 0.011 seconds.
    [2023-09-18T01:38:46Z INFO  openobserve::service::search] search->grpc: result node: 3, is_querier: true, total: 0, took: 31, files: 36, scan_size: 22
    [2023-09-18T01:38:46Z INFO  openobserve::service::search] search->grpc: result node: 1, is_querier: true, total: 0, took: 41, files: 41, scan_size: 16
    [2023-09-18T01:38:46Z ERROR openobserve::handler::http::request::search] search error: ErrorCode(ServerInternalError("search node error"))
    [2023-09-18T01:38:46Z INFO  actix_web::middleware::logger] 10.33.3.28 "POST /api/default/_search?type=logs HTTP/1.0" 500 94 "141" "http://openobserve1.local/web/logstreams/stream-explore?s
    tream_name=default&stream_type=logs&org_identifier=default" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:109.0) Gecko/20100101 Firefox/117.0" 0.148536
    

    ポイントは以下のログであろう。

    [2023-09-18T01:38:46Z ERROR openobserve::service::search] search->grpc: node: 2, search err: Status { code: Unauthenticated, message: "No valid auth token", metadata: MetadataMap { headers: {"c
    ontent-type": "application/grpc", "date": "Mon, 18 Sep 2023 01:38:46 GMT"} }, source: None }                             
    [2023-09-18T01:38:46Z ERROR openobserve::handler::http::request::search] search error: ErrorCode(ServerInternalError("search node error"))
    

    openobserver-2で検索を要求した場合、openobserve-1側に以下のようなログが残った。

    [2023-09-18T01:39:50Z INFO  openobserve::handler::grpc::auth] Err authenticating Invalid value provided for the HTTP Authorization header